/* * Copyright 2011 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.web.bindery.autobean.shared.impl; import com.google.web.bindery.autobean.shared.AutoBean; import com.google.web.bindery.autobean.shared.AutoBeanFactory; import com.google.web.bindery.autobean.shared.AutoBeanUtils; import com.google.web.bindery.autobean.shared.AutoBeanVisitor; import com.google.web.bindery.autobean.shared.AutoBeanVisitor.ParameterizationVisitor; import com.google.web.bindery.autobean.shared.Splittable; import com.google.web.bindery.autobean.shared.ValueCodex; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; /** * Contains the implementation details of AutoBeanCodex. This type was factored * out of AutoBeanCodex so that various implementation details can be accessed * without polluting a public API. */ public class AutoBeanCodexImpl { /** * Describes a means of encoding or decoding a particular type of data to or * from a wire format representation. Any given instance of a Coder should be * stateless; any state required for operation must be maintained in an * {@link EncodeState}. */ public interface Coder { Object decode(EncodeState state, Splittable data); void encode(EncodeState state, Object value); Splittable extractSplittable(EncodeState state, Object value); } /** * Contains transient state for Coder operation. */ public static class EncodeState { /** * Constructs a state object used for decoding payloads. */ public static EncodeState forDecode(AutoBeanFactory factory) { return new EncodeState(factory, null); } /** * Constructs a state object used for encoding payloads. */ public static EncodeState forEncode(AutoBeanFactory factory, StringBuilder sb) { return new EncodeState(factory, sb); } /** * Constructs a "stateless" state for testing Coders that do not require * AutoBean implementation details. */ public static EncodeState forTesting() { return new EncodeState(null, null); } final EnumMap enumMap; final AutoBeanFactory factory; final StringBuilder sb; final Stack<AutoBean<?>> seen; private EncodeState(AutoBeanFactory factory, StringBuilder sb) { this.factory = factory; enumMap = factory instanceof EnumMap ? (EnumMap) factory : null; this.sb = sb; this.seen = sb == null ? null : new Stack<AutoBean<?>>(); } } /** * Dynamically creates a Coder that is capable of operating on a particular * parameterization of a datastructure (e.g. {@code Map<String, List<String>>} * ). */ static 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(collectionCoder(type, stack.pop())); } else if (Map.class.equals(type)) { // Note that the parameters are passed in reverse order stack.push(mapCoder(stack.pop(), stack.pop())); } else if (Splittable.class.equals(type)) { stack.push(splittableCoder()); } else if (type.getEnumConstants() != null) { @SuppressWarnings(value = {"unchecked"}) Class<Enum<?>> enumType = (Class<Enum<?>>) type; stack.push(enumCoder(enumType)); } else if (ValueCodex.canDecode(type)) { stack.push(valueCoder(type)); } else { stack.push(objectCoder(type)); } } public Coder getCoder() { assert stack.size() == 1 : "Incorrect size: " + stack.size(); return stack.pop(); } } /** * Constructs one of the lightweight collection types. */ static 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(EncodeState state, Splittable data) { Collection<Object> collection; if (List.class.equals(type)) { collection = new SplittableList<Object>(data, elementDecoder, state); } else if (Set.class.equals(type)) { collection = new SplittableSet<Object>(data, elementDecoder, state); } else { // Should not reach here throw new RuntimeException(type.getName()); } return collection; } public void encode(EncodeState state, Object value) { if (value == null) { state.sb.append("null"); return; } Iterator<?> it = ((Collection<?>) value).iterator(); state.sb.append("["); if (it.hasNext()) { elementDecoder.encode(state, it.next()); while (it.hasNext()) { state.sb.append(","); elementDecoder.encode(state, it.next()); } } state.sb.append("]"); } public Splittable extractSplittable(EncodeState state, Object value) { return tryExtractSplittable(value); } } /** * Produces enums. * * @param <E> */ static class EnumCoder<E extends Enum<?>> implements Coder { private final Class<E> type; public EnumCoder(Class<E> type) { this.type = type; } public Object decode(EncodeState state, Splittable data) { return state.enumMap.getEnum(type, data.asString()); } public void encode(EncodeState state, Object value) { if (value == null) { state.sb.append("null"); return; } state.sb.append(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value))); } public Splittable extractSplittable(EncodeState state, Object value) { return StringQuoter.split(StringQuoter.quote(state.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(); } } /** * Constructs one of the lightweight Map types, depending on the key type. */ static 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(EncodeState state, Splittable data) { Map<Object, Object> toReturn; if (data.isIndexed()) { assert data.size() == 2 : "Wrong data size: " + data.size(); toReturn = new SplittableComplexMap<Object, Object>(data, keyDecoder, valueDecoder, state); } else { toReturn = new SplittableSimpleMap<Object, Object>(data, keyDecoder, valueDecoder, state); } return toReturn; } public void encode(EncodeState state, Object value) { if (value == null) { state.sb.append("null"); return; } Map<?, ?> map = (Map<?, ?>) value; boolean isSimpleMap = keyDecoder instanceof ValueCoder; if (isSimpleMap) { boolean first = true; state.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 (first) { first = false; } else { state.sb.append(","); } keyDecoder.encode(state, mapKey); state.sb.append(":"); if (mapValue == null) { // Null values must be preserved state.sb.append("null"); } else { valueDecoder.encode(state, mapValue); } } state.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()); } state.sb.append("["); collectionCoder(List.class, keyDecoder).encode(state, keys); state.sb.append(","); collectionCoder(List.class, valueDecoder).encode(state, values); state.sb.append("]"); } } public Splittable extractSplittable(EncodeState state, Object value) { return tryExtractSplittable(value); } } /** * Recurses into {@link AutoBeanCodexImpl}. */ static class ObjectCoder implements Coder { private final Class<?> type; public ObjectCoder(Class<?> type) { this.type = type; } public Object decode(EncodeState state, Splittable data) { AutoBean<?> bean = doDecode(state, type, data); return bean == null ? null : bean.as(); } public void encode(EncodeState state, Object value) { if (value == null) { state.sb.append("null"); return; } doEncode(state, AutoBeanUtils.getAutoBean(value)); } public Splittable extractSplittable(EncodeState state, Object value) { return tryExtractSplittable(value); } } static class PropertyCoderCreator extends AutoBeanVisitor { private AutoBean<?> bean; @Override public boolean visit(AutoBean<?> bean, Context ctx) { this.bean = bean; return true; } @Override public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, PropertyContext ctx) { maybeCreateCoder(propertyName, ctx); return false; } @Override public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) { maybeCreateCoder(propertyName, ctx); return false; } private void maybeCreateCoder(String propertyName, PropertyContext ctx) { CoderCreator creator = new CoderCreator(); ctx.accept(creator); coderFor.put(key(bean, propertyName), creator.getCoder()); } } /** * Extracts properties from a bean and turns them into JSON text. */ static class PropertyGetter extends AutoBeanVisitor { private boolean first = true; private final EncodeState state; public PropertyGetter(EncodeState state) { this.state = state; } @Override public void endVisit(AutoBean<?> bean, Context ctx) { state.sb.append("}"); state.seen.pop(); } @Override public boolean visit(AutoBean<?> bean, Context ctx) { if (state.seen.contains(bean)) { throw new HaltException(new UnsupportedOperationException("Cycles not supported")); } state.seen.push(bean); state.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 { state.sb.append(","); } state.sb.append(StringQuoter.quote(propertyName)); state.sb.append(":"); decoder.encode(state, value); } } /** * Populates beans with data extracted from an evaluated JSON payload. */ static class PropertySetter extends AutoBeanVisitor { private Splittable data; private EncodeState state; public void decodeInto(EncodeState state, Splittable data, AutoBean<?> bean) { this.data = data; this.state = state; 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(state, data.get(propertyName)); ctx.set(propertyValue); } } } /** * A passthrough Coder. */ static class SplittableCoder implements Coder { static final Coder INSTANCE = new SplittableCoder(); public Object decode(EncodeState state, Splittable data) { return data; } public void encode(EncodeState state, Object value) { if (value == null) { state.sb.append("null"); return; } state.sb.append(((Splittable) value).getPayload()); } public Splittable extractSplittable(EncodeState state, Object value) { return (Splittable) value; } } /** * Delegates to ValueCodex. */ static 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(EncodeState state, Splittable propertyValue) { if (propertyValue == null || propertyValue == Splittable.NULL) { return ValueCodex.getUninitializedFieldValue(type); } return ValueCodex.decode(type, propertyValue); } public void encode(EncodeState state, Object value) { state.sb.append(ValueCodex.encode(type, value).getPayload()); } public Splittable extractSplittable(EncodeState state, Object value) { return ValueCodex.encode(type, value); } } /** * A map of AutoBean interface+property names to the Coder for that property. */ private static final Map<String, Coder> coderFor = new HashMap<String, Coder>(); /** * A map of types to a Coder that handles the type. */ private static final Map<Class<?>, Coder> coders = new HashMap<Class<?>, Coder>(); public static Coder collectionCoder(Class<?> type, Coder elementCoder) { return new CollectionCoder(type, elementCoder); } public static Coder doCoderFor(AutoBean<?> bean, String propertyName) { String key = key(bean, propertyName); Coder toReturn = coderFor.get(key); if (toReturn == null) { bean.accept(new PropertyCoderCreator()); toReturn = coderFor.get(key); if (toReturn == null) { throw new IllegalArgumentException(propertyName); } } return toReturn; } public static <T> AutoBean<T> doDecode(EncodeState state, Class<T> clazz, Splittable data) { /* * If we decode the same Splittable twice, re-use the ProxyAutoBean to * maintain referential integrity. If we didn't do this, either facade would * update the same backing data, yet not be the same object via == * comparison. */ @SuppressWarnings("unchecked") AutoBean<T> toReturn = (AutoBean<T>) data.getReified(AutoBeanCodexImpl.class.getName()); if (toReturn != null) { return toReturn; } toReturn = state.factory.create(clazz); data.setReified(AutoBeanCodexImpl.class.getName(), toReturn); if (toReturn == null) { throw new IllegalArgumentException(clazz.getName()); } ((AbstractAutoBean<T>) toReturn).setData(data); return toReturn; } public static void doDecodeInto(EncodeState state, Splittable data, AutoBean<?> bean) { new PropertySetter().decodeInto(state, data, bean); } public static void doEncode(EncodeState state, AutoBean<?> bean) { PropertyGetter e = new PropertyGetter(state); try { bean.accept(e); } catch (HaltException ex) { throw ex.getCause(); } } public static <E extends Enum<?>> Coder enumCoder(Class<E> type) { Coder toReturn = coders.get(type); if (toReturn == null) { toReturn = new EnumCoder<E>(type); coders.put(type, toReturn); } return toReturn; } public static Coder mapCoder(Coder valueCoder, Coder keyCoder) { return new MapCoder(valueCoder, keyCoder); } public static Coder objectCoder(Class<?> type) { Coder toReturn = coders.get(type); if (toReturn == null) { toReturn = new ObjectCoder(type); coders.put(type, toReturn); } return toReturn; } public static Coder splittableCoder() { return SplittableCoder.INSTANCE; } public static Coder valueCoder(Class<?> type) { Coder toReturn = coders.get(type); if (toReturn == null) { toReturn = new ValueCoder(type); coders.put(type, toReturn); } return toReturn; } static Splittable tryExtractSplittable(Object value) { AutoBean<?> bean = AutoBeanUtils.getAutoBean(value); if (bean != null) { value = bean; } if (bean instanceof HasSplittable) { return ((HasSplittable) bean).getSplittable(); } return null; } private static String key(AutoBean<?> bean, String propertyName) { return bean.getType().getName() + ":" + propertyName; } }