| /* |
| * 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) { |
| synchronized (coderFor) { |
| 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) { |
| synchronized (coders) { |
| 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) { |
| synchronized (coders) { |
| 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) { |
| synchronized (coders) { |
| 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; |
| } |
| } |