All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.web.bindery.autobean.shared.impl.AutoBeanCodexImpl Maven / Gradle / Ivy

There is a newer version: 2.7.0.vaadin7
Show newest version
/*
 * 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> 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>();
    }
  }

  /**
   * Dynamically creates a Coder that is capable of operating on a particular
   * parameterization of a datastructure (e.g. {@code Map>}
   * ).
   */
  static class CoderCreator extends ParameterizationVisitor {
    private Stack stack = new Stack();

    @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> enumType = (Class>) 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 collection;
      if (List.class.equals(type)) {
        collection = new SplittableList(data, elementDecoder, state);
      } else if (Set.class.equals(type)) {
        collection = new SplittableSet(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 
   */
  static class EnumCoder> implements Coder {
    private final Class type;

    public EnumCoder(Class 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 toReturn;
      if (data.isIndexed()) {
        assert data.size() == 2 : "Wrong data size: " + data.size();
        toReturn = new SplittableComplexMap(data, keyDecoder, valueDecoder, state);
      } else {
        toReturn = new SplittableSimpleMap(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 keys = new ArrayList(map.size());
        List values = new ArrayList(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 coderFor = new HashMap();
  /**
   * A map of types to a Coder that handles the type.
   */
  private static final Map, Coder> coders = new HashMap, 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  AutoBean doDecode(EncodeState state, Class 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 toReturn = (AutoBean) 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) 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 > Coder enumCoder(Class type) {
    synchronized (coders) {
      Coder toReturn = coders.get(type);
      if (toReturn == null) {
        toReturn = new EnumCoder(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;
  }
}