blob: 3f08de3bf4a42ac4fa918bb0509d20471b7c3e55 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.web.bindery.requestfactory.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;
}
}