blob: bcded2c53c9ab39ac9fa57f8dda6fb9d4b2c6602 [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.AutoBeanVisitor.ParameterizationVisitor;
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.Iterator;
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 {
/**
* Describes a means of encoding or decoding a particular type of data to or
* from a wire format representation.
*/
interface Coder {
Object decode(Splittable data);
void encode(StringBuilder sb, Object value);
}
/**
* Creates a Coder that is capable of operating on a particular
* parameterization of a datastructure (e.g. {@code Map<String, List<String>>}
* ).
*/
class CoderCreator extends ParameterizationVisitor {
private Stack<Coder> stack = new Stack<Coder>();
@Override
public void endVisitType(Class<?> type) {
if (List.class.equals(type) || Set.class.equals(type)) {
stack.push(new CollectionCoder(type, stack.pop()));
} else if (Map.class.equals(type)) {
// Note that the parameters are passed in reverse order
stack.push(new MapCoder(stack.pop(), stack.pop()));
} else if (Splittable.class.equals(type)) {
stack.push(new SplittableDecoder());
} else if (type.getEnumConstants() != null) {
@SuppressWarnings(value = {"rawtypes", "unchecked"})
EnumCoder decoder = new EnumCoder(type);
stack.push(decoder);
} else if (ValueCodex.canDecode(type)) {
stack.push(new ValueCoder(type));
} else {
stack.push(new ObjectCoder(type));
}
}
public Coder getCoder() {
assert stack.size() == 1 : "Incorrect size: " + stack.size();
return stack.pop();
}
}
class CollectionCoder implements Coder {
private final Coder elementDecoder;
private final Class<?> type;
public CollectionCoder(Class<?> type, Coder elementDecoder) {
this.elementDecoder = elementDecoder;
this.type = type;
}
public Object decode(Splittable data) {
Collection<Object> collection;
if (List.class.equals(type)) {
collection = new ArrayList<Object>();
} else if (Set.class.equals(type)) {
collection = new HashSet<Object>();
} else {
// Should not reach here
throw new RuntimeException(type.getName());
}
for (int i = 0, j = data.size(); i < j; i++) {
Object element = data.isNull(i) ? null
: elementDecoder.decode(data.get(i));
collection.add(element);
}
return collection;
}
public void encode(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
return;
}
Iterator<?> it = ((Collection<?>) value).iterator();
sb.append("[");
if (it.hasNext()) {
elementDecoder.encode(sb, it.next());
while (it.hasNext()) {
sb.append(",");
elementDecoder.encode(sb, it.next());
}
}
sb.append("]");
}
}
class EnumCoder<E extends Enum<E>> implements Coder {
private final Class<E> type;
public EnumCoder(Class<E> type) {
this.type = type;
}
public Object decode(Splittable data) {
return enumMap.getEnum(type, data.asString());
}
public void encode(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
}
sb.append(StringQuoter.quote(enumMap.getToken((Enum<?>) value)));
}
}
/**
* Used to stop processing.
*/
static class HaltException extends RuntimeException {
public HaltException(RuntimeException cause) {
super(cause);
}
@Override
public RuntimeException getCause() {
return (RuntimeException) super.getCause();
}
}
class MapCoder implements Coder {
private final Coder keyDecoder;
private final Coder valueDecoder;
/**
* Parameters in reversed order to accommodate stack-based setup.
*/
public MapCoder(Coder valueDecoder, Coder keyDecoder) {
this.keyDecoder = keyDecoder;
this.valueDecoder = valueDecoder;
}
public Object decode(Splittable data) {
Map<Object, Object> toReturn = new HashMap<Object, Object>();
if (data.isIndexed()) {
assert data.size() == 2 : "Wrong data size: " + data.size();
Splittable keys = data.get(0);
Splittable values = data.get(1);
for (int i = 0, j = keys.size(); i < j; i++) {
Object key = keys.isNull(i) ? null : keyDecoder.decode(keys.get(i));
Object value = values.isNull(i) ? null
: valueDecoder.decode(values.get(i));
toReturn.put(key, value);
}
} else {
ValueCoder keyValueDecoder = (ValueCoder) keyDecoder;
for (String rawKey : data.getPropertyKeys()) {
Object key = keyValueDecoder.decode(rawKey);
Object value = data.isNull(rawKey) ? null
: valueDecoder.decode(data.get(rawKey));
toReturn.put(key, value);
}
}
return toReturn;
}
public void encode(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
return;
}
Map<?, ?> map = (Map<?, ?>) value;
boolean isSimpleMap = keyDecoder instanceof ValueCoder;
if (isSimpleMap) {
boolean first = true;
sb.append("{");
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object mapKey = entry.getKey();
if (mapKey == null) {
// A null key in a simple map is meaningless
continue;
}
Object mapValue = entry.getValue();
if (mapValue == null) {
// A null value can be ignored
continue;
}
if (first) {
first = false;
} else {
sb.append(",");
}
keyDecoder.encode(sb, mapKey);
sb.append(":");
valueDecoder.encode(sb, mapValue);
}
sb.append("}");
} else {
List<Object> keys = new ArrayList<Object>(map.size());
List<Object> values = new ArrayList<Object>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
keys.add(entry.getKey());
values.add(entry.getValue());
}
sb.append("[");
new CollectionCoder(List.class, keyDecoder).encode(sb, keys);
sb.append(",");
new CollectionCoder(List.class, valueDecoder).encode(sb, values);
sb.append("]");
}
}
}
class ObjectCoder implements Coder {
private final Class<?> type;
public ObjectCoder(Class<?> type) {
this.type = type;
}
public Object decode(Splittable data) {
AutoBean<?> bean = doDecode(type, data);
return bean == null ? null : bean.as();
}
public void encode(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
return;
}
doEncode(sb, AutoBeanUtils.getAutoBean(value));
}
}
/**
* Extracts properties from a bean and turns them into JSON text.
*/
class PropertyGetter extends AutoBeanVisitor {
private boolean first = true;
private final StringBuilder sb;
public PropertyGetter(StringBuilder sb) {
this.sb = sb;
}
@Override
public void endVisit(AutoBean<?> bean, Context ctx) {
sb.append("}");
seen.pop();
}
@Override
public boolean visit(AutoBean<?> bean, Context ctx) {
if (seen.contains(bean)) {
throw new HaltException(new UnsupportedOperationException(
"Cycles not supported"));
}
seen.push(bean);
sb.append("{");
return true;
}
@Override
public boolean visitReferenceProperty(String propertyName,
AutoBean<?> value, PropertyContext ctx) {
if (value != null) {
encodeProperty(propertyName, value.as(), ctx);
}
return false;
}
@Override
public boolean visitValueProperty(String propertyName, Object value,
PropertyContext ctx) {
if (value != null
&& !value.equals(ValueCodex.getUninitializedFieldValue(ctx.getType()))) {
encodeProperty(propertyName, value, ctx);
}
return false;
}
private void encodeProperty(String propertyName, Object value,
PropertyContext ctx) {
CoderCreator pd = new CoderCreator();
ctx.accept(pd);
Coder decoder = pd.getCoder();
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(StringQuoter.quote(propertyName));
sb.append(":");
decoder.encode(sb, value);
}
}
/**
* Populates beans with data extracted from an evaluated JSON payload.
*/
class PropertySetter extends AutoBeanVisitor {
private Splittable data;
public void decodeInto(Splittable data, AutoBean<?> bean) {
this.data = data;
bean.accept(this);
}
@Override
public boolean visitReferenceProperty(String propertyName,
AutoBean<?> value, PropertyContext ctx) {
decodeProperty(propertyName, ctx);
return false;
}
@Override
public boolean visitValueProperty(String propertyName, Object value,
PropertyContext ctx) {
decodeProperty(propertyName, ctx);
return false;
}
protected void decodeProperty(String propertyName, PropertyContext ctx) {
if (!data.isNull(propertyName)) {
CoderCreator pd = new CoderCreator();
ctx.accept(pd);
Coder decoder = pd.getCoder();
Object propertyValue = decoder.decode(data.get(propertyName));
ctx.set(propertyValue);
}
}
}
class SplittableDecoder implements Coder {
public Object decode(Splittable data) {
return data;
}
public void encode(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
return;
}
sb.append(((Splittable) value).getPayload());
}
}
class ValueCoder implements Coder {
private final Class<?> type;
public ValueCoder(Class<?> type) {
assert type.getEnumConstants() == null : "Should use EnumTypeCodex";
this.type = type;
}
public Object decode(Splittable propertyValue) {
return decode(propertyValue.asString());
}
public Object decode(String propertyValue) {
return ValueCodex.decode(type, propertyValue);
}
public void encode(StringBuilder sb, Object value) {
sb.append(ValueCodex.encode(value).getPayload());
}
}
public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz,
Splittable data) {
return new AutoBeanCodex(factory).doDecode(clazz, data);
}
/**
* 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);
}
/**
* Copy data from a {@link Splittable} into an AutoBean. Unset values in the
* Splittable will not nullify data that already exists in the AutoBean.
*
* @param data the source data to copy
* @param bean the target AutoBean
*/
public static void decodeInto(Splittable data, AutoBean<?> bean) {
new AutoBeanCodex(bean.getFactory()).doDecodeInto(data, bean);
}
/**
* 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();
new AutoBeanCodex(bean.getFactory()).doEncode(sb, bean);
return new LazySplittable(sb.toString());
}
private final EnumMap enumMap;
private final AutoBeanFactory factory;
private final Stack<AutoBean<?>> seen = new Stack<AutoBean<?>>();
private AutoBeanCodex(AutoBeanFactory factory) {
this.factory = factory;
this.enumMap = factory instanceof EnumMap ? (EnumMap) factory : null;
}
<T> AutoBean<T> doDecode(Class<T> clazz, Splittable data) {
AutoBean<T> toReturn = factory.create(clazz);
if (toReturn == null) {
throw new IllegalArgumentException(clazz.getName());
}
doDecodeInto(data, toReturn);
return toReturn;
}
void doDecodeInto(Splittable data, AutoBean<?> bean) {
new PropertySetter().decodeInto(data, bean);
}
void doEncode(StringBuilder sb, AutoBean<?> bean) {
PropertyGetter e = new PropertyGetter(sb);
try {
bean.accept(e);
} catch (HaltException ex) {
throw ex.getCause();
}
}
}