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

com.google.web.bindery.requestfactory.server.Resolver Maven / Gradle / Ivy

The newest version!
/*
 * 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.web.bindery.requestfactory.server;

import com.google.web.bindery.autobean.shared.AutoBean;
import com.google.web.bindery.autobean.shared.AutoBeanUtils;
import com.google.web.bindery.autobean.shared.AutoBeanVisitor;
import com.google.web.bindery.autobean.shared.Splittable;
import com.google.web.bindery.autobean.shared.ValueCodex;
import com.google.web.bindery.autobean.vm.impl.TypeUtils;
import com.google.web.bindery.requestfactory.shared.BaseProxy;
import com.google.web.bindery.requestfactory.shared.EntityProxyId;
import com.google.web.bindery.requestfactory.shared.impl.Constants;
import com.google.web.bindery.requestfactory.shared.impl.SimpleProxyId;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Responsible for converting between domain and client entities. This class has
 * a small amount of temporary state used to handle graph cycles and assignment
 * of synthetic ids.
 *
 * @see RequestState#getResolver()
 */
class Resolver {
  /**
   * A parameterized type with key and value parameters.
   */
  private static class MapType implements ParameterizedType {
    private final Class keyType;
    private final Class valueType;

    public MapType(Class keyType, Class valueType) {
      this.keyType = keyType;
      this.valueType = valueType;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof MapType)) {
        return false;
      }
      MapType other = (MapType) o;
      return keyType.equals(other.keyType) && valueType.equals(other.valueType);
    }

    public Type[] getActualTypeArguments() {
      return new Type[] {keyType, valueType};
    }

    public Type getOwnerType() {
      return null;
    }

    @Override
    public Type getRawType() {
      return Map.class;
    }

    @Override
    public int hashCode() {
      return valueType.hashCode() * 13 + keyType.hashCode() * 7;
    }
  }

  /**
   * A parameterized type with a single parameter.
   */
  private static class CollectionType implements ParameterizedType {
    private final Class rawType;
    private final Class elementType;

    private CollectionType(Class rawType, Class elementType) {
      this.rawType = rawType;
      this.elementType = elementType;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof CollectionType)) {
        return false;
      }
      CollectionType other = (CollectionType) o;
      return rawType.equals(other.rawType) && elementType.equals(other.elementType);
    }

    public Type[] getActualTypeArguments() {
      return new Type[] {elementType};
    }

    public Type getOwnerType() {
      return null;
    }

    public Type getRawType() {
      return rawType;
    }

    @Override
    public int hashCode() {
      return rawType.hashCode() * 13 + elementType.hashCode() * 7;
    }
  }

  /**
   * Copies values and references from a domain object to a client object. This
   * type does not descend into referenced objects.
   */
  private class PropertyResolver extends AutoBeanVisitor {
    private final Object domainEntity;
    private final boolean isOwnerValueProxy;
    private final boolean needsSimpleValues;
    private final Set propertyRefs;

    private PropertyResolver(Resolution resolution) {
      ResolutionKey key = resolution.getResolutionKey();
      this.domainEntity = key.getDomainObject();
      this.isOwnerValueProxy = state.isValueType(TypeUtils.ensureBaseType(key.requestedType));
      this.needsSimpleValues = resolution.needsSimpleValues();
      this.propertyRefs = resolution.takeWork();
    }

    @Override
    public boolean visitReferenceProperty(String propertyName, AutoBean value,
        PropertyContext ctx) {
      /*
       * Send the property if the enclosing type is a ValueProxy, if the owner
       * requested the property, if the property is a list of values, or a map
       * whose keys and values are both value types.
       */
      Class elementType = null;
      if (ctx instanceof CollectionPropertyContext) {
        elementType = ((CollectionPropertyContext) ctx).getElementType();
      }

      Class keyType = null;
      Class valueType = null;
      if (ctx instanceof MapPropertyContext) {
        MapPropertyContext mapCtx = (MapPropertyContext) ctx;
        keyType = mapCtx.getKeyType();
        valueType = mapCtx.getValueType();
      }

      boolean shouldSend =
          isOwnerValueProxy || matchesPropertyRef(propertyRefs, propertyName)
              || (elementType != null && ValueCodex.canDecode(elementType))
              || (keyType != null && ValueCodex.canDecode(keyType) &&
                  valueType != null && ValueCodex.canDecode(valueType));

      if (!shouldSend) {
        return false;
      }

      // Call the getter
      Object domainValue = service.getProperty(domainEntity, propertyName);
      if (domainValue == null) {
        return false;
      }

      // Turn the domain object into something usable on the client side
      Type type;
      if (elementType != null) {
        type = new CollectionType(ctx.getType(), elementType);
      } else if (keyType != null && valueType != null) {
        type = new MapType(keyType, valueType);
      } else {
        type = ctx.getType();
      }
      Resolution resolution = resolveClientValue(domainValue, type);
      addPathsToResolution(resolution, propertyName, propertyRefs);
      ctx.set(resolution.getClientObject());
      return false;
    }

    @Override
    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
      /*
       * Only call the getter for simple values once since they're not
       * explicitly enumerated.
       */
      if (needsSimpleValues) {
        // Limit unrequested value properties?
        value = service.getProperty(domainEntity, propertyName);
        ctx.set(value);
      }
      return false;
    }
  }

  /**
   * Tracks the state of resolving a single client object.
   */
  private static class Resolution {
    /**
     * There's no Collections shortcut for this.
     */
    private static final SortedSet EMPTY = Collections
        .unmodifiableSortedSet(new TreeSet());

    /**
     * The client object.
     */
    private final Object clientObject;

    /**
     * A one-shot flag for {@link #hasWork()} to ensure that simple properties
     * will be resolved, even when there's no requested property set.
     */
    private boolean needsSimpleValues;
    private SortedSet toResolve = EMPTY;
    private final SortedSet resolved = new TreeSet();
    private final ResolutionKey key;

    public Resolution(Object simpleValue) {
      assert !(simpleValue instanceof Resolution);
      this.clientObject = simpleValue;
      this.key = null;
    }

    public Resolution(ResolutionKey key, BaseProxy clientObject) {
      this.clientObject = clientObject;
      this.key = key;
      needsSimpleValues = true;
    }

    /**
     * Removes the prefix from each requested path and enqueues paths that have
     * not been previously resolved for the next batch of work.
     */
    public void addPaths(String prefix, Collection requestedPaths) {
      if (clientObject == null) {
        // No point trying to follow paths past a null value
        return;
      }

      // Identity comparison intentional
      if (toResolve == EMPTY) {
        toResolve = new TreeSet();
      }
      prefix = prefix.isEmpty() ? prefix : (prefix + ".");
      int prefixLength = prefix.length();
      for (String path : requestedPaths) {
        if (path.startsWith(prefix)) {
          toResolve.add(path.substring(prefixLength));
        } else if (path.startsWith("*.")) {
          toResolve.add(path.substring("*.".length()));
        }
      }
      toResolve.removeAll(resolved);
      if (toResolve.isEmpty()) {
        toResolve = EMPTY;
      }
    }

    public Object getClientObject() {
      return clientObject;
    }

    public ResolutionKey getResolutionKey() {
      return key;
    }

    public boolean hasWork() {
      return needsSimpleValues || !toResolve.isEmpty();
    }

    public boolean needsSimpleValues() {
      return needsSimpleValues;
    }

    /**
     * Returns client-object-relative reference paths that should be further
     * resolved.
     */
    public SortedSet takeWork() {
      needsSimpleValues = false;
      SortedSet toReturn = toResolve;
      resolved.addAll(toReturn);
      toResolve = EMPTY;
      return toReturn;
    }
  }

  /**
   * Used to map the objects being resolved and its API slice to the client-side
   * value. This handles the case where a domain object is returned to the
   * client mapped to two proxies of differing types.
   */
  private static class ResolutionKey {
    private final Object domainObject;
    private final int hashCode;
    private final Type requestedType;

    public ResolutionKey(Object domainObject, Type requestedType) {
      this.domainObject = domainObject;
      this.requestedType = requestedType;
      this.hashCode = System.identityHashCode(domainObject) * 13 + requestedType.hashCode() * 7;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof ResolutionKey)) {
        return false;
      }
      ResolutionKey other = (ResolutionKey) o;
      // Object identity comparison intentional
      if (domainObject != other.domainObject) {
        return false;
      }
      if (!requestedType.equals(other.requestedType)) {
        return false;
      }
      return true;
    }

    public Object getDomainObject() {
      return domainObject;
    }

    @Override
    public int hashCode() {
      return hashCode;
    }

    /**
     * For debugging use only.
     */
    @Override
    public String toString() {
      return domainObject.toString() + " => " + requestedType.toString();
    }
  }

  /**
   * Returns the trailing {@code [n]} index value from a path.
   */
  static int index(String path) {
    int idx = path.lastIndexOf('[');
    if (idx == -1) {
      return -1;
    }
    return Integer.parseInt(path.substring(idx + 1, path.lastIndexOf(']')));
  }

  /**
   * Returns {@code true} if the given prefix is one of the requested property
   * references.
   */
  static boolean matchesPropertyRef(Set propertyRefs, String newPrefix) {
    /*
     * Match all fields for a wildcard
     *
     * Also, remove list index suffixes. Not actually used, was in anticipation
     * of OGNL type schemes. That said, Editor will slip in such things.
     */
    return propertyRefs.contains("*")
        || propertyRefs.contains(newPrefix.replaceAll("\\[\\d+\\]", ""));
  }

  /**
   * Removes the trailing {@code [n]} from a path.
   */
  static String snipIndex(String path) {
    int idx = path.lastIndexOf('[');
    if (idx == -1) {
      return path;
    }
    return path.substring(0, idx);
  }

  /**
   * Expand the property references in an InvocationMessage into a
   * fully-expanded list of properties. For example, [foo.bar.baz]
   * will be converted into [foo, foo.bar, foo.bar.baz].
   */
  private static Set expandPropertyRefs(Set refs) {
    if (refs == null) {
      return Collections.emptySet();
    }

    Set toReturn = new TreeSet();
    for (String raw : refs) {
      for (int idx = raw.length(); idx >= 0; idx = raw.lastIndexOf('.', idx - 1)) {
        toReturn.add(raw.substring(0, idx));
      }
    }
    return toReturn;
  }

  /**
   * Maps proxy instances to the Resolution objects.
   *

* FIXME: The proxies are later mutated, which is not an issue as this is an * IdentityHashMap, but still feels weird. We should try to find a way to * put immutable objects as keys in this map. */ private IdentityHashMap clientObjectsToResolutions = new IdentityHashMap(); /** * Maps domain values to client values. This map prevents cycles in the object * graph from causing infinite recursion. */ private final Map resolved = new HashMap(); private final ServiceLayer service; private final RequestState state; /** * Contains Resolutions with path references that have not yet been resolved. */ private Set toProcess = new LinkedHashSet(); private int syntheticId; /** * Should only be called from {@link RequestState}. */ Resolver(RequestState state) { this.state = state; this.service = state.getServiceLayer(); } /** * Given a domain object, return a value that can be encoded by the client. * * @param domainValue the domain object to be converted into a client-side * value * @param assignableTo the type in the client to which the resolved value * should be assignable. A value of {@code null} indicates that any * resolution will suffice. * @param propertyRefs the property references requested by the client */ public Object resolveClientValue(Object domainValue, Type assignableTo, Set propertyRefs) { Resolution toReturn = resolveClientValue(domainValue, assignableTo); if (toReturn == null) { return null; } addPathsToResolution(toReturn, "", expandPropertyRefs(propertyRefs)); while (!toProcess.isEmpty()) { List working = new ArrayList(toProcess); toProcess.clear(); for (Resolution resolution : working) { if (resolution.hasWork()) { AutoBean bean = AutoBeanUtils.getAutoBean((BaseProxy) resolution.getClientObject()); bean.accept(new PropertyResolver(resolution)); } } } return toReturn.getClientObject(); } /** * Convert a client-side value into a domain value. * * @param maybeEntityProxy the client object to resolve * @param detectDeadEntities if true this method will throw a * ReportableException containing a {@link DeadEntityException} if an * EntityProxy cannot be resolved */ public Object resolveDomainValue(Object maybeEntityProxy, boolean detectDeadEntities) { if (maybeEntityProxy instanceof BaseProxy) { AutoBean bean = AutoBeanUtils.getAutoBean((BaseProxy) maybeEntityProxy); Object domain = bean.getTag(Constants.DOMAIN_OBJECT); if (domain == null && detectDeadEntities) { throw new ReportableException(new DeadEntityException( "The requested entity is not available on the server")); } return domain; } else if (maybeEntityProxy instanceof Collection) { Collection accumulator; if (maybeEntityProxy instanceof List) { accumulator = new ArrayList(); } else if (maybeEntityProxy instanceof Set) { accumulator = new HashSet(); } else { throw new ReportableException("Unsupported collection type " + maybeEntityProxy.getClass().getName()); } for (Object o : (Collection) maybeEntityProxy) { accumulator.add(resolveDomainValue(o, detectDeadEntities)); } return accumulator; } else if (maybeEntityProxy instanceof Map) { Map accumulator = new HashMap(); for (Entry entry : ((Map) maybeEntityProxy).entrySet()) { accumulator.put( resolveDomainValue(entry.getKey(), detectDeadEntities), resolveDomainValue(entry.getValue(), detectDeadEntities)); } return accumulator; } return maybeEntityProxy; } /** * Calls {@link Resolution#addPaths(String, Collection)}, enqueuing * {@code key} if {@link Resolution#hasWork()} returns {@code true}. This * method will also expand paths on the members of Collections and Maps. */ private void addPathsToResolution(Resolution resolution, String prefix, Set propertyRefs) { if (propertyRefs.isEmpty()) { // No work to do return; } if (resolution.getResolutionKey() != null) { // Working on a proxied type assert resolution.getClientObject() instanceof BaseProxy : "Expecting BaseProxy, found " + resolution.getClientObject().getClass().getCanonicalName(); resolution.addPaths(prefix, propertyRefs); if (resolution.hasWork()) { toProcess.add(resolution); } return; } if (resolution.getClientObject() instanceof Collection) { // Pass the paths onto the Resolutions for the contained elements Collection collection = (Collection) resolution.getClientObject(); for (Object obj : collection) { Resolution subResolution = clientObjectsToResolutions.get(obj); // subResolution will be null for List, etc. if (subResolution != null) { addPathsToResolution(subResolution, prefix, propertyRefs); } } return; } if (resolution.getClientObject() instanceof Map) { Map map = (Map) resolution.getClientObject(); for (Map.Entry entry : map.entrySet()) { Resolution keyResolution = clientObjectsToResolutions.get(entry.getKey()); if (keyResolution != null) { addPathsToResolution(keyResolution, prefix + ".keys", propertyRefs); } Resolution valueResolution = clientObjectsToResolutions.get(entry.getValue()); if (valueResolution != null) { addPathsToResolution(valueResolution, prefix + ".values", propertyRefs); } } return; } assert false : "Should not add paths to client type " + resolution.getClientObject().getClass().getCanonicalName(); } /** * Creates a resolution for a simple value. */ private Resolution makeResolution(Object domainValue) { assert !state.isEntityType(domainValue.getClass()) && !state.isValueType(domainValue.getClass()) : "Not a simple value type"; return new Resolution(domainValue); } /** * Create or reuse a Resolution for a proxy object. */ private Resolution makeResolution(ResolutionKey key, BaseProxy clientObject) { Resolution resolution = resolved.get(key); if (resolution == null) { resolution = new Resolution(key, clientObject); clientObjectsToResolutions.put(clientObject, resolution); toProcess.add(resolution); resolved.put(key, resolution); } return resolution; } /** * Creates a proxy instance held by a Resolution for a given domain type. */ private Resolution resolveClientProxy(Object domainEntity, Class proxyType, ResolutionKey key) { if (domainEntity == null) { return null; } SimpleProxyId id = state.getStableId(domainEntity); boolean isEntityProxy = state.isEntityType(proxyType); Object domainVersion; // Create the id or update an ephemeral id by calculating its address if (id == null || id.isEphemeral()) { // The address is an id or an id plus a path Object domainId; if (isEntityProxy) { // Compute data needed to return id to the client domainId = service.getId(domainEntity); domainVersion = service.getVersion(domainEntity); } else { domainId = null; domainVersion = null; } if (id == null) { if (domainId == null) { /* * This will happen when server code attempts to return an unpersisted * object to the client. In this case, we'll assign a synthetic id * that is valid for the duration of the response. The client is * expected to assign a client-local id to this object and then it * will behave as though it were an object newly-created by the * client. */ id = state.getIdFactory().allocateSyntheticId(proxyType, ++syntheticId); } else { Splittable flatValue = state.flatten(domainId); id = state.getIdFactory().getId(proxyType, flatValue.getPayload(), 0); } } else if (domainId != null) { // Mark an ephemeral id as having been persisted Splittable flatValue = state.flatten(domainId); id.setServerId(flatValue.getPayload()); } } else if (isEntityProxy) { // Already have the id, just pull the current version domainVersion = service.getVersion(domainEntity); } else { // The version of a value object is always null domainVersion = null; } @SuppressWarnings("unchecked") AutoBean bean = (AutoBean) state.getBeanForPayload(id, domainEntity); bean.setTag(Constants.IN_RESPONSE, true); if (domainVersion != null) { Splittable flatVersion = state.flatten(domainVersion); bean.setTag(Constants.VERSION_PROPERTY_B64, SimpleRequestProcessor.toBase64(flatVersion .getPayload())); } T clientObject = bean.as(); return makeResolution(key, clientObject); } /** * Creates a Resolution object that holds a client value that represents the * given domain value. The resolved client value will be assignable to * {@code clientType}. */ private Resolution resolveClientValue(Object domainValue, Type clientType) { if (domainValue == null) { return null; } boolean anyType = clientType == null; if (anyType) { clientType = Object.class; } Class assignableTo = TypeUtils.ensureBaseType(clientType); ResolutionKey key = new ResolutionKey(domainValue, clientType); Resolution previous = resolved.get(key); if (previous != null && assignableTo.isInstance(previous.getClientObject())) { return previous; } Class returnClass = service.resolveClientType(domainValue.getClass(), assignableTo, true); if (anyType) { assignableTo = returnClass; } // Pass simple values through if (ValueCodex.canDecode(returnClass)) { return makeResolution(domainValue); } // Convert entities to EntityProxies or EntityProxyIds boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass); boolean isId = EntityProxyId.class.isAssignableFrom(returnClass); if (isProxy || isId) { Class proxyClass = returnClass.asSubclass(BaseProxy.class); return resolveClientProxy(domainValue, proxyClass, key); } // Convert collections if (Collection.class.isAssignableFrom(returnClass)) { Collection accumulator; if (List.class.isAssignableFrom(returnClass)) { accumulator = new ArrayList(); } else if (Set.class.isAssignableFrom(returnClass)) { accumulator = new HashSet(); } else { throw new ReportableException("Unsupported collection type" + returnClass.getName()); } Type elementType = TypeUtils.getSingleParameterization(Collection.class, clientType); for (Object o : (Collection) domainValue) { Resolution resolution = resolveClientValue(o, elementType); accumulator.add(getClientObject(resolution)); } return makeResolution(accumulator); } if (Map.class.isAssignableFrom(returnClass)) { Map accumulator = new HashMap(); Type[] entryTypes = TypeUtils.getParameterization(Map.class, clientType); for (Map.Entry entry : ((Map) domainValue).entrySet()) { accumulator.put( getClientObject(resolveClientValue(entry.getKey(), entryTypes[0])), getClientObject(resolveClientValue(entry.getValue(), entryTypes[1]))); } return makeResolution(accumulator); } throw new ReportableException("Unsupported domain type " + returnClass.getCanonicalName()); } private Object getClientObject(Resolution resolution) { return resolution == null ? null : resolution.getClientObject(); } }