Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package net.peierls.util.restlet;
- import com.google.common.base.CharMatcher;
- import com.google.common.base.Functions;
- import com.google.common.base.Predicate;
- import com.google.common.base.Splitter;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.Iterables;
- import com.google.common.collect.Ordering;
- import com.google.common.collect.PeekingIterator;
- import static com.google.common.collect.Iterators.peekingIterator;
- import static com.google.common.collect.Lists.newArrayList;
- import static com.google.common.collect.Maps.newLinkedHashMap;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import javax.inject.Inject;
- import org.codehaus.jackson.map.ObjectMapper;
- import org.restlet.data.Form;
- import org.restlet.util.NamedValue;
- /**
- * Converts HTML form representation (application/www-form-urlencoded)
- * to POJO.
- */
- public class FormDeserializer {
- /**
- * Creates a form deserializer that will use the give object
- * mapper to convert the basic structure to a typed target
- * object and to deserialize the values of that structure
- * from strings.
- */
- @Inject public FormDeserializer(ObjectMapper objectMapper) {
- this.objectMapper = objectMapper;
- }
- /**
- * Converts a Form to a Java object of the target type, using
- * each {@code name=value} pair to set the corresponding property
- * on the target object.
- * <p>
- * Compound names, using period (.) as the delimiter, are treated
- * as pseudo-dereferences (a la JavaScript or Groovy) to set properties
- * of sub-objects, e.g., {@code a.b=c} for bean targets is treated
- * like a call to {@code target.getA().setB(c)}.
- * <p>
- * Numeric components of compound names are treated as indices
- * into a sequence named by the preceding components, e.g.,
- * {@code a.1=c} is treated as {@code target.getA()[1] = c}
- * (or {@code target.getA().set(1, c)}, if the "a" property of
- * the target is a list rather than an array).
- * Unless an element with index 0 is set, the indices are origin 1.
- * <p>
- * Sequences are also created by names with multiple values, e.g.,
- * {@code a=x&a=y} is equivalent to {@code a.1=x&a.2=y}, with the
- * value of the "a" property in the target being a sequence of two
- * values.
- * <p>
- * When a name appears both indexed and non-indexed, the last
- * assignment wins: {@code a=x&a.1=a1&a.2=a2} will set the "a"
- * property to a sequence of values {@code [a1, a2]}, but
- * {@code a.1=a1&a.2=a2&a=x} will set the "a" property to {@code x}.
- * <p>
- * If the top level consists only of integer indices, the subobjects
- * will be interpreted as elements of a sequence (and T must be a List
- * or List subtype instead).
- * <p>
- * The values are deserialized using the Jackson ObjectMapper that was
- * used to construct this FormDeserializer.
- * Any type that can be deserialized from a string can be used.
- * While it is possible for Jackson to deserialize object graphs
- * that have internal references, it is not possible for the form
- * values to refer outside of themselves.
- * <p>
- * Runtime exceptions from Jackson conversion are propagated without
- * exception translation. This could be considered a bug.
- * @param source the Restlet Form object to be deserialized
- * @param targetType the type of the target object into which the form
- * is to be deserialized.
- * @return the deserialized object of the target type
- */
- public <T> T deserialize(Form source, Class<T> targetType) {
- Map<String, Object> rootMap = newLinkedHashMap();
- for (NamedValue<String> namedValue : source) {
- String name = namedValue.getName();
- String value = namedValue.getValue();
- Iterable<String> names = ON_PERIODS.split(name);
- if (!Iterables.isEmpty(names) && Iterables.all(names, NON_BLANK)) {
- addToMap(rootMap, peekingIterator(names.iterator()), value);
- }
- }
- removeNullPrefix(rootMap);
- List<Object> rootList = numericKeysToList(rootMap);
- if (rootList == null) {
- return objectMapper.convertValue(rootMap, targetType);
- } else {
- return objectMapper.convertValue(rootList, targetType);
- }
- }
- private static void addToMap(Map<String, Object> map,
- PeekingIterator<String> names, String value) {
- assert names.hasNext() : "empty name list in call to addToMap";
- String name = names.next();
- Object oldValue = map.get(name);
- // Decide whether this is a leaf node, in which case we can
- // assign the value, or an internal node.
- if (names.hasNext()) {
- // This is an internal node, so the thing referred to by
- // name is either a List or a Map, depending on whether
- // the next name is an index or a name.
- String nextKey = names.peek();
- if (NUMERIC.apply(nextKey)) {
- // The next name is an index, so we make sure
- // the thing referred to by name is a list, and
- // then we call addToList.
- ArrayList<Object> values;
- if (oldValue instanceof ArrayList) {
- // Already have a list in place.
- @SuppressWarnings("unchecked")
- ArrayList<Object> tmp = (ArrayList<Object>) oldValue;
- values = tmp;
- } else {
- // Nothing here, or something that isn't indexable,
- // so we replace with list.
- values = newArrayList();
- map.put(name, values);
- }
- int index = Integer.valueOf(names.next());
- addToList(values, index, names, value);
- } else {
- // The next name is a name, not an index, so
- // we make sure the value at this name is a map.
- Map<String, Object> subMap;
- if (oldValue instanceof Map) {
- // It's a map already.
- @SuppressWarnings("unchecked")
- Map<String, Object> tmp = (Map<String, Object>) oldValue;
- subMap = tmp;
- } else {
- // Whatever is there is not a map, so we replace
- // it with an empty map.
- subMap = newLinkedHashMap();
- map.put(name, subMap);
- }
- // Now we can recursively add. We only peeked
- // at next name; we haven't consumed it.
- addToMap(subMap, names, value);
- }
- } else {
- // This is a leaf node, so we can put the new value,
- // but we handle things differently depending on
- // whether there is an existing value (and what type
- // it is, if so).
- if (oldValue == null) {
- // Nothing there, so we add it.
- map.put(name, value);
- } else if (oldValue instanceof Map) {
- // There's an existing map. We could go either way
- // on this, but our policy is last thing in wins,
- // so we replace the map with the new scalar value.
- map.put(name, value);
- } else {
- // There's an existing value that is either scalar
- // or a list. Either way this name is going to be
- // associated with a list.
- if (oldValue instanceof ArrayList) {
- // Existing value is a list, so we add to it.
- @SuppressWarnings("unchecked")
- ArrayList<Object> values = (ArrayList<Object>) oldValue;
- values.add(value);
- } else {
- // Existing value is a scalar, so we convert
- // the value at this name to a list containing
- // the existing value and the new value.
- map.put(name, newArrayList(oldValue, value));
- }
- }
- }
- }
- private static void addToList(ArrayList<Object> list, int index,
- PeekingIterator<String> names, String value) {
- // We are going to be putting something at the given index, so
- // we add nulls to make sure there's a slot of it.
- while (list.size() <= index) {
- list.add(null);
- }
- Object oldValue = list.get(index);
- // Decide whether this is a leaf node, in which case we can
- // assign the value, or an internal node.
- if (names.hasNext()) {
- String nextKey = names.peek();
- if (NUMERIC.apply(nextKey)) {
- // The next name is an index, so we make sure
- // the thing at the given index is a list, and
- // then we call addToList.
- ArrayList<Object> values;
- if (oldValue instanceof ArrayList) {
- // Already have a list in place.
- @SuppressWarnings("unchecked")
- ArrayList<Object> tmp = (ArrayList<Object>) oldValue;
- values = tmp;
- } else {
- // Nothing here, or something that isn't indexable,
- // so we replace with list.
- values = newArrayList();
- list.set(index, values);
- }
- int subIndex = Integer.valueOf(names.next());
- addToList(values, subIndex, names, value);
- } else {
- // The next name is a name, not an index, so
- // we make sure the value at this index is a map.
- Map<String, Object> subMap;
- if (oldValue instanceof Map) {
- // It's a map already.
- @SuppressWarnings("unchecked")
- Map<String, Object> tmp = (Map<String, Object>) oldValue;
- subMap = tmp;
- } else {
- // Whatever is there is not a map, so we replace
- // it with an empty map.
- subMap = newLinkedHashMap();
- list.set(index, subMap);
- }
- // Now we can recursively add. We only peeked
- // at next name; we haven't consumed it.
- addToMap(subMap, names, value);
- }
- } else {
- // This is a leaf node, so we can set the new value,
- // but we handle things differently depending on
- // whether there is an existing value (and what type
- // it is, if so).
- if (oldValue == null) {
- // Nothing there, so we set it.
- list.set(index, value);
- } else if (oldValue instanceof Map) {
- // There's an existing map. We could go either way
- // on this, but our policy is last thing in wins,
- // so we replace the map with the new scalar value.
- list.set(index, value);
- } else {
- // There's an existing value that is either scalar
- // or a list. Either way this name is going to be
- // associated with a list.
- if (oldValue instanceof ArrayList) {
- // Existing value is a list, so we add to it.
- @SuppressWarnings("unchecked")
- ArrayList<Object> values = (ArrayList<Object>) oldValue;
- values.add(value);
- } else {
- // Existing value is a scalar, so we convert
- // the value at this name to a list containing
- // the existing value and the new value.
- list.set(index, newArrayList(oldValue, value));
- }
- }
- }
- }
- /**
- * Recursively removes the 0th (first) element of every
- * list within a value when that element is null. This
- * handles the question of 0-origin vs. 1-origin indexing.
- */
- private static void removeNullPrefix(Object value) {
- if (value instanceof ArrayList) {
- @SuppressWarnings("unchecked")
- ArrayList<Object> list = (ArrayList<Object>) value;
- if (list.get(0) == null) {
- list.remove(0);
- }
- for (Object v : list) {
- removeNullPrefix(v);
- }
- } else if (value instanceof Map) {
- @SuppressWarnings("unchecked")
- Map<String, Object> map = (Map<String, Object>) value;
- for (Object v : map.values()) {
- removeNullPrefix(v);
- }
- }
- }
- /**
- * If all of the given map's names are numeric, converts this map
- * into a list by sorting the names and returning a list of the
- * values in name order. Returns null otherwise. This means that
- * missing indices are ignored, but this is only called at the
- * top level, where it makes no sense to have gaps.
- */
- private static List<Object> numericKeysToList(final Map<String, Object> map) {
- if (Iterables.all(map.keySet(), NUMERIC)) {
- return ImmutableList.copyOf(Iterables.transform(
- Ordering.natural().sortedCopy(map.keySet()),
- Functions.forMap(map)
- ));
- } else {
- return null;
- }
- }
- private final ObjectMapper objectMapper;
- private static final Splitter ON_PERIODS = Splitter.on('.').trimResults();
- private static final Predicate<String> NON_BLANK = new Predicate<String>() {
- public boolean apply(String s) {
- return !s.isEmpty();
- }
- };
- private static final Predicate<String> NUMERIC = new Predicate<String>() {
- public boolean apply(String s) {
- return CharMatcher.DIGIT.matchesAllOf(s);
- }
- };
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement