// *************************************************************************************************************************** | |
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * | |
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * | |
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * | |
// * with the License. You may obtain a copy of the License at * | |
// * * | |
// * http://www.apache.org/licenses/LICENSE-2.0 * | |
// * * | |
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * | |
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * | |
// * specific language governing permissions and limitations under the License. * | |
// *************************************************************************************************************************** | |
package org.apache.juneau.urlencoding; | |
import static org.apache.juneau.internal.ArrayUtils.*; | |
import java.io.IOException; | |
import java.lang.reflect.*; | |
import java.util.*; | |
import org.apache.juneau.*; | |
import org.apache.juneau.internal.*; | |
import org.apache.juneau.serializer.*; | |
import org.apache.juneau.transform.*; | |
import org.apache.juneau.uon.*; | |
/** | |
* Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}. | |
* | |
* <p> | |
* This class is NOT thread safe. | |
* It is typically discarded after one-time use although it can be reused within the same thread. | |
*/ | |
@SuppressWarnings({ "rawtypes", "unchecked" }) | |
public class UrlEncodingSerializerSession extends UonSerializerSession { | |
private final UrlEncodingSerializer ctx; | |
/** | |
* Constructor. | |
* | |
* @param ctx | |
* The context creating this session object. | |
* The context contains all the configuration settings for this object. | |
* @param encode Override the {@link UonSerializer#UON_encoding} setting. | |
* @param args | |
* Runtime arguments. | |
* These specify session-level information such as locale and URI context. | |
* It also include session-level properties that override the properties defined on the bean and | |
* serializer contexts. | |
*/ | |
protected UrlEncodingSerializerSession(UrlEncodingSerializer ctx, Boolean encode, SerializerSessionArgs args) { | |
super(ctx, encode, args); | |
this.ctx = ctx; | |
} | |
/* | |
* Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. | |
*/ | |
private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { | |
ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this); | |
if (cm.isCollectionOrArray()) { | |
if (isExpandedParams()) | |
return true; | |
if (getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams()) | |
return true; | |
} | |
return false; | |
} | |
/* | |
* Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list. | |
*/ | |
private boolean shouldUseExpandedParams(Object value) { | |
if (value == null || ! isExpandedParams()) | |
return false; | |
ClassMeta<?> cm = getClassMetaForObject(value).getSerializedClassMeta(this); | |
if (cm.isCollectionOrArray()) { | |
if (isExpandedParams()) | |
return true; | |
} | |
return false; | |
} | |
@Override /* SerializerSession */ | |
protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { | |
serializeAnything(getUonWriter(out), o); | |
} | |
/* | |
* Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method. | |
*/ | |
private SerializerWriter serializeAnything(UonWriter out, Object o) throws IOException, SerializeException { | |
ClassMeta<?> aType; // The actual type | |
ClassMeta<?> sType; // The serialized type | |
ClassMeta<?> eType = getExpectedRootType(o); | |
aType = push2("root", o, eType); | |
indent--; | |
if (aType == null) | |
aType = object(); | |
sType = aType; | |
String typeName = getBeanTypeName(eType, aType, null); | |
// Swap if necessary | |
PojoSwap swap = aType.getPojoSwap(this); | |
if (swap != null) { | |
o = swap(swap, o); | |
sType = swap.getSwapClassMeta(this); | |
// If the getSwapClass() method returns Object, we need to figure out | |
// the actual type now. | |
if (sType.isObject()) | |
sType = getClassMetaForObject(o); | |
} | |
if (sType.isMap()) { | |
if (o instanceof BeanMap) | |
serializeBeanMap(out, (BeanMap)o, typeName); | |
else | |
serializeMap(out, (Map)o, sType); | |
} else if (sType.isBean()) { | |
serializeBeanMap(out, toBeanMap(o), typeName); | |
} else if (sType.isCollection() || sType.isArray()) { | |
Map m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o); | |
serializeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class)); | |
} else if (sType.isReader() || sType.isInputStream()) { | |
IOUtils.pipe(o, out); | |
} else { | |
// All other types can't be serialized as key/value pairs, so we create a | |
// mock key/value pair with a "_value" key. | |
out.append("_value="); | |
super.serializeAnything(out, o, null, null, null); | |
return out; | |
} | |
pop(); | |
return out; | |
} | |
/* | |
* Converts a Collection into an integer-indexed map. | |
*/ | |
private static Map<Integer,Object> getCollectionMap(Collection<?> c) { | |
Map<Integer,Object> m = new TreeMap<>(); | |
int i = 0; | |
for (Object o : c) | |
m.put(i++, o); | |
return m; | |
} | |
/* | |
* Converts an array into an integer-indexed map. | |
*/ | |
private static Map<Integer,Object> getCollectionMap(Object array) { | |
Map<Integer,Object> m = new TreeMap<>(); | |
for (int i = 0; i < Array.getLength(array); i++) | |
m.put(i, Array.get(array, i)); | |
return m; | |
} | |
private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws IOException, SerializeException { | |
m = sort(m); | |
ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType(); | |
boolean addAmp = false; | |
for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { | |
Object key = generalize(e.getKey(), keyType); | |
Object value = e.getValue(); | |
if (shouldUseExpandedParams(value)) { | |
Iterator i = value instanceof Collection ? ((Collection)value).iterator() : iterator(value); | |
while (i.hasNext()) { | |
if (addAmp) | |
out.cr(indent).append('&'); | |
out.appendObject(key, true).append('='); | |
super.serializeAnything(out, i.next(), null, (key == null ? null : key.toString()), null); | |
addAmp = true; | |
} | |
} else { | |
if (addAmp) | |
out.cr(indent).append('&'); | |
out.appendObject(key, true).append('='); | |
super.serializeAnything(out, value, valueType, (key == null ? null : key.toString()), null); | |
addAmp = true; | |
} | |
} | |
return out; | |
} | |
private SerializerWriter serializeCollectionMap(UonWriter out, Map m, ClassMeta<?> type) throws IOException, SerializeException { | |
ClassMeta<?> valueType = type.getValueType(); | |
boolean addAmp = false; | |
for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { | |
if (addAmp) | |
out.cr(indent).append('&'); | |
out.append(e.getKey()).append('='); | |
super.serializeAnything(out, e.getValue(), valueType, null, null); | |
addAmp = true; | |
} | |
return out; | |
} | |
private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws IOException, SerializeException { | |
boolean addAmp = false; | |
for (BeanPropertyValue p : m.getValues(isTrimNullProperties(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null)) { | |
BeanPropertyMeta pMeta = p.getMeta(); | |
if (pMeta.canRead()) { | |
ClassMeta<?> cMeta = p.getClassMeta(); | |
ClassMeta<?> sMeta = cMeta.getSerializedClassMeta(this); | |
String key = p.getName(); | |
Object value = p.getValue(); | |
Throwable t = p.getThrown(); | |
if (t != null) | |
onBeanGetterException(pMeta, t); | |
if (canIgnoreValue(sMeta, key, value)) | |
continue; | |
if (value != null && shouldUseExpandedParams(pMeta)) { | |
// Transformed object array bean properties may be transformed resulting in ArrayLists, | |
// so we need to check type if we think it's an array. | |
Iterator i = (sMeta.isCollection() || value instanceof Collection) ? ((Collection)value).iterator() : iterator(value); | |
while (i.hasNext()) { | |
if (addAmp) | |
out.cr(indent).append('&'); | |
out.appendObject(key, true).append('='); | |
super.serializeAnything(out, i.next(), cMeta.getElementType(), key, pMeta); | |
addAmp = true; | |
} | |
} else { | |
if (addAmp) | |
out.cr(indent).append('&'); | |
out.appendObject(key, true).append('='); | |
super.serializeAnything(out, value, cMeta, key, pMeta); | |
addAmp = true; | |
} | |
} | |
} | |
return out; | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Properties | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Configuration property: Serialize bean property collections/arrays as separate key/value pairs. | |
* | |
* @see UrlEncodingSerializer#URLENC_expandedParams | |
* @return | |
* <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. | |
* <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. | |
*/ | |
protected final boolean isExpandedParams() { | |
return ctx.isExpandedParams(); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Extended metadata | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Returns the language-specific metadata on the specified class. | |
* | |
* @param cm The class to return the metadata on. | |
* @return The metadata. | |
*/ | |
protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { | |
return ctx.getUrlEncodingClassMeta(cm); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Other methods | |
//----------------------------------------------------------------------------------------------------------------- | |
@Override /* Session */ | |
public ObjectMap toMap() { | |
return super.toMap() | |
.append("UrlEncodingSerializerSession", new DefaultFilteringObjectMap() | |
); | |
} | |
} |