| /* |
| * 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.gwt.requestfactory.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.gwt.requestfactory.rebind.model.RequestMethod.CollectionType; |
| import com.google.gwt.requestfactory.shared.EntityProxy; |
| import com.google.gwt.requestfactory.shared.InstanceRequest; |
| import com.google.gwt.requestfactory.shared.ProxyFor; |
| import com.google.gwt.requestfactory.shared.Request; |
| import com.google.gwt.requestfactory.shared.RequestContext; |
| import com.google.gwt.requestfactory.shared.RequestFactory; |
| import com.google.gwt.requestfactory.shared.Service; |
| |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Represents a RequestFactory interface declaration. |
| */ |
| public class RequestFactoryModel { |
| 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 poisonedMessage() { |
| return "Unable to create RequestFactoryModel model due to previous errors"; |
| } |
| |
| private final TreeLogger logger; |
| private final JClassType collectionInterface; |
| private final List<ContextMethod> contextMethods = new ArrayList<ContextMethod>(); |
| private final JClassType entityProxyInterface; |
| private final JClassType factoryType; |
| private final JClassType instanceRequestInterface; |
| private final JClassType listInterface; |
| 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 setInterface; |
| private final JClassType requestContextInterface; |
| private final JClassType requestFactoryInterface; |
| private final JClassType requestInterface; |
| |
| 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()); |
| setInterface = oracle.findType(Set.class.getCanonicalName()); |
| requestContextInterface = oracle.findType(RequestContext.class.getCanonicalName()); |
| requestFactoryInterface = oracle.findType(RequestFactory.class.getCanonicalName()); |
| requestInterface = oracle.findType(Request.class.getCanonicalName()); |
| |
| 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 Collection<EntityProxyModel> getAllProxyModels() { |
| return Collections.unmodifiableCollection(peers.values()); |
| } |
| |
| 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); |
| if (serviceAnnotation == null) { |
| poison("RequestContext subtype %s is missing a @%s annotation", |
| contextType.getQualifiedSourceName(), Service.class.getSimpleName()); |
| return; |
| } |
| Class<?> serviceClass = serviceAnnotation.value(); |
| contextBuilder.setServiceClass(serviceClass); |
| |
| 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(method); |
| |
| if (!validateContextMethodAndSetDataType(methodBuilder, method, serviceClass)) { |
| continue; |
| } |
| |
| requestMethods.add(methodBuilder.build()); |
| } |
| |
| contextBuilder.setRequestMethods(requestMethods); |
| } |
| |
| private void die(String message) throws UnableToCompleteException { |
| poison(message); |
| throw new UnableToCompleteException(); |
| } |
| |
| /** |
| * Return a list of public methods that match the given methodName. |
| */ |
| private List<Method> findMethods(Class<?> domainType, String methodName) { |
| List<Method> toReturn = new ArrayList<Method>(); |
| for (Method method : domainType.getMethods()) { |
| if (methodName.equals(method.getName()) |
| && (method.getModifiers() & Modifier.PUBLIC) != 0) { |
| toReturn.add(method); |
| } |
| } |
| return toReturn; |
| } |
| |
| private EntityProxyModel getEntityProxyType(JClassType entityProxyType) |
| throws UnableToCompleteException { |
| 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); |
| |
| builder.setQualifiedSourceName(entityProxyType.getQualifiedSourceName()); |
| |
| // Get the server domain object type |
| ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class); |
| if (proxyFor == null) { |
| poison("The %s type does not have a @%s annotation", |
| entityProxyType.getQualifiedSourceName(), |
| ProxyFor.class.getSimpleName()); |
| // early exit, because further processing causes NPEs in numerous spots |
| die(poisonedMessage()); |
| } else { |
| Class<?> domainType = proxyFor.value(); |
| builder.setProxyFor(domainType); |
| validateDomainType(domainType); |
| } |
| |
| // Look at the methods declared on the EntityProxy |
| List<RequestMethod> requestMethods = new ArrayList<RequestMethod>(); |
| for (JMethod method : entityProxyType.getInheritableMethods()) { |
| if (method.getEnclosingType().equals(entityProxyInterface)) { |
| // Ignore methods on EntityProxy |
| continue; |
| } |
| RequestMethod.Builder methodBuilder = new RequestMethod.Builder(); |
| methodBuilder.setDeclarationMethod(method); |
| |
| JType transportedType; |
| String name = method.getName(); |
| if (name.startsWith("get") && method.getParameters().length == 0) { |
| // Getter |
| transportedType = method.getReturnType(); |
| |
| } else if (name.startsWith("set") && method.getParameters().length == 1) { |
| 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(); |
| if (validateDomainBeanMethod(requestMethod, builder)) { |
| requestMethods.add(requestMethod); |
| } |
| } |
| builder.setRequestMethods(requestMethods); |
| |
| toReturn = builder.build(); |
| peers.put(entityProxyType, toReturn); |
| peerBuilders.remove(entityProxyType); |
| } |
| return toReturn; |
| } |
| |
| private boolean isStatic(Method domainMethod) { |
| return (domainMethod.getModifiers() & Modifier.STATIC) != 0; |
| } |
| |
| private String methodLocation(Method domainMethod) { |
| return domainMethod.getDeclaringClass().getName() + "." |
| + domainMethod.getName(); |
| } |
| |
| private String methodLocation(JMethod proxyMethod) { |
| return proxyMethod.getEnclosingType().getName() + "." |
| + proxyMethod.getName(); |
| } |
| |
| private void poison(String message, Object... args) { |
| logger.log(TreeLogger.ERROR, String.format(message, args)); |
| poisoned = true; |
| } |
| |
| /** |
| * Examine a RequestContext method to see if it returns a transportable type. |
| */ |
| private boolean validateContextMethodAndSetDataType( |
| RequestMethod.Builder methodBuilder, JMethod method, Class<?> serviceClass) |
| throws UnableToCompleteException { |
| JClassType requestReturnType = method.getReturnType().isInterface(); |
| JClassType invocationReturnType; |
| if (requestReturnType == null) { |
| // Primitive return type |
| poison(badContextReturnType(method, requestInterface, |
| instanceRequestInterface)); |
| return false; |
| } |
| |
| /* |
| * TODO: bad assumption |
| * Implicit assumption is that the Service and ProxyFor |
| * classes are the same. This is because an instance method should |
| * technically be looked up on the class that is the instance parameter, |
| * and not on the serviceClass, which consists of static service methods. |
| * Can't be fixed until it is fixed in JsonRequestProcessor. |
| */ |
| Method domainMethod = validateExistsAndNotOverriden(method, serviceClass, |
| false); |
| if (domainMethod == null) { |
| return false; |
| } |
| |
| |
| if (instanceRequestInterface.isAssignableFrom(requestReturnType)) { |
| if (isStatic(domainMethod)) { |
| poison("Method %s.%s is an instance method, " |
| + "while the corresponding method on %s is static", |
| method.getEnclosingType().getName(), |
| method.getName(), |
| serviceClass.getName()); |
| return false; |
| } |
| // Instance method invocation |
| JClassType[] params = ModelUtils.findParameterizationOf( |
| instanceRequestInterface, requestReturnType); |
| methodBuilder.setInstanceType(getEntityProxyType(params[0])); |
| invocationReturnType = params[1]; |
| } else if (requestInterface.isAssignableFrom(requestReturnType)) { |
| if (!isStatic(domainMethod)) { |
| poison("Method %s.%s is a static method, " |
| + "while the corresponding method on %s is not", |
| method.getEnclosingType().getName(), |
| method.getName(), |
| serviceClass.getName()); |
| return false; |
| } |
| // 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(); |
| Class<?>[] domainParams = domainMethod.getParameterTypes(); |
| if (params.length != domainParams.length) { |
| poison("Method %s.%s parameters do not match same method on %s", |
| method.getEnclosingType().getName(), |
| method.getName(), |
| serviceClass.getName()); |
| } |
| for (int i = 0; i < params.length; ++i) { |
| JParameter param = params[i]; |
| Class<?> domainParam = domainParams[i]; |
| paramsOk = validateTransportableType(new RequestMethod.Builder(), |
| param.getType(), false) |
| && paramsOk; |
| paramsOk = validateProxyAndDomainTypeEquals(param.getType(), domainParam, i, |
| methodLocation(method), methodLocation(domainMethod)) |
| && paramsOk; |
| } |
| |
| return validateTransportableType(methodBuilder, invocationReturnType, true) |
| && validateProxyAndDomainTypeEquals(invocationReturnType, |
| domainMethod.getReturnType(), -1, methodLocation(method), |
| methodLocation(domainMethod)) |
| && paramsOk; |
| } |
| |
| /** |
| * Examine a domain method to see if it matches the proxy method. |
| */ |
| private boolean validateDomainBeanMethod(RequestMethod requestMethod, |
| EntityProxyModel.Builder entityBuilder) throws UnableToCompleteException { |
| JMethod proxyMethod = requestMethod.getDeclarationMethod(); |
| // check if method exists on domain object |
| Class<?> domainType = entityBuilder.peek().getProxyFor(); |
| Method domainMethod = validateExistsAndNotOverriden(proxyMethod, domainType, |
| true); |
| if (domainMethod == null) { |
| return false; |
| } |
| |
| boolean isGetter = proxyMethod.getName().startsWith("get"); |
| if (isGetter) { |
| // compare return type of domain to proxy return type |
| String returnTypeName = domainMethod.getReturnType().getName(); |
| // isEntityType() returns true for collections, but we want the Collection |
| String propertyTypeName = |
| requestMethod.isCollectionType() || requestMethod.isValueType() ? |
| requestMethod.getDataType().getQualifiedBinaryName() : |
| requestMethod.getEntityType().getProxyFor().getName(); |
| if (!returnTypeName.equals(propertyTypeName)) { |
| poison("Method %s.%s return type %s does not match return type %s " |
| + " of method %s.%s", domainType.getName(), |
| domainMethod.getName(), returnTypeName, |
| propertyTypeName, |
| proxyMethod.getEnclosingType().getName(), proxyMethod.getName()); |
| return false; |
| } |
| } |
| JParameter[] proxyParams = proxyMethod.getParameters(); |
| Class<?>[] domainParams = domainMethod.getParameterTypes(); |
| if (proxyParams.length != domainParams.length) { |
| poison("Method %s.%s parameter mismatch with %s.%s", |
| proxyMethod.getEnclosingType().getName(), |
| proxyMethod.getName(), |
| domainType.getName(), |
| domainMethod.getName()); |
| return false; |
| } |
| for (int i = 0; i < proxyParams.length; i++) { |
| JType proxyParam = proxyParams[i].getType(); |
| Class<?> domainParam = domainParams[i]; |
| if (!validateProxyAndDomainTypeEquals(proxyParam, domainParam, i, |
| methodLocation(proxyMethod), methodLocation(domainMethod))) { |
| poison("Parameter %d of %s.%s doesn't match method %s.%s", |
| i, proxyMethod.getEnclosingType().getName(), |
| proxyMethod.getName(), |
| domainType.getName(), |
| domainMethod.getName()); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Examine a domain type and see if it includes a getId() method. |
| */ |
| private boolean validateDomainType(Class<?> domainType) { |
| try { |
| domainType.getMethod("getId"); |
| } catch (NoSuchMethodException e) { |
| poison("The class %s is missing method getId()", domainType.getName()); |
| return false; |
| } |
| try { |
| domainType.getMethod("getVersion"); |
| } catch (NoSuchMethodException e) { |
| poison("The class %s is missing method getVersion()", domainType.getName()); |
| return false; |
| } |
| return true; |
| } |
| |
| private Method validateExistsAndNotOverriden(JMethod clientMethod, |
| Class<?> serverType, boolean isGetterOrSetter) { |
| List<Method> domainMethods = findMethods(serverType, clientMethod.getName()); |
| if (domainMethods.size() == 0) { |
| poison("Method %s.%s has no corresponding public method on %s", |
| clientMethod.getEnclosingType().getQualifiedBinaryName(), |
| clientMethod.getName(), serverType.getName()); |
| return null; |
| } |
| if (domainMethods.size() > 1) { |
| poison("Method %s.%s is overloaded on %s", |
| clientMethod.getEnclosingType().getQualifiedBinaryName(), |
| clientMethod.getName(), serverType.getName()); |
| return null; |
| } |
| Method domainMethod = domainMethods.get(0); |
| if (isGetterOrSetter && isStatic(domainMethod)) { |
| poison("Method %s.%s is declared static", serverType.getName(), |
| domainMethod.getName()); |
| return null; |
| } |
| return domainMethod; |
| } |
| |
| /** |
| * Compare type from Proxy and Domain. |
| */ |
| private boolean validateProxyAndDomainTypeEquals(JType proxyType, |
| Class<?> domainType, int paramNumber, String clientMethod, |
| String serverMethod) throws UnableToCompleteException { |
| boolean matchOk = false; |
| if (ModelUtils.isValueType(oracle, proxyType) |
| || collectionInterface.isAssignableFrom(proxyType.isClassOrInterface())) { |
| // allow int to match int or Integer |
| matchOk = proxyType.getQualifiedSourceName().equals( |
| ModelUtils.maybeAutobox(domainType).getName()) |
| || proxyType.getQualifiedSourceName().equals(domainType.getName()); |
| } else { |
| matchOk = getEntityProxyType( |
| proxyType.isClassOrInterface()).getProxyFor().equals(domainType); |
| } |
| if (!matchOk) { |
| if (paramNumber < 0) { |
| poison("Return type of method %s does not match method %s", clientMethod, |
| serverMethod); |
| } else { |
| poison("Parameter %d of method %s does not match method %s", |
| paramNumber, clientMethod, serverMethod); |
| } |
| } |
| return matchOk; |
| } |
| |
| /** |
| * 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)) { |
| // Simple values, like Integer and String |
| methodBuilder.setValueType(true); |
| } else if (entityProxyInterface.isAssignableFrom(transportedClass)) { |
| // EntityProxy 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 { |
| // Unknown type, fail |
| poison("Invalid Request parameterization %s", |
| transportedClass.getQualifiedSourceName()); |
| return false; |
| } |
| methodBuilder.setDataType(transportedClass); |
| return true; |
| } |
| |
| } |