blob: 2ae2f0ecfb1952a11ff382b11d156d3b5c9e31b2 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed 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 com.google.gwt.autobean.shared;
import com.google.gwt.autobean.shared.impl.EnumMap;
import com.google.gwt.autobean.shared.impl.LazySplittable;
import com.google.gwt.autobean.shared.impl.StringQuoter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
/**
* Utility methods for encoding an AutoBean graph into a JSON-compatible string.
* This codex intentionally does not preserve object identity, nor does it
* encode cycles, but it will detect them.
*/
public class AutoBeanCodex {
static class Decoder extends AutoBeanVisitor {
private final Stack<AutoBean<?>> beanStack = new Stack<AutoBean<?>>();
private final Stack<Splittable> dataStack = new Stack<Splittable>();
private AutoBean<?> bean;
private Splittable data;
private final AutoBeanFactory factory;
public Decoder(AutoBeanFactory factory) {
this.factory = factory;
}
@SuppressWarnings("unchecked")
public <T> AutoBean<T> decode(Splittable data, Class<T> type) {
push(data, type);
bean.accept(this);
return (AutoBean<T>) pop();
}
@Override
public boolean visitCollectionProperty(String propertyName,
AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
if (data.isNull(propertyName)) {
return false;
}
Collection<Object> collection;
if (List.class.equals(ctx.getType())) {
collection = new ArrayList<Object>();
} else if (Set.class.equals(ctx.getType())) {
collection = new HashSet<Object>();
} else {
throw new UnsupportedOperationException("Only List and Set supported");
}
boolean isValue = ValueCodex.canDecode(ctx.getElementType());
boolean isEncoded = Splittable.class.equals(ctx.getElementType());
Splittable listData = data.get(propertyName);
for (int i = 0, j = listData.size(); i < j; i++) {
if (listData.isNull(i)) {
collection.add(null);
} else {
if (isValue) {
collection.add(decodeValue(ctx.getElementType(), listData.get(i)));
} else if (isEncoded) {
collection.add(listData.get(i));
} else {
collection.add(decode(listData.get(i), ctx.getElementType()).as());
}
}
}
ctx.set(collection);
return false;
}
@Override
public boolean visitMapProperty(String propertyName,
AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
if (data.isNull(propertyName)) {
return false;
}
Map<?, ?> map;
if (ValueCodex.canDecode(ctx.getKeyType())) {
map = decodeValueKeyMap(data.get(propertyName), ctx.getKeyType(),
ctx.getValueType());
} else {
map = decodeObjectKeyMap(data.get(propertyName), ctx.getKeyType(),
ctx.getValueType());
}
ctx.set(map);
return false;
}
@Override
public boolean visitReferenceProperty(String propertyName,
AutoBean<?> value, PropertyContext ctx) {
if (data.isNull(propertyName)) {
return false;
}
if (Splittable.class.equals(ctx.getType())) {
ctx.set(data.get(propertyName));
return false;
}
push(data.get(propertyName), ctx.getType());
bean.accept(this);
ctx.set(pop().as());
return false;
}
@Override
public boolean visitValueProperty(String propertyName, Object value,
PropertyContext ctx) {
if (!data.isNull(propertyName)) {
Object object;
Splittable propertyValue = data.get(propertyName);
Class<?> type = ctx.getType();
object = decodeValue(type, propertyValue);
ctx.set(object);
}
return false;
}
private Map<?, ?> decodeObjectKeyMap(Splittable map, Class<?> keyType,
Class<?> valueType) {
boolean isEncodedKey = Splittable.class.equals(keyType);
boolean isEncodedValue = Splittable.class.equals(valueType);
boolean isValueValue = Splittable.class.equals(valueType);
Splittable keyList = map.get(0);
Splittable valueList = map.get(1);
assert keyList.size() == valueList.size();
Map<Object, Object> toReturn = new HashMap<Object, Object>(keyList.size());
for (int i = 0, j = keyList.size(); i < j; i++) {
Object key;
if (isEncodedKey) {
key = keyList.get(i);
} else {
key = decode(keyList.get(i), keyType).as();
}
Object value;
if (valueList.isNull(i)) {
value = null;
} else if (isEncodedValue) {
value = keyList.get(i);
} else if (isValueValue) {
value = decodeValue(valueType, keyList.get(i));
} else {
value = decode(valueList.get(i), valueType).as();
}
toReturn.put(key, value);
}
return toReturn;
}
private Object decodeValue(Class<?> type, Splittable propertyValue) {
return decodeValue(type, propertyValue.asString());
}
private Object decodeValue(Class<?> type, String propertyValue) {
Object object;
if (type.isEnum() && bean.getFactory() instanceof EnumMap) {
// The generics kind of get in the way here
@SuppressWarnings({"unchecked", "rawtypes"})
Class<Enum> enumType = (Class<Enum>) type;
@SuppressWarnings("unchecked")
Enum<?> e = ((EnumMap) bean.getFactory()).getEnum(enumType,
propertyValue);
object = e;
} else {
object = ValueCodex.decode(type, propertyValue);
}
return object;
}
private Map<?, ?> decodeValueKeyMap(Splittable map, Class<?> keyType,
Class<?> valueType) {
Map<Object, Object> toReturn = new HashMap<Object, Object>();
boolean isEncodedValue = Splittable.class.equals(valueType);
boolean isValueValue = ValueCodex.canDecode(valueType);
for (String encodedKey : map.getPropertyKeys()) {
Object key = decodeValue(keyType, encodedKey);
Object value;
if (map.isNull(encodedKey)) {
value = null;
} else if (isEncodedValue) {
value = map.get(encodedKey);
} else if (isValueValue) {
value = decodeValue(valueType, map.get(encodedKey));
} else {
value = decode(map.get(encodedKey), valueType).as();
}
toReturn.put(key, value);
}
return toReturn;
}
private AutoBean<?> pop() {
dataStack.pop();
if (dataStack.isEmpty()) {
data = null;
} else {
data = dataStack.peek();
}
AutoBean<?> toReturn = beanStack.pop();
if (beanStack.isEmpty()) {
bean = null;
} else {
bean = beanStack.peek();
}
return toReturn;
}
private void push(Splittable data, Class<?> type) {
this.data = data;
bean = factory.create(type);
if (bean == null) {
throw new IllegalArgumentException(
"The AutoBeanFactory cannot create a " + type.getName());
}
dataStack.push(data);
beanStack.push(bean);
}
}
static class Encoder extends AutoBeanVisitor {
private EnumMap enumMap;
private Set<AutoBean<?>> seen = new HashSet<AutoBean<?>>();
private Stack<StringBuilder> stack = new Stack<StringBuilder>();
private StringBuilder sb;
public Encoder(AutoBeanFactory factory) {
if (factory instanceof EnumMap) {
enumMap = (EnumMap) factory;
}
}
@Override
public void endVisit(AutoBean<?> bean, Context ctx) {
if (sb.length() == 0) {
// No properties
sb.append("{");
} else {
sb.setCharAt(0, '{');
}
sb.append("}");
}
@Override
public void endVisitReferenceProperty(String propertyName,
AutoBean<?> value, PropertyContext ctx) {
StringBuilder popped = pop();
if (popped.length() > 0) {
sb.append(",\"").append(propertyName).append("\":").append(
popped.toString());
}
}
@Override
public boolean visitCollectionProperty(String propertyName,
AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
push(new StringBuilder());
if (value == null) {
return false;
}
Collection<?> collection = value.as();
if (collection.isEmpty()) {
sb.append("[]");
return false;
}
if (ValueCodex.canDecode(ctx.getElementType())) {
for (Object element : collection) {
sb.append(",").append(
encodeValue(ctx.getElementType(), element).getPayload());
}
} else {
boolean isEncoded = Splittable.class.equals(ctx.getElementType());
for (Object element : collection) {
sb.append(",");
if (element == null) {
sb.append("null");
} else if (isEncoded) {
sb.append(((Splittable) element).getPayload());
} else {
encodeToStringBuilder(sb, element);
}
}
}
sb.setCharAt(0, '[');
sb.append("]");
return false;
}
@Override
public boolean visitMapProperty(String propertyName,
AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
push(new StringBuilder());
if (value == null) {
return false;
}
Map<?, ?> map = value.as();
if (map.isEmpty()) {
sb.append("{}");
return false;
}
Class<?> keyType = ctx.getKeyType();
Class<?> valueType = ctx.getValueType();
boolean isEncodedKey = Splittable.class.equals(keyType);
boolean isEncodedValue = Splittable.class.equals(valueType);
boolean isValueKey = ValueCodex.canDecode(keyType);
boolean isValueValue = ValueCodex.canDecode(valueType);
if (isValueKey) {
writeValueKeyMap(map, keyType, valueType, isEncodedValue, isValueValue);
} else {
writeObjectKeyMap(map, valueType, isEncodedKey, isEncodedValue,
isValueValue);
}
return false;
}
@Override
public boolean visitReferenceProperty(String propertyName,
AutoBean<?> value, PropertyContext ctx) {
push(new StringBuilder());
if (value == null) {
return false;
}
if (Splittable.class.equals(ctx.getType())) {
sb.append(((Splittable) value.as()).getPayload());
return false;
}
if (seen.contains(value)) {
haltOnCycle();
}
return true;
}
@Override
public boolean visitValueProperty(String propertyName, Object value,
PropertyContext ctx) {
// Skip primitive types whose values are uninteresting.
Class<?> type = ctx.getType();
Object blankValue = ValueCodex.getUninitializedFieldValue(type);
if (value == blankValue || value != null && value.equals(blankValue)) {
return false;
}
// Special handling for enums if we have an obfuscation map
Splittable split;
split = encodeValue(type, value);
sb.append(",\"").append(propertyName).append("\":").append(
split.getPayload());
return false;
}
StringBuilder pop() {
StringBuilder toReturn = stack.pop();
sb = stack.peek();
return toReturn;
}
void push(StringBuilder sb) {
stack.push(sb);
this.sb = sb;
}
private void encodeToStringBuilder(StringBuilder accumulator, Object value) {
push(new StringBuilder());
AutoBean<?> bean = AutoBeanUtils.getAutoBean(value);
if (!seen.add(bean)) {
haltOnCycle();
}
bean.accept(this);
accumulator.append(pop().toString());
seen.remove(bean);
}
/**
* Encodes a value, with special handling for enums to allow the field name
* to be overridden.
*/
private Splittable encodeValue(Class<?> expectedType, Object value) {
Splittable split;
if (value instanceof Enum<?> && enumMap != null) {
split = ValueCodex.encode(String.class,
enumMap.getToken((Enum<?>) value));
} else {
split = ValueCodex.encode(expectedType, value);
}
return split;
}
private void haltOnCycle() {
throw new HaltException(new UnsupportedOperationException(
"Cycle detected"));
}
/**
* Writes a map JSON literal where the keys are object types. This is
* encoded as a list of two lists, since it's possible that two distinct
* objects have the same encoded form.
*/
private void writeObjectKeyMap(Map<?, ?> map, Class<?> valueType,
boolean isEncodedKey, boolean isEncodedValue, boolean isValueValue) {
StringBuilder keys = new StringBuilder();
StringBuilder values = new StringBuilder();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (isEncodedKey) {
keys.append(",").append(((Splittable) entry.getKey()).getPayload());
} else {
encodeToStringBuilder(keys.append(","), entry.getKey());
}
if (isEncodedValue) {
values.append(",").append(
((Splittable) entry.getValue()).getPayload());
} else if (isValueValue) {
values.append(",").append(
encodeValue(valueType, entry.getValue()).getPayload());
} else {
encodeToStringBuilder(values.append(","), entry.getValue());
}
}
keys.setCharAt(0, '[');
keys.append("]");
values.setCharAt(0, '[');
values.append("]");
sb.append("[").append(keys.toString()).append(",").append(
values.toString()).append("]");
}
/**
* Writes a map JSON literal where the keys are value types.
*/
private void writeValueKeyMap(Map<?, ?> map, Class<?> keyType,
Class<?> valueType, boolean isEncodedValue, boolean isValueValue) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
sb.append(",").append(encodeValue(keyType, entry.getKey()).getPayload()).append(
":");
if (isEncodedValue) {
sb.append(((Splittable) entry.getValue()).getPayload());
} else if (isValueValue) {
sb.append(encodeValue(valueType, entry.getValue()).getPayload());
} else {
encodeToStringBuilder(sb, entry.getValue());
}
}
sb.setCharAt(0, '{');
sb.append("}");
}
}
/**
* Used to stop processing.
*/
static class HaltException extends RuntimeException {
public HaltException(RuntimeException cause) {
super(cause);
}
@Override
public RuntimeException getCause() {
return (RuntimeException) super.getCause();
}
}
public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz,
Splittable data) {
return new Decoder(factory).decode(data, clazz);
}
/**
* Decode an AutoBeanCodex payload.
*
* @param <T> the expected return type
* @param factory an AutoBeanFactory capable of producing {@code AutoBean<T>}
* @param clazz the expected return type
* @param payload a payload string previously generated by
* {@link #encode(AutoBean)}
* @return an AutoBean containing the payload contents
*/
public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz,
String payload) {
Splittable data = StringQuoter.split(payload);
return decode(factory, clazz, data);
}
/**
* Encodes an AutoBean. The actual payload contents can be retrieved through
* {@link Splittable#getPayload()}.
*
* @param bean the bean to encode
* @return a Splittable that encodes the state of the AutoBean
*/
public static Splittable encode(AutoBean<?> bean) {
if (bean == null) {
return LazySplittable.NULL;
}
StringBuilder sb = new StringBuilder();
encodeForJsoPayload(sb, bean);
return new LazySplittable(sb.toString());
}
// ["prop",value,"prop",value, ...]
private static void encodeForJsoPayload(StringBuilder sb, AutoBean<?> bean) {
Encoder e = new Encoder(bean.getFactory());
e.push(sb);
try {
bean.accept(e);
} catch (HaltException ex) {
throw ex.getCause();
}
}
private AutoBeanCodex() {
}
}