blob: fbefda141a60b2940da8e8a7a8783f05790501db [file] [log] [blame]
/*
* 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<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, 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<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.
*<p>
* 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<BaseProxy, Resolution> clientObjectsToResolutions =
new IdentityHashMap<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;
} else if (maybeEntityProxy instanceof Map<?, ?>) {
Map<Object, Object> accumulator = new HashMap<Object, Object>();
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<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;
}
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 <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 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) {
Resolution resolution = resolveClientValue(o, elementType);
accumulator.add(getClientObject(resolution));
}
return makeResolution(accumulator);
}
if (Map.class.isAssignableFrom(returnClass)) {
Map<Object, Object> accumulator = new HashMap<Object, Object>();
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();
}
}