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