/* * 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<String> 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<String> EMPTY = Collections .unmodifiableSortedSet(new TreeSet<String>()); /** * 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<String> toResolve = EMPTY; private final SortedSet<String> resolved = new TreeSet<String>(); 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<String> requestedPaths) { if (clientObject == null) { // No point trying to follow paths past a null value return; } // Identity comparison intentional if (toResolve == EMPTY) { toResolve = new TreeSet<String>(); } 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<String> takeWork() { needsSimpleValues = false; SortedSet<String> 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<String> 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, <code>[foo.bar.baz]</code> * will be converted into <code>[foo, foo.bar, foo.bar.baz]</code>. */ private static Set<String> expandPropertyRefs(Set<String> refs) { if (refs == null) { return Collections.emptySet(); } Set<String> toReturn = new TreeSet<String>(); 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<BaseProxy, Resolution> clientObjectsToResolutions = new HashMap<BaseProxy, Resolution>(); /** * Maps domain values to client values. This map prevents cycles in the object * graph from causing infinite recursion. */ private final Map<ResolutionKey, Resolution> resolved = new HashMap<ResolutionKey, Resolution>(); private final ServiceLayer service; private final RequestState state; /** * Contains Resolutions with path references that have not yet been resolved. */ private Set<Resolution> toProcess = new LinkedHashSet<Resolution>(); 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<String> propertyRefs) { Resolution toReturn = resolveClientValue(domainValue, assignableTo); if (toReturn == null) { return null; } addPathsToResolution(toReturn, "", expandPropertyRefs(propertyRefs)); while (!toProcess.isEmpty()) { List<Resolution> working = new ArrayList<Resolution>(toProcess); toProcess.clear(); for (Resolution resolution : working) { if (resolution.hasWork()) { AutoBean<BaseProxy> 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 <code>true</code> 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<BaseProxy> 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<Object> accumulator; if (maybeEntityProxy instanceof List<?>) { accumulator = new ArrayList<Object>(); } else if (maybeEntityProxy instanceof Set<?>) { accumulator = new HashSet<Object>(); } 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<String> 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<Integer>, 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 <T extends BaseProxy> Resolution resolveClientProxy(Object domainEntity, Class<T> proxyType, ResolutionKey key) { if (domainEntity == null) { return null; } SimpleProxyId<? extends BaseProxy> 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<T> bean = (AutoBean<T>) 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 new Resolution(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<? extends BaseProxy> proxyClass = returnClass.asSubclass(BaseProxy.class); return resolveClientProxy(domainValue, proxyClass, key); } // Convert collections if (Collection.class.isAssignableFrom(returnClass)) { Collection<Object> accumulator; if (List.class.isAssignableFrom(returnClass)) { accumulator = new ArrayList<Object>(); } else if (Set.class.isAssignableFrom(returnClass)) { accumulator = new HashSet<Object>(); } 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()); } }