Suport polymorphic return and parameter values in RequestFactory. Issue 5367. Review at http://gwt-code-reviews.appspot.com/1453811 Patch by: bobv Review by: rjrjr git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10317 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java index 718eaed..7bbd30a 100644 --- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java +++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java
@@ -43,6 +43,7 @@ import com.google.web.bindery.requestfactory.gwt.rebind.model.ContextMethod; import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel; import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel.Type; +import com.google.web.bindery.requestfactory.gwt.rebind.model.HasExtraTypes; import com.google.web.bindery.requestfactory.gwt.rebind.model.ModelVisitor; import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModel; import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestMethod; @@ -84,6 +85,24 @@ } @Override + public boolean visit(ContextMethod x) { + visitExtraTypes(x); + return true; + } + + @Override + public boolean visit(EntityProxyModel x) { + visitExtraTypes(x); + return true; + } + + @Override + public boolean visit(RequestFactoryModel x) { + visitExtraTypes(x); + return true; + } + + @Override void examineTypeOnce(JClassType type) { // Need this to handle List<Foo>, Map<Foo> JParameterizedType parameterized = type.isParameterized(); @@ -99,6 +118,14 @@ } peer.accept(this); } + + void visitExtraTypes(HasExtraTypes x) { + if (x.getExtraTypes() != null) { + for (EntityProxyModel extra : x.getExtraTypes()) { + extra.accept(this); + } + } + } } /** @@ -235,6 +262,7 @@ @Override public void endVisit(EntityProxyModel x) { models.add(x); + models.addAll(x.getSuperProxyTypes()); } }); return models;
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java index 4de5093..efa6b4c 100644 --- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java +++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java
@@ -26,7 +26,7 @@ /** * Represents a service endpoint. */ -public class ContextMethod implements AcceptsModelVisitor { +public class ContextMethod implements AcceptsModelVisitor, HasExtraTypes { /** * Builds a {@link ContextMethod}. @@ -42,23 +42,31 @@ } } - public void setDeclaredMethod(JMethod method) { + public Builder setDeclaredMethod(JMethod method) { toReturn.methodName = method.getName(); JClassType returnClass = method.getReturnType().isClassOrInterface(); toReturn.interfaceName = returnClass.getQualifiedSourceName(); toReturn.packageName = returnClass.getPackage().getName(); - toReturn.simpleSourceName = returnClass.getName().replace('.', '_') - + "Impl"; - toReturn.dialect = returnClass.isAnnotationPresent(JsonRpcService.class) - ? Dialect.JSON_RPC : Dialect.STANDARD; + toReturn.simpleSourceName = returnClass.getName().replace('.', '_') + "Impl"; + toReturn.dialect = + returnClass.isAnnotationPresent(JsonRpcService.class) ? Dialect.JSON_RPC + : Dialect.STANDARD; + return this; } - public void setRequestMethods(List<RequestMethod> requestMethods) { + public Builder setExtraTypes(List<EntityProxyModel> extraTypes) { + toReturn.extraTypes = extraTypes; + return this; + } + + public Builder setRequestMethods(List<RequestMethod> requestMethods) { toReturn.requestMethods = requestMethods; + return this; } } private Dialect dialect; + private List<EntityProxyModel> extraTypes; private String interfaceName; private String methodName; private String packageName; @@ -81,6 +89,10 @@ return dialect; } + public List<EntityProxyModel> getExtraTypes() { + return Collections.unmodifiableList(extraTypes); + } + /** * The qualified source name of the RequestContext sub-interface (i.e., the * return type of the method declaration).
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java index 87c2eac..76756a4 100644 --- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java +++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java
@@ -21,7 +21,7 @@ /** * Represents an EntityProxy subtype. */ -public class EntityProxyModel implements AcceptsModelVisitor { +public class EntityProxyModel implements AcceptsModelVisitor, HasExtraTypes { /** * Builds {@link EntityProxyModel}. */ @@ -29,6 +29,9 @@ private EntityProxyModel toReturn = new EntityProxyModel(); public EntityProxyModel build() { + if (toReturn.superProxyTypes == null) { + toReturn.superProxyTypes = Collections.emptyList(); + } try { return toReturn; } finally { @@ -44,21 +47,35 @@ return toReturn; } - public void setQualifiedBinaryName(String qualifiedBinaryName) { - toReturn.qualifiedBinaryName = qualifiedBinaryName; + public Builder setExtraTypes(List<EntityProxyModel> extraTypes) { + toReturn.extraTypes = extraTypes; + return this; } - public void setQualifiedSourceName(String name) { + public Builder setQualifiedBinaryName(String qualifiedBinaryName) { + toReturn.qualifiedBinaryName = qualifiedBinaryName; + return this; + } + + public Builder setQualifiedSourceName(String name) { assert !name.contains(" "); toReturn.qualifiedSourceName = name; + return this; } - public void setRequestMethods(List<RequestMethod> requestMethods) { + public Builder setRequestMethods(List<RequestMethod> requestMethods) { toReturn.requestMethods = requestMethods; + return this; } - public void setType(Type type) { + public Builder setSuperProxyTypes(List<EntityProxyModel> superTypes) { + toReturn.superProxyTypes = superTypes; + return this; + } + + public Builder setType(Type type) { toReturn.type = type; + return this; } } @@ -70,9 +87,11 @@ ENTITY, VALUE } + private List<EntityProxyModel> extraTypes; private String qualifiedBinaryName; private String qualifiedSourceName; private List<RequestMethod> requestMethods; + private List<EntityProxyModel> superProxyTypes; private Type type; private EntityProxyModel() { @@ -87,6 +106,10 @@ visitor.endVisit(this); } + public List<EntityProxyModel> getExtraTypes() { + return Collections.unmodifiableList(extraTypes); + } + public String getQualifiedBinaryName() { return qualifiedBinaryName; } @@ -99,6 +122,13 @@ return Collections.unmodifiableList(requestMethods); } + /** + * Returns the proxy types to which the EntityProxyModel is assignable. + */ + public List<EntityProxyModel> getSuperProxyTypes() { + return superProxyTypes; + } + public Type getType() { return type; }
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java new file mode 100644 index 0000000..6cd8a68 --- /dev/null +++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java
@@ -0,0 +1,26 @@ +/* + * Copyright 2011 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 java.util.List; + +/** + * Capability interface for types that can be annotated with + * {@link com.google.web.bindery.requestfactory.shared.ExtraTypes}. + */ +public interface HasExtraTypes { + List<EntityProxyModel> getExtraTypes(); +}
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java index dc9a23c..3f08de3 100644 --- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java +++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java
@@ -15,8 +15,6 @@ */ package com.google.web.bindery.requestfactory.gwt.rebind.model; -import com.google.web.bindery.autobean.gwt.rebind.model.JBeanMethod; -import com.google.web.bindery.autobean.shared.Splittable; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.JClassType; @@ -26,9 +24,13 @@ 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; @@ -46,6 +48,7 @@ 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; @@ -53,18 +56,16 @@ /** * Represents a RequestFactory interface declaration. */ -public class RequestFactoryModel implements AcceptsModelVisitor { +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 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) { @@ -74,6 +75,7 @@ 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; @@ -83,11 +85,13 @@ /** * This map prevents cyclic type dependencies from overflowing the stack. */ - private final Map<JClassType, EntityProxyModel.Builder> peerBuilders = new HashMap<JClassType, EntityProxyModel.Builder>(); + 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 final Map<JClassType, EntityProxyModel> peers = + new LinkedHashMap<JClassType, EntityProxyModel>(); private boolean poisoned; private final JClassType requestContextInterface; private final JClassType requestFactoryInterface; @@ -114,6 +118,7 @@ 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 @@ -126,10 +131,8 @@ } 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", + 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; @@ -162,6 +165,14 @@ 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; } @@ -185,16 +196,15 @@ /** * Examine a RequestContext subtype to populate a ContextMethod. */ - private void buildContextMethod(ContextMethod.Builder contextBuilder, - JClassType contextType) throws UnableToCompleteException { + 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()); + 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; } @@ -208,15 +218,48 @@ RequestMethod.Builder methodBuilder = new RequestMethod.Builder(); methodBuilder.setDeclarationMethod(contextType, method); - if (!validateContextMethodAndSetDataType(methodBuilder, method, - jsonRpcAnnotation != null)) { + if (!validateContextMethodAndSetDataType(methodBuilder, method, jsonRpcAnnotation != null)) { continue; } requestMethods.add(methodBuilder.build()); } - contextBuilder.setRequestMethods(requestMethods); + 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 { @@ -238,6 +281,15 @@ 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)) { @@ -245,9 +297,8 @@ } 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()); + 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()); } @@ -257,10 +308,9 @@ 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()); + 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 @@ -283,28 +333,26 @@ if (previouslySeen == null) { duplicatePropertyGetters.put(propertyName, method); } else { - poison("Duplicate accessors for property %s: %s() and %s()", - propertyName, previouslySeen.getName(), method.getName()); + 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)) { + } 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) { + } 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()); + 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.setRequestMethods(requestMethods); + builder.setExtraTypes(checkExtraTypes(entityProxyType, false)).setRequestMethods( + requestMethods); toReturn = builder.build(); peers.put(entityProxyType, toReturn); @@ -319,36 +367,50 @@ } /** + * 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 { + 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)); + poison(badContextReturnType(method, requestInterface, instanceRequestInterface)); return false; } if (instanceRequestInterface.isAssignableFrom(requestReturnType)) { // Instance method invocation - JClassType[] params = ModelUtils.findParameterizationOf( - instanceRequestInterface, requestReturnType); + 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); + JClassType[] params = ModelUtils.findParameterizationOf(requestInterface, requestReturnType); invocationReturnType = params[0]; } else { // Unhandled return type, must be something random - poison(badContextReturnType(method, requestInterface, - instanceRequestInterface)); + poison(badContextReturnType(method, requestInterface, instanceRequestInterface)); return false; } @@ -357,15 +419,14 @@ 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; + 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 (JBeanMethod.SET.matches(maybeSetter) || JBeanMethod.SET_BUILDER.matches(maybeSetter)) { if (allowSetters) { methodBuilder.addExtraSetter(maybeSetter); } else { @@ -379,14 +440,13 @@ /** * Examines a type to see if it can be transported. */ - private boolean validateTransportableType( - RequestMethod.Builder methodBuilder, JType type, boolean requireObject) - throws UnableToCompleteException { + 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()); + poison("The type %s cannot be transported by RequestFactory as" + " a return type", type + .getQualifiedSourceName()); return false; } else { // Primitives always ok @@ -394,8 +454,7 @@ } } - if (ModelUtils.isValueType(oracle, transportedClass) - || splittableType.equals(transportedClass)) { + if (ModelUtils.isValueType(oracle, transportedClass) || splittableType.equals(transportedClass)) { // Simple values, like Integer and String methodBuilder.setValueType(true); } else if (entityProxyInterface.isAssignableFrom(transportedClass) @@ -414,14 +473,13 @@ } 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()); + 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]; + JClassType elementType = + ModelUtils.findParameterizationOf(collectionInterface, transportedClass)[0]; methodBuilder.setCollectionElementType(elementType); validateTransportableType(methodBuilder, elementType, requireObject); } else if (mapInterface.isAssignableFrom(transportedClass)) { @@ -433,13 +491,12 @@ if (mapInterface.equals(parameterized.getBaseType())) { methodBuilder.setCollectionType(CollectionType.MAP); } else { - poison("Requests that return maps may be declared with" + " %s only", - mapInterface.getQualifiedSourceName()); + 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[] params = ModelUtils.findParameterizationOf(mapInterface, transportedClass); JClassType keyType = params[0]; JClassType valueType = params[1]; methodBuilder.setMapKeyType(keyType); @@ -448,8 +505,7 @@ validateTransportableType(methodBuilder, valueType, requireObject); } else { // Unknown type, fail - poison("Invalid Request parameterization %s", - transportedClass.getQualifiedSourceName()); + poison("Invalid Request parameterization %s", transportedClass.getQualifiedSourceName()); return false; } methodBuilder.setDataType(transportedClass);
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java b/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java index 683c6a6..448ded7 100644 --- a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java +++ b/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
@@ -97,25 +97,30 @@ * Returns a method descriptor that should be invoked on the service object. */ public String getDomainMethodDescriptor(String operation) { - return operationData.get(new OperationKey(operation)).getDomainMethodDescriptor(); + OperationData data = operationData.get(new OperationKey(operation)); + return data == null ? null : data.getDomainMethodDescriptor(); } public String getRequestContext(String operation) { - return operationData.get(new OperationKey(operation)).getRequestContext(); + OperationData data = operationData.get(new OperationKey(operation)); + return data == null ? null : data.getRequestContext(); } public String getRequestContextMethodDescriptor(String operation) { - return operationData.get(new OperationKey(operation)).getClientMethodDescriptor(); + OperationData data = operationData.get(new OperationKey(operation)); + return data == null ? null : data.getClientMethodDescriptor(); } public String getRequestContextMethodName(String operation) { - return operationData.get(new OperationKey(operation)).getMethodName(); + OperationData data = operationData.get(new OperationKey(operation)); + return data == null ? null : data.getMethodName(); } /** * Returns a type's binary name based on an obfuscated token. */ public String getTypeFromToken(String token) { - return typeTokens.get(token).getClassName(); + Type type = typeTokens.get(token); + return type == null ? null : type.getClassName(); } } \ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java index b0933bc..9d19c99 100644 --- a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java +++ b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
@@ -31,6 +31,7 @@ import com.google.web.bindery.autobean.shared.ValueCodex; 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.ProxyFor; import com.google.web.bindery.requestfactory.shared.ProxyForName; @@ -48,6 +49,7 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -55,6 +57,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; @@ -358,6 +362,47 @@ } } + private class ExtraTypesCollector extends EmptyVisitor { + private final ErrorContext logger; + private final Set<Type> collected = new HashSet<Type>(); + + public ExtraTypesCollector(ErrorContext logger) { + this.logger = logger; + logger.spam("Collecting extra types"); + } + + public Type[] exec(Type type) { + for (Type toExamine : getSupertypes(logger, type)) { + RequestFactoryInterfaceValidator.this.visit(logger, toExamine.getInternalName(), this); + } + return collected.toArray(new Type[collected.size()]); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (!desc.equals(Type.getDescriptor(ExtraTypes.class))) { + return null; + } + + return new EmptyVisitor() { + + @Override + public AnnotationVisitor visitArray(String name) { + if (!"value".equals(name)) { + return null; + } + return new EmptyVisitor() { + @Override + public void visit(String name, Object value) { + collected.add((Type) value); + } + }; + } + + }; + } + } + /** * Collects information about domain objects. This visitor is intended to be * iteratively applied to collect all methods in a type hierarchy. @@ -626,7 +671,8 @@ * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy, * FooBProxy). */ - private final Map<Type, List<Type>> domainToClientType = new HashMap<Type, List<Type>>(); + private final Map<Type, SortedSet<Type>> domainToClientType = + new HashMap<Type, SortedSet<Type>>(); /** * The type {@link EntityProxy}. */ @@ -650,6 +696,20 @@ */ private final Map<Type, Set<RFMethod>> methodsInHierarchy = new HashMap<Type, Set<RFMethod>>(); /** + * Not static because it depends on {@link #parentLogger}. + */ + private final Comparator<Type> typeNameComparator = new Comparator<Type>() { + @Override + public int compare(Type a, Type b) { + if (isAssignable(parentLogger, a, b)) { + return 1; + } else if (isAssignable(parentLogger, b, a)) { + return -1; + } + return a.getInternalName().compareTo(b.getInternalName()); + } + }; + /** * Used to resolve obfuscated type tokens. */ private final Map<String, Type> typeTokens = new HashMap<String, Type>(); @@ -831,7 +891,7 @@ return; } - Type domainServiceType = getDomainType(logger, requestContextType); + Type domainServiceType = getDomainType(logger, requestContextType, false); if (domainServiceType == errorType) { logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName .toSourceName(binaryName), Service.class.getSimpleName(), ServiceName.class @@ -860,6 +920,7 @@ maybeCheckReferredProxies(logger, method); } + maybeCheckExtraTypes(logger, requestContextType); checkUnresolvedKeyTypes(logger); } @@ -903,6 +964,8 @@ validateRequestContext(returnType.getClassName()); } } + + maybeCheckExtraTypes(logger, requestFactoryType); } /** @@ -942,7 +1005,7 @@ */ String getEntityProxyTypeName(String domainTypeBinaryName, String clientTypeBinaryName) { Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeBinaryName)); - List<Type> found = domainToClientType.get(key); + SortedSet<Type> found = domainToClientType.get(key); /* * If nothing was found look for proxyable supertypes the domain object can @@ -970,7 +1033,7 @@ // Common case if (found.size() == 1) { - typeToReturn = found.get(0); + typeToReturn = found.first(); } else { // Search for the first assignable type Type assignableTo = Type.getObjectType(BinaryName.toInternalName(clientTypeBinaryName)); @@ -993,10 +1056,9 @@ clientToDomainType.put(clientType, domainType); if (isAssignable(logger, baseProxyIntf, clientType)) { - maybeCheckProxyType(logger, clientType); - List<Type> list = domainToClientType.get(domainType); + SortedSet<Type> list = domainToClientType.get(domainType); if (list == null) { - list = new ArrayList<Type>(); + list = new TreeSet<Type>(typeNameComparator); domainToClientType.put(domainType, list); } list.add(clientType); @@ -1127,9 +1189,9 @@ private Method createDomainMethod(ErrorContext logger, Method clientMethod) { Type[] args = clientMethod.getArgumentTypes(); for (int i = 0, j = args.length; i < j; i++) { - args[i] = getDomainType(logger, args[i]); + args[i] = getDomainType(logger, args[i], true); } - Type returnType = getDomainType(logger, clientMethod.getReturnType()); + Type returnType = getDomainType(logger, clientMethod.getReturnType(), true); return new Method(clientMethod.getName(), returnType, args); } @@ -1280,7 +1342,7 @@ * domain object type. RequestContext types will be resolved to their service * object. */ - private Type getDomainType(ErrorContext logger, Type clientType) { + private Type getDomainType(ErrorContext logger, Type clientType, boolean requireMapping) { Type domainType = clientToDomainType.get(clientType); if (domainType != null) { return domainType; @@ -1294,8 +1356,10 @@ DomainMapper pv = new DomainMapper(logger); visit(logger, clientType.getInternalName(), pv); if (pv.getDomainInternalName() == null) { - logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType), - ProxyFor.class.getSimpleName(), Service.class.getSimpleName()); + if (requireMapping) { + logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType), + ProxyFor.class.getSimpleName(), Service.class.getSimpleName()); + } domainType = errorType; } else { domainType = Type.getObjectType(pv.getDomainInternalName()); @@ -1306,7 +1370,9 @@ } } addToDomainMap(logger, domainType, clientType); - maybeCheckProxyType(logger, clientType); + if (domainType != errorType) { + maybeCheckProxyType(logger, clientType); + } return domainType; } @@ -1457,6 +1523,14 @@ } /** + * Examines a type for an {@link ExtraTypes} annotation and processes the + * referred types. + */ + private void maybeCheckExtraTypes(ErrorContext logger, Type type) { + maybeCheckProxyType(logger, new ExtraTypesCollector(logger.setType(type)).exec(type)); + } + + /** * Examine an array of Types and call {@link #validateEntityProxy(String)} or * {@link #validateValueProxy(String)} if the type is a proxy. */ @@ -1494,6 +1568,23 @@ } } + /** + * 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.gwt.rebind.model.RequestFactoryModel#shouldAttemptProxyValidation() + */ + private boolean shouldAttemptProxyValidation(ErrorContext logger, Type type) { + logger = logger.setType(type); + if (!isAssignable(logger, entityProxyIntf, type) && !isAssignable(logger, valueProxyIntf, type)) { + return false; + } + if (getDomainType(logger, type, false) == errorType) { + return false; + } + return true; + } + private void validateProxy(String binaryName, Type expectedType, boolean requireId) { if (fastFail(binaryName)) { return; @@ -1509,8 +1600,15 @@ return; } + // Check supertypes first + for (Type supertype : getSupertypes(logger, proxyType)) { + if (shouldAttemptProxyValidation(logger, supertype)) { + maybeCheckProxyType(logger, supertype); + } + } + // Find the domain type - Type domainType = getDomainType(logger, proxyType); + Type domainType = getDomainType(logger, proxyType, false); if (domainType == errorType) { logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName .toSourceName(binaryName), ProxyFor.class.getSimpleName(), ProxyForName.class @@ -1539,6 +1637,7 @@ checkPropertyMethod(logger, clientPropertyMethod, domainType); maybeCheckReferredProxies(logger, clientPropertyMethod); } + maybeCheckExtraTypes(logger, proxyType); } /**
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java index 7b8c5a6..38e5a97 100644 --- a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java +++ b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
@@ -404,7 +404,7 @@ boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass); boolean isId = EntityProxyId.class.isAssignableFrom(returnClass); if (isProxy || isId) { - Class<? extends BaseProxy> proxyClass = assignableTo.asSubclass(BaseProxy.class); + Class<? extends BaseProxy> proxyClass = returnClass.asSubclass(BaseProxy.class); BaseProxy entity = resolveClientProxy(domainValue, proxyClass, propertyRefs, key, prefix); if (isId) { return assignableTo.cast(((EntityProxy) entity).stableId());
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java b/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java index eaab4bc..8d93655 100644 --- a/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java +++ b/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java
@@ -18,6 +18,7 @@ /** * A proxy for a server-side domain object. */ +@ProxyFor(Object.class) public interface EntityProxy extends BaseProxy { /** * Returns the {@link EntityProxyId} that identifies a particular instance of
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java new file mode 100644 index 0000000..0273c97 --- /dev/null +++ b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
@@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.shared; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * This annotation can be applied to {@link EntityProxy}, {@link ValueProxy}, + * {@link RequestContext}, and {@link RequestFactory} type declarations to + * include additional polymorphic proxy types that are not explicitly + * referenced. + */ +@Target(ElementType.TYPE) +public @interface ExtraTypes { + Class<? extends BaseProxy>[] value(); +}
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java b/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java index bbd7760..14e9aa2 100644 --- a/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java +++ b/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java
@@ -19,5 +19,6 @@ * An analog to EntityProxy for domain types that do not have an identity * concept. */ +@ProxyFor(Object.class) public interface ValueProxy extends BaseProxy { }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java index 111e77f..ae23ac2 100644 --- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java +++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
@@ -15,10 +15,12 @@ */ package com.google.web.bindery.requestfactory.vm; +import com.google.web.bindery.autobean.shared.AutoBean; import com.google.web.bindery.autobean.shared.AutoBean.PropertyName; import com.google.web.bindery.autobean.shared.AutoBeanFactory; import com.google.web.bindery.autobean.vm.impl.BeanMethod; import com.google.web.bindery.autobean.vm.impl.TypeUtils; +import com.google.web.bindery.requestfactory.shared.BaseProxy; import com.google.web.bindery.requestfactory.shared.InstanceRequest; import com.google.web.bindery.requestfactory.shared.JsonRpcContent; import com.google.web.bindery.requestfactory.shared.JsonRpcWireName; @@ -26,9 +28,10 @@ import com.google.web.bindery.requestfactory.shared.RequestContext; import com.google.web.bindery.requestfactory.shared.impl.AbstractRequest; import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext; -import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestFactory; import com.google.web.bindery.requestfactory.shared.impl.RequestData; +import com.google.web.bindery.requestfactory.shared.impl.SimpleProxyId; import com.google.web.bindery.requestfactory.vm.impl.OperationKey; +import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; @@ -182,11 +185,13 @@ static final Object[] NO_ARGS = new Object[0]; private final Class<? extends RequestContext> context; private final Dialect dialect; + private final TypeTokenResolver tokenResolver; - protected InProcessRequestContext(AbstractRequestFactory factory, Dialect dialect, + protected InProcessRequestContext(InProcessRequestFactory factory, Dialect dialect, Class<? extends RequestContext> context) { super(factory, dialect); this.context = context; + this.tokenResolver = factory.getTypeTokenResolver(); this.dialect = dialect; } @@ -198,6 +203,14 @@ } @Override + public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id) { + if (tokenResolver.isReferencedType(clazz.getName())) { + return super.createProxy(clazz, id); + } + throw new IllegalArgumentException("Unknown proxy type " + clazz.getName()); + } + + @Override protected AutoBeanFactory getAutoBeanFactory() { return ((InProcessRequestFactory) getRequestFactory()).getAutoBeanFactory(); }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java index da49296..fc3802b 100644 --- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java +++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
@@ -131,4 +131,8 @@ protected String getTypeToken(Class<? extends BaseProxy> clazz) { return isEntityType(clazz) || isValueType(clazz) ? OperationKey.hash(clazz.getName()) : null; } + + TypeTokenResolver getTypeTokenResolver() { + return deobfuscator; + } }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java index 96caf85..4ed5112 100644 --- a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java +++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
@@ -22,8 +22,10 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Properties; +import java.util.Set; /** * Resolves payload type tokens to binary class names. @@ -46,6 +48,8 @@ public TypeTokenResolver build() { TypeTokenResolver toReturn = d; toReturn.typeTokens = Collections.unmodifiableMap(toReturn.typeTokens); + toReturn.referencedTypes = + Collections.unmodifiableSet(new HashSet<String>(toReturn.typeTokens.values())); d = null; return toReturn; } @@ -86,6 +90,11 @@ } /** + * The values of {@link #typeTokens}. + */ + private Set<String> referencedTypes; + + /** * Map of obfuscated ids to binary class names. */ private Map<String, String> typeTokens = new HashMap<String, String>(); @@ -94,6 +103,10 @@ return typeTokens.get(typeToken); } + public boolean isReferencedType(String binaryName) { + return referencedTypes.contains(binaryName); + } + /** * Closes the OutputStream. */
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java index 83baef3..ffd546d 100644 --- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java +++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
@@ -19,27 +19,751 @@ import com.google.gwt.junit.client.GWTTestCase; import com.google.web.bindery.event.shared.EventBus; import com.google.web.bindery.event.shared.SimpleEventBus; +import com.google.web.bindery.requestfactory.shared.EntityProxy; +import com.google.web.bindery.requestfactory.shared.ExtraTypes; +import com.google.web.bindery.requestfactory.shared.ProxyFor; +import com.google.web.bindery.requestfactory.shared.Receiver; +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.SimpleFooProxy; import com.google.web.bindery.requestfactory.shared.TestRequestFactory; +import java.util.Arrays; +import java.util.List; + /** - * Just tests the - * {@link com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator} to see - * if polymorphic signatures are allowed. + * Tests behavior of RequestFactory when using polymorphic Request return types + * and other non-trivial type hierarchies. */ public class RequestFactoryPolymorphicTest extends GWTTestCase { + /** + * Mandatory javadoc. + */ + public static class A { + private static int idCount = 0; + + public static A findA(int id) { + A toReturn = new A(); + toReturn.id = id; + return toReturn; + } + + private String a = "a"; + + protected int id = idCount++; + + public String getA() { + return a; + } + + public int getId() { + return id; + } + + public int getVersion() { + return 0; + } + + public void setA(String value) { + a = value; + } + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(A.class) + public interface AProxy extends EntityProxy, HasA { + // Mix in getA() from HasA + void setA(String value); + } + + /** + * Mandatory javadoc. + */ + public static class ASub extends A { + } + + /** + * Mandatory javadoc. + */ + public static class B extends A { + public static B findB(int id) { + B toReturn = new B(); + toReturn.id = id; + return toReturn; + } + + private String b = "b"; + + public String getB() { + return b; + } + + public void setB(String value) { + b = value; + } + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(B.class) + public interface B1Proxy extends AProxy { + String getB(); + + void setB(String value); + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(B.class) + public interface B2Proxy extends AProxy { + String getB(); + + void setB(String value); + } + + /** + * Mandatory javadoc. + */ + public static class BSub extends B { + } + + /** + * Mandatory javadoc. + */ + public static class C extends B { + public static C findC(int id) { + C toReturn = new C(); + toReturn.id = id; + return toReturn; + } + + private String c = "c"; + + public String getC() { + return c; + } + + public void setC(String value) { + c = value; + } + } + /** + * Mandatory javadoc. + */ + @ProxyFor(C.class) + public interface C1Proxy extends B1Proxy { + String getC(); + + void setC(String value); + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(C.class) + public interface C2Proxy extends B2Proxy { + String getC(); + + void setC(String value); + } + + /** + * Mandatory javadoc. + */ + public static class CSub extends C { + } + + /** + * Mandatory javadoc. + */ + public static class D extends A { + public static D findD(int id) { + D toReturn = new D(); + toReturn.id = id; + return toReturn; + } + + private String d = "d"; + + public String getD() { + return d; + } + + public void setD(String value) { + d = value; + } + } + + /** + * Mandatory javadoc. + */ + @ExtraTypes(D2Proxy.class) + @ProxyFor(D.class) + public interface D1Proxy extends AProxy { + String getD(); + + void setD(String value); + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(D.class) + public interface D2Proxy extends EntityProxy { + String getD(); + + void setD(String value); + } + + /** + * This class should not be referenced except as a superclass of + * {@link MoreDerivedProxy}. + */ + @ProxyFor(D.class) + public interface D3Proxy extends EntityProxy { + String getD(); + + void setD(String value); + } + + /** + * Mandatory javadoc. + */ + public static class DSub extends D { + } + + /** + * Mandatory javadoc. + */ + public interface HasA { + String getA(); + } + + /** + * Mandatory javadoc. + */ + public static class Impl { + public static A AasA() { + return new A(); + } + + public static A BasA() { + return new B(); + } + + public static B BasB() { + return new B(); + } + + public static A CasA() { + return new C(); + } + + public static B CasB() { + return new C(); + } + + public static String checkA(Object obj) { + return A.class.equals(obj.getClass()) && "A".equals(((A) obj).getA()) ? "" : "checkA"; + } + + public static String checkB(Object obj) { + return B.class.equals(obj.getClass()) && "B".equals(((B) obj).getB()) ? "" : "checkB"; + } + + public static String checkC(Object obj) { + return C.class.equals(obj.getClass()) && "C".equals(((C) obj).getC()) ? "" : "checkC"; + } + + public static String checkD(Object obj) { + return D.class.equals(obj.getClass()) && "D".equals(((D) obj).getD()) ? "" : "checkD"; + } + + public static String checkList(List<Object> list) { + if (list.size() != 4) { + return "size"; + } + String temp; + temp = checkA(list.get(0)); + if (!temp.isEmpty()) { + return temp; + } + temp = checkB(list.get(1)); + if (!temp.isEmpty()) { + return temp; + } + temp = checkC(list.get(2)); + if (!temp.isEmpty()) { + return temp; + } + temp = checkD(list.get(3)); + if (!temp.isEmpty()) { + return temp; + } + return ""; + } + + public static String checkW(Object obj) { + return W.class.equals(obj.getClass()) && "W".equals(((W) obj).getW()) ? "" : "checkW"; + } + + public static String checkZ(Object obj) { + return Z.class.equals(obj.getClass()) && "Z".equals(((Z) obj).getZ()) ? "" : "checkZ"; + } + + public static A DasA() { + return new D(); + } + + public static D DasD() { + return new D(); + } + + public static List<A> testCollection() { + return Arrays.asList(new A(), new B(), new C(), new D()); + } + + public static List<A> testCollectionSub() { + return Arrays.asList(new ASub(), new BSub(), new CSub(), new DSub()); + } + + public static W W() { + return new W(); + } + + public static W W2() { + return new W(); + } + + public static Z Z() { + return new Z(); + } + + public static Z Z2() { + return new Z(); + } + } + + /** + * Check diamond inheritance. + */ + @ProxyFor(D.class) + public interface MoreDerivedProxy extends AProxy, D1Proxy, D2Proxy, D3Proxy { + } + + /** + * The W and Z types are used with the WZProxy and ZWProxy to demonstrate + * proxy interface inheritance even when their proxy-for types aren't related. + */ + public static class W { + private static int idCount; + + public static W findW(int id) { + W toReturn = new W(); + toReturn.id = id; + return toReturn; + } + + private int id = idCount++; + + private String w = "w"; + + private String z = "z"; + + public int getId() { + return id; + } + + public int getVersion() { + return 0; + } + + public String getW() { + return w; + } + + public String getZ() { + return z; + } + + public void setW(String w) { + this.w = w; + } + + public void setZ(String z) { + this.z = z; + } + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(W.class) + public interface WProxy extends EntityProxy { + String getW(); + + void setW(String w); + } + /** + * Mandatory javadoc. + */ + @ProxyFor(W.class) + public interface WZProxy extends ZProxy { + String getW(); + + void setW(String w); + } + /** + * @see W + */ + public static class Z extends A { + private static int idCount; + + public static Z findZ(int id) { + Z toReturn = new Z(); + toReturn.id = id; + return toReturn; + } + + private int id = idCount++; + + private String w = "w"; + + private String z = "z"; + + public int getId() { + return id; + } + + public int getVersion() { + return 0; + } + + public String getW() { + return w; + } + + public String getZ() { + return z; + } + + public void setW(String w) { + this.w = w; + } + + public void setZ(String z) { + this.z = z; + } + } + /** + * Mandatory javadoc. + */ + @ProxyFor(Z.class) + public interface ZProxy extends EntityProxy { + String getZ(); + + void setZ(String z); + } + + /** + * Mandatory javadoc. + */ + @ProxyFor(Z.class) + public interface ZWProxy extends WProxy { + String getZ(); + + void setZ(String z); + } + + /** + * Mandatory javadoc. + */ + @ExtraTypes(B2Proxy.class) + protected interface Factory extends RequestFactory { + Context ctx(); + } + + /** + * Verifies that the received proxy's proxy type is exactly {@code clazz} and + * checks the values of the properties. + */ + static class CastAndCheckReceiver extends Receiver<EntityProxy> { + public static CastAndCheckReceiver of(Class<?> clazz) { + return new CastAndCheckReceiver(clazz); + } + + private final Class<?> clazz; + + public CastAndCheckReceiver(Class<?> clazz) { + this.clazz = clazz; + } + + @Override + public void onSuccess(EntityProxy response) { + assertNotNull(response); + assertEquals(clazz, response.stableId().getProxyClass()); + if (response instanceof HasA) { + assertEquals("a", ((HasA) response).getA()); + } + if (response instanceof B1Proxy) { + assertEquals("b", ((B1Proxy) response).getB()); + } + if (response instanceof B2Proxy) { + assertEquals("b", ((B2Proxy) response).getB()); + } + if (response instanceof C1Proxy) { + assertEquals("c", ((C1Proxy) response).getC()); + } + if (response instanceof C2Proxy) { + assertEquals("c", ((C2Proxy) response).getC()); + } + if (response instanceof D1Proxy) { + assertEquals("d", ((D1Proxy) response).getD()); + } + if (response instanceof D2Proxy) { + assertEquals("d", ((D2Proxy) response).getD()); + } + } + } + + @ExtraTypes({C1Proxy.class, C2Proxy.class, MoreDerivedProxy.class}) + @Service(Impl.class) + interface Context extends RequestContext { + Request<AProxy> AasA(); + + Request<AProxy> BasA(); + + Request<B1Proxy> BasB(); + + Request<AProxy> CasA(); + + Request<B1Proxy> CasB(); + + Request<String> checkA(EntityProxy proxy); + + Request<String> checkB(EntityProxy proxy); + + Request<String> checkC(EntityProxy proxy); + + Request<String> checkD(EntityProxy proxy); + + Request<String> checkList(List<EntityProxy> list); + + Request<String> checkW(EntityProxy proxy); + + Request<String> checkZ(EntityProxy proxy); + + Request<AProxy> DasA(); + + Request<D1Proxy> DasD(); + + Request<List<AProxy>> testCollection(); + + Request<List<AProxy>> testCollectionSub(); + + Request<WProxy> W(); + + Request<WZProxy> W2(); + + Request<ZProxy> Z(); + + Request<ZWProxy> Z2(); + } + + /** + * Checks that the incoming list is {@code [ A, B, C, D ]}. + */ + static class ListChecker extends Receiver<List<AProxy>> { + @Override + public void onSuccess(List<AProxy> response) { + new CastAndCheckReceiver(AProxy.class).onSuccess(response.get(0)); + new CastAndCheckReceiver(B1Proxy.class).onSuccess(response.get(1)); + new CastAndCheckReceiver(C1Proxy.class).onSuccess(response.get(2)); + new CastAndCheckReceiver(MoreDerivedProxy.class).onSuccess(response.get(3)); + } + } + + private final Receiver<String> checkReceiver = new Receiver<String>() { + + @Override + public void onSuccess(String response) { + assertEquals("", response); + } + }; + + private static final int TEST_DELAY = 5000; + + protected Factory factory; + @Override public String getModuleName() { return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite"; } - public void testGenerator() { + public void testCreation() { + delayTestFinish(TEST_DELAY); + Context ctx = factory.ctx(); + checkA(ctx, AProxy.class); + checkB(ctx, B1Proxy.class); + checkB(ctx, B2Proxy.class); + checkC(ctx, C1Proxy.class); + checkC(ctx, C2Proxy.class); + checkD(ctx, D1Proxy.class); + checkD(ctx, D2Proxy.class); + // D3Proxy is a proxy supertype, assignable to BaseProxy and has @ProxyFor + checkD(ctx, D3Proxy.class); + checkD(ctx, MoreDerivedProxy.class); + checkW(ctx, WProxy.class); + checkW(ctx, WZProxy.class); + checkZ(ctx, ZProxy.class); + checkZ(ctx, ZWProxy.class); + ctx.fire(new Receiver<Void>() { + @Override + public void onSuccess(Void response) { + finishTest(); + } + }); + } + + /** + * Ensure heterogeneous collections work. + */ + public void testCreationList() { + Context ctx = factory.ctx(); + ctx.checkList( + Arrays.asList(create(ctx, AProxy.class), create(ctx, B2Proxy.class), create(ctx, + C2Proxy.class), create(ctx, D2Proxy.class))).to(checkReceiver); + ctx.fire(new Receiver<Void>() { + @Override + public void onSuccess(Void response) { + finishTest(); + } + }); + } + + public void testGenericRequest() { TestRequestFactory rf = GWT.create(TestRequestFactory.class); EventBus eventBus = new SimpleEventBus(); rf.initialize(eventBus); - SimpleFooProxy simpleFoo = rf.testFooPolymorphicRequest().create( - SimpleFooProxy.class); + SimpleFooProxy simpleFoo = rf.testGenericRequest().create(SimpleFooProxy.class); assertNull(simpleFoo.getUserName()); } + + public void testRetrieval() { + delayTestFinish(TEST_DELAY); + Context ctx = factory.ctx(); + ctx.AasA().to(CastAndCheckReceiver.of(AProxy.class)); + ctx.BasA().to(CastAndCheckReceiver.of(B1Proxy.class)); + ctx.BasB().to(CastAndCheckReceiver.of(B1Proxy.class)); + ctx.CasA().to(CastAndCheckReceiver.of(C1Proxy.class)); + ctx.CasB().to(CastAndCheckReceiver.of(C1Proxy.class)); + ctx.DasA().to(CastAndCheckReceiver.of(MoreDerivedProxy.class)); + ctx.DasD().to(CastAndCheckReceiver.of(MoreDerivedProxy.class)); + ctx.W().to(CastAndCheckReceiver.of(WProxy.class)); + ctx.W2().to(CastAndCheckReceiver.of(WZProxy.class)); + ctx.Z().to(CastAndCheckReceiver.of(ZProxy.class)); + ctx.Z2().to(CastAndCheckReceiver.of(ZWProxy.class)); + ctx.fire(new Receiver<Void>() { + @Override + public void onSuccess(Void response) { + finishTest(); + } + }); + } + + public void testRetrievalCollection() { + delayTestFinish(TEST_DELAY); + Context ctx = factory.ctx(); + ctx.testCollection().to(new ListChecker()); + ctx.testCollectionSub().to(new ListChecker()); + ctx.fire(new Receiver<Void>() { + @Override + public void onSuccess(Void response) { + finishTest(); + } + }); + } + + protected Factory createFactory() { + Factory f = GWT.create(Factory.class); + f.initialize(new SimpleEventBus()); + return f; + } + + @Override + protected void gwtSetUp() throws Exception { + factory = createFactory(); + } + + private void checkA(Context ctx, Class<? extends AProxy> clazz) { + ctx.checkA(create(ctx, clazz)).to(checkReceiver); + } + + private void checkB(Context ctx, Class<? extends EntityProxy> clazz) { + ctx.checkB(create(ctx, clazz)).to(checkReceiver); + } + + private void checkC(Context ctx, Class<? extends EntityProxy> clazz) { + ctx.checkC(create(ctx, clazz)).to(checkReceiver); + } + + private void checkD(Context ctx, Class<? extends EntityProxy> clazz) { + ctx.checkD(create(ctx, clazz)).to(checkReceiver); + } + + private void checkW(Context ctx, Class<? extends EntityProxy> clazz) { + ctx.checkW(create(ctx, clazz)).to(checkReceiver); + } + + private void checkZ(Context ctx, Class<? extends EntityProxy> clazz) { + ctx.checkZ(create(ctx, clazz)).to(checkReceiver); + } + + private <T extends EntityProxy> T create(Context ctx, Class<T> clazz) { + T obj = ctx.create(clazz); + if (obj instanceof AProxy) { + ((AProxy) obj).setA("A"); + } + if (obj instanceof B1Proxy) { + ((B1Proxy) obj).setB("B"); + } + if (obj instanceof B2Proxy) { + ((B2Proxy) obj).setB("B"); + } + if (obj instanceof C1Proxy) { + ((C1Proxy) obj).setC("C"); + } + if (obj instanceof C2Proxy) { + ((C2Proxy) obj).setC("C"); + } + if (obj instanceof D1Proxy) { + ((D1Proxy) obj).setD("D"); + } + if (obj instanceof D2Proxy) { + ((D2Proxy) obj).setD("D"); + } + if (obj instanceof D3Proxy) { + ((D3Proxy) obj).setD("D"); + } + if (obj instanceof WProxy) { + ((WProxy) obj).setW("W"); + } + if (obj instanceof ZProxy) { + ((ZProxy) obj).setZ("Z"); + } + if (obj instanceof WZProxy) { + ((WZProxy) obj).setW("W"); + } + if (obj instanceof ZWProxy) { + ((ZWProxy) obj).setZ("Z"); + } + return obj; + } }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java new file mode 100644 index 0000000..6dc30c5 --- /dev/null +++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
@@ -0,0 +1,137 @@ +/* + * Copyright 2011 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.requestfactory.gwt.client.RequestFactoryPolymorphicTest; +import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.ClassLoaderLoader; +import com.google.web.bindery.requestfactory.shared.BaseProxy; + +import java.util.logging.Logger; + +/** + * A JRE version of {@link RequestFactoryPolymorphicTest} that includes + * additional tests for the RequestFactoryInterfaceValidator that are specific + * to type-hierarchy mapping. + */ +public class RequestFactoryPolymorphicJreTest extends RequestFactoryPolymorphicTest { + RequestFactoryInterfaceValidator v; + + @Override + public String getModuleName() { + return null; + } + + /** + * Disabling, since this is a test of the code Generator. + */ + @Override + public void testGenericRequest() { + } + + /** + * Check related proxy types with unrelated domain types. + * */ + public void testUnrelatedDomainTypes() { + // Simple mappings + check(v, W.class, WProxy.class, WProxy.class); + check(v, W.class, WZProxy.class, WZProxy.class); + check(v, Z.class, ZProxy.class, ZProxy.class); + check(v, Z.class, ZWProxy.class, ZWProxy.class); + + // Look for derived proxy types that map to the domain type + check(v, Z.class, WProxy.class, ZWProxy.class); + check(v, W.class, ZProxy.class, WZProxy.class); + + /* + * This test is included to verify that the validator will fail gracefully + * when asked for a nonsensical assignment. For these two tests, the + * requested proxy type isn't mapped to the domain type, nor are there any + * *sub*-types of the requested proxy class that are assignable to the + * domaintype. The requested proxy type's supertype is assignable to the + * domain type, however the supertype isn't assignable to its subtype, so + * it's not a valid choice. Normally, the RequestFactoryInterfaceValidator + * would detect the mismatch between the RequestContext and the service + * method return types, so this shouldn't be a problem in practice. It would + * only ever crop up when the SkipInterfaceValidation annotation has been + * used. + */ + check(v, Z.class, WZProxy.class, null); + check(v, W.class, ZWProxy.class, null); + } + + /** + * Tests that the RequestFactoryInterfaceValidator is producing the correct + * mappings from domain types back to client types. + */ + public void testValidator() { + /* + * Check explicit mappings. Not all of the types are directly referenced in + * the method declarations, so this also tests the ExtraTypes annotation. + */ + check(v, A.class, AProxy.class, AProxy.class); + check(v, B.class, B1Proxy.class, B1Proxy.class); + check(v, B.class, B2Proxy.class, B2Proxy.class); + check(v, C.class, C1Proxy.class, C1Proxy.class); + check(v, C.class, C2Proxy.class, C2Proxy.class); + + // Check types without explicit mappings. + check(v, ASub.class, AProxy.class, AProxy.class); + check(v, BSub.class, B1Proxy.class, B1Proxy.class); + check(v, BSub.class, B2Proxy.class, B2Proxy.class); + check(v, CSub.class, C1Proxy.class, C1Proxy.class); + check(v, CSub.class, C2Proxy.class, C2Proxy.class); + + // Check assignments with proxies extending proxies + check(v, C.class, B1Proxy.class, C1Proxy.class); + check(v, C.class, B2Proxy.class, C2Proxy.class); + + // Should prefer more-derived interfaces when possible + check(v, D.class, AProxy.class, MoreDerivedProxy.class); + check(v, D.class, D1Proxy.class, MoreDerivedProxy.class); + check(v, D.class, D2Proxy.class, MoreDerivedProxy.class); + check(v, D.class, D3Proxy.class, MoreDerivedProxy.class); + check(v, D.class, MoreDerivedProxy.class, MoreDerivedProxy.class); + check(v, DSub.class, AProxy.class, MoreDerivedProxy.class); + check(v, DSub.class, D1Proxy.class, MoreDerivedProxy.class); + check(v, DSub.class, D2Proxy.class, MoreDerivedProxy.class); + check(v, DSub.class, D3Proxy.class, MoreDerivedProxy.class); + check(v, DSub.class, MoreDerivedProxy.class, MoreDerivedProxy.class); + } + + @Override + protected Factory createFactory() { + return RequestFactoryJreTest.createInProcess(Factory.class); + } + + @Override + protected void gwtSetUp() throws Exception { + super.gwtSetUp(); + Logger logger = Logger.getLogger(getClass().getName()); + ClassLoaderLoader loader = new ClassLoaderLoader(getClass().getClassLoader()); + v = new RequestFactoryInterfaceValidator(logger, loader); + v.validateRequestFactory(Factory.class.getName()); + } + + private void check(RequestFactoryInterfaceValidator v, Class<?> domainType, + Class<? extends BaseProxy> declaredReturnType, Class<? extends BaseProxy> expectedClientType) { + String type = v.getEntityProxyTypeName(domainType.getName(), declaredReturnType.getName()); + if (expectedClientType == null) { + assertNull(type, type); + } else { + assertEquals(expectedClientType.getName(), type); + } + } +}
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java b/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java index 6ced0f6..8e4aefd 100644 --- a/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java +++ b/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java
@@ -16,8 +16,10 @@ package com.google.web.bindery.requestfactory.shared; /** - * Creates TestFooPolymorphicRequest. + * Creates TestFooPolymorphicRequest which verifies that the + * RequestFactoryGenerator can handle interface declarations that use generic + * type parameters. */ public interface TestRequestFactory extends RequestFactory { - TestFooPolymorphicRequest testFooPolymorphicRequest(); + TestFooPolymorphicRequest testGenericRequest(); }
diff --git a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java index d714c79..f1193a7 100644 --- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java +++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -23,6 +23,7 @@ import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest; import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest; import com.google.web.bindery.requestfactory.server.RequestFactoryJreTest; +import com.google.web.bindery.requestfactory.server.RequestFactoryPolymorphicJreTest; import com.google.web.bindery.requestfactory.server.RequestFactoryUnicodeEscapingJreTest; import com.google.web.bindery.requestfactory.server.ServiceInheritanceJreTest; import com.google.web.bindery.requestfactory.server.ServiceLocatorTest; @@ -47,6 +48,7 @@ suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class); suite.addTestSuite(RequestFactoryInterfaceValidatorTest.class); suite.addTestSuite(RequestFactoryJreTest.class); + suite.addTestSuite(RequestFactoryPolymorphicJreTest.class); suite.addTestSuite(RequestFactoryUnicodeEscapingJreTest.class); suite.addTestSuite(ServiceInheritanceJreTest.class); suite.addTestSuite(ServiceLocatorTest.class);