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.EntityProxy;
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.List;
import java.util.Map;
import java.util.Set;
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;
    }
  }

  /**
   * 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;
    }

    @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);
  }

  /**
   * 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;
  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) {
    return resolveClientValue(domainValue, assignableTo,
        getPropertyRefs(propertyRefs), "");
  }

  /**
   * 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;
  }

  /**
   * 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 Set getPropertyRefs(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;
  }

  /**
   * Converts a domain entity into an EntityProxy that will be sent to the
   * client.
   */
  private  T resolveClientProxy(final Object domainEntity,
      Class proxyType, final Set propertyRefs, ResolutionKey key,
      final String prefix) {
    if (domainEntity == null) {
      return null;
    }

    SimpleProxyId id = state.getStableId(domainEntity);

    boolean isEntityProxy = state.isEntityType(proxyType);
    final boolean isOwnerValueProxy = state.isValueType(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);
    resolved.put(key, bean.as());
    bean.setTag(Constants.IN_RESPONSE, true);
    if (domainVersion != null) {
      Splittable flatVersion = state.flatten(domainVersion);
      bean.setTag(Constants.VERSION_PROPERTY_B64,
          SimpleRequestProcessor.toBase64(flatVersion.getPayload()));
    }

    bean.accept(new AutoBeanVisitor() {

      @Override
      public boolean visitReferenceProperty(String propertyName,
          AutoBean value, PropertyContext ctx) {
        // Does the user care about the property?
        String newPrefix = (prefix.length() > 0 ? (prefix + ".") : "")
            + propertyName;

        /*
         * 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, newPrefix)
            || 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);
        }
        Object clientValue = resolveClientValue(domainValue, type,
            propertyRefs, newPrefix);

        ctx.set(clientValue);
        return false;
      }

      @Override
      public boolean visitValueProperty(String propertyName, Object value,
          PropertyContext ctx) {
        // Limit unrequested value properties?
        value = service.getProperty(domainEntity, propertyName);
        ctx.set(value);
        return false;
      }
    });

    return bean.as();
  }

  /**
   * Recursive-descent implementation.
   */
  private Object resolveClientValue(Object domainValue, Type returnType,
      Set propertyRefs, String prefix) {
    if (domainValue == null) {
      return null;
    }

    boolean anyType = returnType == null;
    if (anyType) {
      returnType = Object.class;
    }

    Class assignableTo = TypeUtils.ensureBaseType(returnType);
    ResolutionKey key = new ResolutionKey(domainValue, returnType);

    Object previous = resolved.get(key);
    if (previous != null && assignableTo.isInstance(previous)) {
      return assignableTo.cast(previous);
    }

    Class returnClass = service.resolveClientType(domainValue.getClass(),
        assignableTo, true);

    if (anyType) {
      assignableTo = returnClass;
    }

    // Pass simple values through
    if (ValueCodex.canDecode(returnClass)) {
      return assignableTo.cast(domainValue);
    }

    // Convert entities to EntityProxies or EntityProxyIds
    boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass);
    boolean isId = EntityProxyId.class.isAssignableFrom(returnClass);
    if (isProxy || isId) {
      Class proxyClass = assignableTo.asSubclass(BaseProxy.class);
      BaseProxy entity = resolveClientProxy(domainValue, proxyClass,
          propertyRefs, key, prefix);
      if (isId) {
        return assignableTo.cast(((EntityProxy) entity).stableId());
      }
      return assignableTo.cast(entity);
    }

    // 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());
      }
      resolved.put(key, accumulator);

      Type elementType = TypeUtils.getSingleParameterization(Collection.class,
          returnType);
      for (Object o : (Collection) domainValue) {
        accumulator.add(resolveClientValue(o, elementType, propertyRefs, prefix));
      }
      return assignableTo.cast(accumulator);
    }

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