| /* |
| * 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.gwt.rebind.model; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.JParameter; |
| import com.google.gwt.core.ext.typeinfo.JParameterizedType; |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.editor.rebind.model.ModelUtils; |
| import com.google.web.bindery.autobean.gwt.rebind.model.JBeanMethod; |
| import com.google.web.bindery.autobean.shared.Splittable; |
| import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel.Type; |
| import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestMethod.CollectionType; |
| import com.google.web.bindery.requestfactory.shared.BaseProxy; |
| import com.google.web.bindery.requestfactory.shared.EntityProxy; |
| import com.google.web.bindery.requestfactory.shared.ExtraTypes; |
| import com.google.web.bindery.requestfactory.shared.InstanceRequest; |
| import com.google.web.bindery.requestfactory.shared.JsonRpcProxy; |
| import com.google.web.bindery.requestfactory.shared.JsonRpcService; |
| import com.google.web.bindery.requestfactory.shared.ProxyFor; |
| import com.google.web.bindery.requestfactory.shared.ProxyForName; |
| import com.google.web.bindery.requestfactory.shared.Request; |
| import com.google.web.bindery.requestfactory.shared.RequestContext; |
| import com.google.web.bindery.requestfactory.shared.RequestFactory; |
| import com.google.web.bindery.requestfactory.shared.Service; |
| import com.google.web.bindery.requestfactory.shared.ServiceName; |
| import com.google.web.bindery.requestfactory.shared.ValueProxy; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Represents a RequestFactory interface declaration. |
| */ |
| public class RequestFactoryModel implements AcceptsModelVisitor, HasExtraTypes { |
| public static String poisonedMessage() { |
| return "Unable to create RequestFactoryModel model due to previous errors"; |
| } |
| |
| static String badContextReturnType(JMethod method, JClassType requestInterface, |
| JClassType instanceRequestInterface) { |
| return String.format("Return type %s in method %s must be an interface assignable" |
| + " to %s or %s", method.getReturnType(), method.getName(), requestInterface |
| .getSimpleSourceName(), instanceRequestInterface.getSimpleSourceName()); |
| } |
| |
| static String noSettersAllowed(JMethod found) { |
| return String.format("Optional setters not allowed here: ", found.getName()); |
| } |
| |
| private final JClassType collectionInterface; |
| private final List<ContextMethod> contextMethods = new ArrayList<ContextMethod>(); |
| private final JClassType entityProxyInterface; |
| private final List<EntityProxyModel> extraTypes; |
| private final JClassType factoryType; |
| private final JClassType instanceRequestInterface; |
| private final JClassType listInterface; |
| private final TreeLogger logger; |
| private final JClassType mapInterface; |
| private final TypeOracle oracle; |
| /** |
| * This map prevents cyclic type dependencies from overflowing the stack. |
| */ |
| private final Map<JClassType, EntityProxyModel.Builder> peerBuilders = |
| new HashMap<JClassType, EntityProxyModel.Builder>(); |
| /** |
| * Iterated by {@link #getAllProxyModels()}. |
| */ |
| private final Map<JClassType, EntityProxyModel> peers = |
| new LinkedHashMap<JClassType, EntityProxyModel>(); |
| private boolean poisoned; |
| private final JClassType requestContextInterface; |
| private final JClassType requestFactoryInterface; |
| private final JClassType requestInterface; |
| private final JClassType setInterface; |
| private final JClassType splittableType; |
| |
| private final JClassType valueProxyInterface; |
| |
| public RequestFactoryModel(TreeLogger logger, JClassType factoryType) |
| throws UnableToCompleteException { |
| this.logger = logger; |
| this.factoryType = factoryType; |
| this.oracle = factoryType.getOracle(); |
| collectionInterface = oracle.findType(Collection.class.getCanonicalName()); |
| entityProxyInterface = oracle.findType(EntityProxy.class.getCanonicalName()); |
| instanceRequestInterface = oracle.findType(InstanceRequest.class.getCanonicalName()); |
| listInterface = oracle.findType(List.class.getCanonicalName()); |
| mapInterface = oracle.findType(Map.class.getCanonicalName()); |
| requestContextInterface = oracle.findType(RequestContext.class.getCanonicalName()); |
| requestFactoryInterface = oracle.findType(RequestFactory.class.getCanonicalName()); |
| requestInterface = oracle.findType(Request.class.getCanonicalName()); |
| setInterface = oracle.findType(Set.class.getCanonicalName()); |
| splittableType = oracle.findType(Splittable.class.getCanonicalName()); |
| valueProxyInterface = oracle.findType(ValueProxy.class.getCanonicalName()); |
| |
| extraTypes = checkExtraTypes(factoryType, false); |
| for (JMethod method : factoryType.getOverridableMethods()) { |
| if (method.getEnclosingType().equals(requestFactoryInterface)) { |
| // Ignore methods defined an RequestFactory itself |
| continue; |
| } |
| |
| if (method.getParameters().length > 0) { |
| poison("Unexpected parameter on method %s", method.getName()); |
| continue; |
| } |
| |
| JClassType contextType = method.getReturnType().isInterface(); |
| if (contextType == null || !requestContextInterface.isAssignableFrom(contextType)) { |
| poison("Unexpected return type %s on method %s is not" + " an interface assignable to %s", |
| method.getReturnType().getQualifiedSourceName(), method.getName(), |
| requestContextInterface.getSimpleSourceName()); |
| continue; |
| } |
| |
| ContextMethod.Builder builder = new ContextMethod.Builder(); |
| builder.setDeclaredMethod(method); |
| buildContextMethod(builder, contextType); |
| contextMethods.add(builder.build()); |
| } |
| |
| if (poisoned) { |
| die(poisonedMessage()); |
| } |
| } |
| |
| public void accept(ModelVisitor visitor) { |
| if (visitor.visit(this)) { |
| for (EntityProxyModel model : getAllProxyModels()) { |
| model.accept(visitor); |
| } |
| for (ContextMethod method : getMethods()) { |
| method.accept(visitor); |
| } |
| } |
| visitor.endVisit(this); |
| } |
| |
| public Collection<EntityProxyModel> getAllProxyModels() { |
| return Collections.unmodifiableCollection(peers.values()); |
| } |
| |
| /** |
| * These extra types will have already been added to the extra types for each |
| * {@link ContextMethod} in the model. |
| */ |
| public List<EntityProxyModel> getExtraTypes() { |
| return Collections.unmodifiableList(extraTypes); |
| } |
| |
| public JClassType getFactoryType() { |
| return factoryType; |
| } |
| |
| public List<ContextMethod> getMethods() { |
| return Collections.unmodifiableList(contextMethods); |
| } |
| |
| public EntityProxyModel getPeer(JClassType entityProxyType) { |
| return peers.get(entityProxyType); |
| } |
| |
| /** |
| * For debugging use only. |
| */ |
| @Override |
| public String toString() { |
| return getFactoryType().getQualifiedSourceName(); |
| } |
| |
| /** |
| * Examine a RequestContext subtype to populate a ContextMethod. |
| */ |
| private void buildContextMethod(ContextMethod.Builder contextBuilder, JClassType contextType) |
| throws UnableToCompleteException { |
| Service serviceAnnotation = contextType.getAnnotation(Service.class); |
| ServiceName serviceNameAnnotation = contextType.getAnnotation(ServiceName.class); |
| JsonRpcService jsonRpcAnnotation = contextType.getAnnotation(JsonRpcService.class); |
| if (serviceAnnotation == null && serviceNameAnnotation == null && jsonRpcAnnotation == null) { |
| poison("RequestContext subtype %s is missing a @%s or @%s annotation", contextType |
| .getQualifiedSourceName(), Service.class.getSimpleName(), JsonRpcService.class |
| .getSimpleName()); |
| return; |
| } |
| |
| List<RequestMethod> requestMethods = new ArrayList<RequestMethod>(); |
| for (JMethod method : contextType.getInheritableMethods()) { |
| if (method.getEnclosingType().equals(requestContextInterface)) { |
| // Ignore methods declared in RequestContext |
| continue; |
| } |
| |
| RequestMethod.Builder methodBuilder = new RequestMethod.Builder(); |
| methodBuilder.setDeclarationMethod(contextType, method); |
| |
| if (!validateContextMethodAndSetDataType(methodBuilder, method, jsonRpcAnnotation != null)) { |
| continue; |
| } |
| |
| requestMethods.add(methodBuilder.build()); |
| } |
| |
| contextBuilder.setExtraTypes(checkExtraTypes(contextType, true)).setRequestMethods( |
| requestMethods); |
| } |
| |
| /** |
| * Checks type and its supertypes for {@link ExtraTypes} annotations. |
| * |
| * @param type the type to examine |
| * @param addModelExtraTypes if {@code true} the contents of the |
| * {@link #extraTypes} field will be added to the returned list. |
| */ |
| private List<EntityProxyModel> checkExtraTypes(JClassType type, boolean addModelExtraTypes) |
| throws UnableToCompleteException { |
| Set<EntityProxyModel> toReturn = new LinkedHashSet<EntityProxyModel>(); |
| if (addModelExtraTypes && extraTypes != null) { |
| toReturn.addAll(extraTypes); |
| } |
| for (JClassType toExamine : type.getFlattenedSupertypeHierarchy()) { |
| ExtraTypes proxyExtraTypes = toExamine.getAnnotation(ExtraTypes.class); |
| if (proxyExtraTypes != null) { |
| for (Class<? extends BaseProxy> clazz : proxyExtraTypes.value()) { |
| JClassType proxy = oracle.findType(clazz.getCanonicalName()); |
| if (proxy == null) { |
| poison("Unknown class %s in @%s", clazz.getCanonicalName(), ExtraTypes.class |
| .getSimpleName()); |
| } else { |
| toReturn.add(getEntityProxyType(proxy)); |
| } |
| } |
| } |
| } |
| if (toReturn.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| return new ArrayList<EntityProxyModel>(toReturn); |
| } |
| |
| private void die(String message) throws UnableToCompleteException { |
| poison(message); |
| throw new UnableToCompleteException(); |
| } |
| |
| private EntityProxyModel getEntityProxyType(JClassType entityProxyType) |
| throws UnableToCompleteException { |
| entityProxyType = ModelUtils.ensureBaseType(entityProxyType); |
| EntityProxyModel toReturn = peers.get(entityProxyType); |
| if (toReturn == null) { |
| EntityProxyModel.Builder inProgress = peerBuilders.get(entityProxyType); |
| if (inProgress != null) { |
| toReturn = inProgress.peek(); |
| } |
| } |
| if (toReturn == null) { |
| EntityProxyModel.Builder builder = new EntityProxyModel.Builder(); |
| peerBuilders.put(entityProxyType, builder); |
| |
| // Validate possible super-proxy types first |
| for (JClassType supertype : entityProxyType.getFlattenedSupertypeHierarchy()) { |
| List<EntityProxyModel> superTypes = new ArrayList<EntityProxyModel>(); |
| if (supertype != entityProxyType && shouldAttemptProxyValidation(supertype)) { |
| superTypes.add(getEntityProxyType(supertype)); |
| } |
| builder.setSuperProxyTypes(superTypes); |
| } |
| |
| builder.setQualifiedBinaryName(ModelUtils.getQualifiedBaseBinaryName(entityProxyType)); |
| builder.setQualifiedSourceName(ModelUtils.getQualifiedBaseSourceName(entityProxyType)); |
| if (entityProxyInterface.isAssignableFrom(entityProxyType)) { |
| builder.setType(Type.ENTITY); |
| } else if (valueProxyInterface.isAssignableFrom(entityProxyType)) { |
| builder.setType(Type.VALUE); |
| } else { |
| poison("The type %s is not assignable to either %s or %s", entityProxyInterface |
| .getQualifiedSourceName(), valueProxyInterface.getQualifiedSourceName()); |
| // Cannot continue, since knowing the behavior is crucial |
| die(poisonedMessage()); |
| } |
| |
| // Get the server domain object type |
| ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class); |
| ProxyForName proxyForName = entityProxyType.getAnnotation(ProxyForName.class); |
| JsonRpcProxy jsonRpcProxy = entityProxyType.getAnnotation(JsonRpcProxy.class); |
| if (proxyFor == null && proxyForName == null && jsonRpcProxy == null) { |
| poison("The %s type does not have a @%s, @%s, or @%s annotation", entityProxyType |
| .getQualifiedSourceName(), ProxyFor.class.getSimpleName(), ProxyForName.class |
| .getSimpleName(), JsonRpcProxy.class.getSimpleName()); |
| } |
| |
| // Look at the methods declared on the EntityProxy |
| List<RequestMethod> requestMethods = new ArrayList<RequestMethod>(); |
| Map<String, JMethod> duplicatePropertyGetters = new HashMap<String, JMethod>(); |
| for (JMethod method : entityProxyType.getInheritableMethods()) { |
| if (method.getEnclosingType().equals(entityProxyInterface)) { |
| // Ignore methods on EntityProxy |
| continue; |
| } |
| RequestMethod.Builder methodBuilder = new RequestMethod.Builder(); |
| methodBuilder.setDeclarationMethod(entityProxyType, method); |
| |
| JType transportedType; |
| String name = method.getName(); |
| if (JBeanMethod.GET.matches(method)) { |
| transportedType = method.getReturnType(); |
| String propertyName = JBeanMethod.GET.inferName(method); |
| JMethod previouslySeen = duplicatePropertyGetters.get(propertyName); |
| if (previouslySeen == null) { |
| duplicatePropertyGetters.put(propertyName, method); |
| } else { |
| poison("Duplicate accessors for property %s: %s() and %s()", propertyName, |
| previouslySeen.getName(), method.getName()); |
| } |
| |
| } else if (JBeanMethod.SET.matches(method) || JBeanMethod.SET_BUILDER.matches(method)) { |
| transportedType = method.getParameters()[0].getType(); |
| |
| } else if (name.equals("stableId") && method.getParameters().length == 0) { |
| // Ignore any overload of stableId |
| continue; |
| } else { |
| poison("The method %s is neither a getter nor a setter", method.getReadableDeclaration()); |
| continue; |
| } |
| validateTransportableType(methodBuilder, transportedType, false); |
| RequestMethod requestMethod = methodBuilder.build(); |
| requestMethods.add(requestMethod); |
| } |
| builder.setExtraTypes(checkExtraTypes(entityProxyType, false)).setRequestMethods( |
| requestMethods); |
| |
| toReturn = builder.build(); |
| peers.put(entityProxyType, toReturn); |
| peerBuilders.remove(entityProxyType); |
| } |
| return toReturn; |
| } |
| |
| private void poison(String message, Object... args) { |
| logger.log(TreeLogger.ERROR, String.format(message, args)); |
| poisoned = true; |
| } |
| |
| /** |
| * Returns {@code true} if the type is assignable to EntityProxy or ValueProxy |
| * and has a mapping to a domain type. |
| * |
| * @see com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator#shouldAttemptProxyValidation() |
| */ |
| private boolean shouldAttemptProxyValidation(JClassType maybeProxy) { |
| if (!entityProxyInterface.isAssignableFrom(maybeProxy) |
| && !valueProxyInterface.isAssignableFrom(maybeProxy)) { |
| return false; |
| } |
| if (maybeProxy.getAnnotation(ProxyFor.class) == null |
| && maybeProxy.getAnnotation(ProxyForName.class) == null) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Examine a RequestContext method to see if it returns a transportable type. |
| */ |
| private boolean validateContextMethodAndSetDataType(RequestMethod.Builder methodBuilder, |
| JMethod method, boolean allowSetters) throws UnableToCompleteException { |
| JClassType requestReturnType = method.getReturnType().isInterface(); |
| JClassType invocationReturnType; |
| if (requestReturnType == null) { |
| // Primitive return type |
| poison(badContextReturnType(method, requestInterface, instanceRequestInterface)); |
| return false; |
| } |
| |
| if (instanceRequestInterface.isAssignableFrom(requestReturnType)) { |
| // Instance method invocation |
| JClassType[] params = |
| ModelUtils.findParameterizationOf(instanceRequestInterface, requestReturnType); |
| methodBuilder.setInstanceType(getEntityProxyType(params[0])); |
| invocationReturnType = params[1]; |
| } else if (requestInterface.isAssignableFrom(requestReturnType)) { |
| // Static method invocation |
| JClassType[] params = ModelUtils.findParameterizationOf(requestInterface, requestReturnType); |
| invocationReturnType = params[0]; |
| |
| } else { |
| // Unhandled return type, must be something random |
| poison(badContextReturnType(method, requestInterface, instanceRequestInterface)); |
| return false; |
| } |
| |
| // Validate the parameters |
| boolean paramsOk = true; |
| JParameter[] params = method.getParameters(); |
| for (int i = 0; i < params.length; ++i) { |
| JParameter param = params[i]; |
| paramsOk = |
| validateTransportableType(new RequestMethod.Builder(), param.getType(), false) |
| && paramsOk; |
| } |
| |
| // Validate any extra properties on the request type |
| for (JMethod maybeSetter : requestReturnType.getInheritableMethods()) { |
| if (JBeanMethod.SET.matches(maybeSetter) || JBeanMethod.SET_BUILDER.matches(maybeSetter)) { |
| if (allowSetters) { |
| methodBuilder.addExtraSetter(maybeSetter); |
| } else { |
| poison(noSettersAllowed(maybeSetter)); |
| } |
| } |
| } |
| return validateTransportableType(methodBuilder, invocationReturnType, true); |
| } |
| |
| /** |
| * Examines a type to see if it can be transported. |
| */ |
| private boolean validateTransportableType(RequestMethod.Builder methodBuilder, JType type, |
| boolean requireObject) throws UnableToCompleteException { |
| JClassType transportedClass = type.isClassOrInterface(); |
| if (transportedClass == null) { |
| if (requireObject) { |
| poison("The type %s cannot be transported by RequestFactory as" + " a return type", type |
| .getQualifiedSourceName()); |
| return false; |
| } else { |
| // Primitives always ok |
| return true; |
| } |
| } |
| |
| if (ModelUtils.isValueType(oracle, transportedClass) || splittableType.equals(transportedClass)) { |
| // Simple values, like Integer and String |
| methodBuilder.setValueType(true); |
| } else if (entityProxyInterface.isAssignableFrom(transportedClass) |
| || valueProxyInterface.isAssignableFrom(transportedClass)) { |
| // EntityProxy and ValueProxy return types |
| methodBuilder.setEntityType(getEntityProxyType(transportedClass)); |
| } else if (collectionInterface.isAssignableFrom(transportedClass)) { |
| // Only allow certain collections for now |
| JParameterizedType parameterized = transportedClass.isParameterized(); |
| if (parameterized == null) { |
| poison("Requests that return collections of List or Set must be parameterized"); |
| return false; |
| } |
| if (listInterface.equals(parameterized.getBaseType())) { |
| methodBuilder.setCollectionType(CollectionType.LIST); |
| } else if (setInterface.equals(parameterized.getBaseType())) { |
| methodBuilder.setCollectionType(CollectionType.SET); |
| } else { |
| poison("Requests that return collections may be declared with" + " %s or %s only", |
| listInterface.getQualifiedSourceName(), setInterface.getQualifiedSourceName()); |
| return false; |
| } |
| // Also record the element type in the method builder |
| JClassType elementType = |
| ModelUtils.findParameterizationOf(collectionInterface, transportedClass)[0]; |
| methodBuilder.setCollectionElementType(elementType); |
| validateTransportableType(methodBuilder, elementType, requireObject); |
| } else if (mapInterface.isAssignableFrom(transportedClass)) { |
| JParameterizedType parameterized = transportedClass.isParameterized(); |
| if (parameterized == null) { |
| poison("Requests that return Maps must be parameterized"); |
| return false; |
| } |
| if (mapInterface.equals(parameterized.getBaseType())) { |
| methodBuilder.setCollectionType(CollectionType.MAP); |
| } else { |
| poison("Requests that return maps may be declared with" + " %s only", mapInterface |
| .getQualifiedSourceName()); |
| return false; |
| } |
| // Also record the element type in the method builder |
| JClassType[] params = ModelUtils.findParameterizationOf(mapInterface, transportedClass); |
| JClassType keyType = params[0]; |
| JClassType valueType = params[1]; |
| methodBuilder.setMapKeyType(keyType); |
| methodBuilder.setMapValueType(valueType); |
| validateTransportableType(methodBuilder, keyType, requireObject); |
| validateTransportableType(methodBuilder, valueType, requireObject); |
| } else { |
| // Unknown type, fail |
| poison("Invalid Request parameterization %s", transportedClass.getQualifiedSourceName()); |
| return false; |
| } |
| methodBuilder.setDataType(transportedClass); |
| return true; |
| } |
| |
| } |