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

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

There is a newer version: 2.10.0
Show 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.LinkedHashSet;
import java.util.List;
import java.util.Map;
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 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, or if the property is a list of values.
       */
      Class elementType =
          ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx)
              .getElementType() : null;
      boolean shouldSend =
          isOwnerValueProxy || matchesPropertyRef(propertyRefs, propertyName)
              || elementType != null && ValueCodex.canDecode(elementType);

      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 = ctx.getType();
      } else {
        type = new CollectionType(ctx.getType(), elementType);
      }
      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) {
      // 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));
        }
      }
      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) {
    return 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.
   */
  private Map clientObjectsToResolutions =
      new HashMap();
  /**
   * 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;
    }
    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.
   */
  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;
    }
    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) {
        accumulator.add(resolveClientValue(o, elementType).getClientObject());
      }
      return makeResolution(accumulator);
    }

    throw new ReportableException("Unsupported domain type " + returnClass.getCanonicalName());
  }
}