blob: 5c1d1091cea60fcc73716938a0c90a73d721e005 [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.gwt.requestfactory.rebind;
import com.google.gwt.autobean.rebind.model.JBeanMethod;
import com.google.gwt.autobean.shared.AutoBean;
import com.google.gwt.autobean.shared.AutoBean.PropertyName;
import com.google.gwt.autobean.shared.AutoBeanFactory;
import com.google.gwt.autobean.shared.AutoBeanFactory.Category;
import com.google.gwt.autobean.shared.AutoBeanFactory.NoWrap;
import com.google.gwt.autobean.shared.impl.EnumMap.ExtraEnums;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
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.JEnumType;
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.JTypeParameter;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.editor.rebind.model.ModelUtils;
import com.google.gwt.requestfactory.client.impl.AbstractClientRequestFactory;
import com.google.gwt.requestfactory.rebind.model.AcceptsModelVisitor;
import com.google.gwt.requestfactory.rebind.model.ContextMethod;
import com.google.gwt.requestfactory.rebind.model.EntityProxyModel;
import com.google.gwt.requestfactory.rebind.model.EntityProxyModel.Type;
import com.google.gwt.requestfactory.rebind.model.ModelVisitor;
import com.google.gwt.requestfactory.rebind.model.RequestFactoryModel;
import com.google.gwt.requestfactory.rebind.model.RequestMethod;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.JsonRpcContent;
import com.google.gwt.requestfactory.shared.impl.AbstractRequest;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory;
import com.google.gwt.requestfactory.shared.impl.RequestData;
import com.google.gwt.requestfactory.shared.impl.ValueProxyCategory;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Generates implementations of
* {@link com.google.gwt.requestfactory.shared.RequestFactory RequestFactory}
* and its nested interfaces.
*/
public class RequestFactoryGenerator extends Generator {
/**
* Visits all types reachable from a RequestContext.
*/
private static class AllReachableTypesVisitor extends
RequestMethodTypesVisitor {
private final RequestFactoryModel model;
public AllReachableTypesVisitor(RequestFactoryModel model) {
this.model = model;
}
void examineTypeOnce(JClassType type) {
// Need this to handle List<Foo>, Map<Foo>
JParameterizedType parameterized = type.isParameterized();
if (parameterized != null) {
for (JClassType arg : parameterized.getTypeArgs()) {
maybeVisit(arg);
}
}
JClassType base = ModelUtils.ensureBaseType(type);
EntityProxyModel peer = model.getPeer(base);
if (peer == null) {
return;
}
peer.accept(this);
}
}
/**
* Visits all types immediately referenced by methods defined in a
* RequestContext.
*/
private abstract static class RequestMethodTypesVisitor extends ModelVisitor {
private final Set<JClassType> seen = new HashSet<JClassType>();
@Override
public void endVisit(RequestMethod x) {
// Request<Foo> -> Foo
maybeVisit(x.getDataType());
// InstanceRequest<Proxy, Foo> -> Proxy
if (x.getInstanceType() != null) {
x.getInstanceType().accept(this);
}
// Request<Void> doSomething(Foo foo, Bar bar) -> Foo, Bar
for (JType param : x.getDeclarationMethod().getParameterTypes()) {
maybeVisit(param.isClassOrInterface());
}
// setFoo(Foo foo) -> Foo
for (JMethod method : x.getExtraSetters()) {
maybeVisit(method.getParameterTypes()[0].isClassOrInterface());
}
}
abstract void examineTypeOnce(JClassType type);
void maybeVisit(JClassType type) {
if (type == null) {
return;
} else if (!seen.add(type)) {
// Short-circuit to prevent type-loops
return;
}
examineTypeOnce(type);
}
}
private GeneratorContext context;
private TreeLogger logger;
private RequestFactoryModel model;
@Override
public String generate(TreeLogger logger, GeneratorContext context,
String typeName) throws UnableToCompleteException {
this.context = context;
this.logger = logger;
TypeOracle oracle = context.getTypeOracle();
JClassType toGenerate = oracle.findType(typeName).isInterface();
if (toGenerate == null) {
logger.log(TreeLogger.ERROR, typeName + " is not an interface type");
throw new UnableToCompleteException();
}
String packageName = toGenerate.getPackage().getName();
String simpleSourceName = toGenerate.getName().replace('.', '_') + "Impl";
PrintWriter pw = context.tryCreate(logger, packageName, simpleSourceName);
if (pw == null) {
return packageName + "." + simpleSourceName;
}
model = new RequestFactoryModel(logger, toGenerate);
ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
packageName, simpleSourceName);
factory.setSuperclass(AbstractClientRequestFactory.class.getCanonicalName());
factory.addImplementedInterface(typeName);
SourceWriter sw = factory.createSourceWriter(context, pw);
writeAutoBeanFactory(sw, model.getAllProxyModels(), findExtraEnums(model));
writeContextMethods(sw);
writeContextImplementations();
writeTypeMap(sw);
sw.commit(logger);
return factory.getCreatedClassName();
}
/**
* Find enums that needed to be added to the EnumMap that are not referenced
* by any of the proxies. This is necessary because the RequestFactory depends
* on the AutoBeanCodex to serialize enum values, which in turn depends on the
* AutoBeanFactory's enum map. That enum map only contains enum types
* reachable from the AutoBean interfaces, which could lead to method
* parameters being un-encodable.
*/
private Set<JEnumType> findExtraEnums(AcceptsModelVisitor method) {
final Set<JEnumType> toReturn = new LinkedHashSet<JEnumType>();
final Set<JEnumType> referenced = new HashSet<JEnumType>();
// Called from the adder visitor below on each EntityProxy seen
final ModelVisitor remover = new AllReachableTypesVisitor(model) {
@Override
void examineTypeOnce(JClassType type) {
JEnumType asEnum = type.isEnum();
if (asEnum != null) {
referenced.add(asEnum);
}
super.examineTypeOnce(type);
}
};
// Add enums used by RequestMethods
method.accept(new RequestMethodTypesVisitor() {
@Override
public boolean visit(EntityProxyModel x) {
x.accept(remover);
return false;
}
@Override
void examineTypeOnce(JClassType type) {
JEnumType asEnum = type.isEnum();
if (asEnum != null) {
toReturn.add(asEnum);
}
}
});
toReturn.removeAll(referenced);
if (toReturn.isEmpty()) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(toReturn);
}
/**
* Find all EntityProxyModels reachable from a given ContextMethod.
*/
private Set<EntityProxyModel> findReferencedEntities(ContextMethod method) {
final Set<EntityProxyModel> models = new LinkedHashSet<EntityProxyModel>();
method.accept(new AllReachableTypesVisitor(model) {
@Override
public void endVisit(EntityProxyModel x) {
models.add(x);
}
});
return models;
}
private void writeAutoBeanFactory(SourceWriter sw,
Collection<EntityProxyModel> models, Collection<JEnumType> extraEnums) {
if (!extraEnums.isEmpty()) {
StringBuilder extraClasses = new StringBuilder();
for (JEnumType enumType : extraEnums) {
if (extraClasses.length() > 0) {
extraClasses.append(",");
}
extraClasses.append(enumType.getQualifiedSourceName()).append(".class");
}
sw.println("@%s({%s})", ExtraEnums.class.getCanonicalName(), extraClasses);
}
// Map in static implementations of EntityProxy methods
sw.println("@%s({%s.class, %s.class, %s.class})",
Category.class.getCanonicalName(),
EntityProxyCategory.class.getCanonicalName(),
ValueProxyCategory.class.getCanonicalName(),
BaseProxyCategory.class.getCanonicalName());
// Don't wrap our id type, because it makes code grungy
sw.println("@%s(%s.class)", NoWrap.class.getCanonicalName(),
EntityProxyId.class.getCanonicalName());
sw.println("interface Factory extends %s {",
AutoBeanFactory.class.getCanonicalName());
sw.indent();
for (EntityProxyModel proxy : models) {
// AutoBean<FooProxy> com_google_FooProxy();
sw.println("%s<%s> %s();", AutoBean.class.getCanonicalName(),
proxy.getQualifiedSourceName(),
proxy.getQualifiedSourceName().replace('.', '_'));
}
sw.outdent();
sw.println("}");
// public static final Factory FACTORY = GWT.create(Factory.class);
sw.println("public static Factory FACTORY;", GWT.class.getCanonicalName());
// Write public accessor
sw.println("@Override public Factory getAutoBeanFactory() {");
sw.indent();
sw.println("if (FACTORY == null) {");
sw.indentln("FACTORY = %s.create(Factory.class);",
GWT.class.getCanonicalName());
sw.println("}");
sw.println("return FACTORY;");
sw.outdent();
sw.println("}");
}
private void writeContextImplementations() {
for (ContextMethod method : model.getMethods()) {
PrintWriter pw = context.tryCreate(logger, method.getPackageName(),
method.getSimpleSourceName());
if (pw == null) {
// Already generated
continue;
}
ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
method.getPackageName(), method.getSimpleSourceName());
factory.setSuperclass(AbstractRequestContext.class.getCanonicalName());
factory.addImplementedInterface(method.getImplementedInterfaceQualifiedSourceName());
SourceWriter sw = factory.createSourceWriter(context, pw);
// Constructor that accepts the parent RequestFactory
sw.println(
"public %s(%s requestFactory) {super(requestFactory, %s.%s);}",
method.getSimpleSourceName(),
AbstractRequestFactory.class.getCanonicalName(),
Dialect.class.getCanonicalName(), method.getDialect().name());
Set<EntityProxyModel> models = findReferencedEntities(method);
Set<JEnumType> extraEnumTypes = findExtraEnums(method);
writeAutoBeanFactory(sw, models, extraEnumTypes);
// Write each Request method
for (RequestMethod request : method.getRequestMethods()) {
JMethod jmethod = request.getDeclarationMethod();
String operation = request.getOperation();
// foo, bar, baz
StringBuilder parameterArray = new StringBuilder();
// final Foo foo, final Bar bar, final Baz baz
StringBuilder parameterDeclaration = new StringBuilder();
// <P extends Blah>
StringBuilder typeParameterDeclaration = new StringBuilder();
if (request.isInstance()) {
// Leave a spot for the using() method to fill in later
parameterArray.append(",null");
}
for (JTypeParameter param : jmethod.getTypeParameters()) {
typeParameterDeclaration.append(",").append(
param.getQualifiedSourceName());
}
for (JParameter param : jmethod.getParameters()) {
parameterArray.append(",").append(param.getName());
parameterDeclaration.append(",final ").append(
param.getType().getParameterizedQualifiedSourceName()).append(" ").append(
param.getName());
}
if (parameterArray.length() > 0) {
parameterArray.deleteCharAt(0);
}
if (parameterDeclaration.length() > 0) {
parameterDeclaration.deleteCharAt(0);
}
if (typeParameterDeclaration.length() > 0) {
typeParameterDeclaration.deleteCharAt(0).insert(0, "<").append(">");
}
// public Request<Foo> doFoo(final Foo foo) {
sw.println("public %s %s %s(%s) {", typeParameterDeclaration,
jmethod.getReturnType().getParameterizedQualifiedSourceName(),
jmethod.getName(), parameterDeclaration);
sw.indent();
// The implements clause covers InstanceRequest
// class X extends AbstractRequest<Return> implements Request<Return> {
sw.println("class X extends %s<%s> implements %s {",
AbstractRequest.class.getCanonicalName(),
request.getDataType().getParameterizedQualifiedSourceName(),
jmethod.getReturnType().getParameterizedQualifiedSourceName());
sw.indent();
// public X() { super(FooRequestContext.this); }
sw.println("public X() { super(%s.this);}",
method.getSimpleSourceName());
// This could also be gotten rid of by having only Request /
// InstanceRequest
sw.println("@Override public X with(String... paths) {super.with(paths); return this;}");
// makeRequestData()
sw.println("@Override protected %s makeRequestData() {",
RequestData.class.getCanonicalName());
// return new RequestData("Foo::bar", {parameters}, propertyRefs,
// List.class, FooProxy.class);
String elementType = request.isCollectionType()
? request.getCollectionElementType().getQualifiedSourceName()
+ ".class" : "null";
String returnTypeBaseQualifiedName = ModelUtils.ensureBaseType(
request.getDataType()).getQualifiedSourceName();
sw.indentln(
"return new %s(\"%s\", new Object[] {%s}, propertyRefs, %s.class, %s);",
RequestData.class.getCanonicalName(), operation, parameterArray,
returnTypeBaseQualifiedName, elementType);
sw.println("}");
/*
* Only support extra properties in JSON-RPC payloads. Could add this to
* standard requests to provide out-of-band data.
*/
if (method.getDialect().equals(Dialect.JSON_RPC)) {
for (JMethod setter : request.getExtraSetters()) {
PropertyName propertyNameAnnotation = setter.getAnnotation(PropertyName.class);
String propertyName = propertyNameAnnotation == null
? JBeanMethod.SET.inferName(setter)
: propertyNameAnnotation.value();
String maybeReturn = JBeanMethod.SET_BUILDER.matches(setter)
? "return this;" : "";
sw.println(
"%s { getRequestData().setNamedParameter(\"%s\", %s); %s}",
setter.getReadableDeclaration(false, false, false, false, true),
propertyName, setter.getParameters()[0].getName(), maybeReturn);
}
}
// end class X{}
sw.outdent();
sw.println("}");
// Instantiate, enqueue, and return
sw.println("X x = new X();");
if (request.getApiVersion() != null) {
sw.println("x.getRequestData().setApiVersion(\"%s\");",
Generator.escape(request.getApiVersion()));
}
// JSON-RPC payloads send their parameters in a by-name fashion
if (method.getDialect().equals(Dialect.JSON_RPC)) {
for (JParameter param : jmethod.getParameters()) {
PropertyName annotation = param.getAnnotation(PropertyName.class);
String propertyName = annotation == null ? param.getName()
: annotation.value();
boolean isContent = param.isAnnotationPresent(JsonRpcContent.class);
if (isContent) {
sw.println("x.getRequestData().setRequestContent(%s);",
param.getName());
} else {
sw.println("x.getRequestData().setNamedParameter(\"%s\", %s);",
propertyName, param.getName());
}
}
}
// See comment in AbstractRequest.using(EntityProxy)
if (!request.isInstance()) {
sw.println("addInvocation(x);");
}
sw.println("return x;");
sw.outdent();
sw.println("}");
}
sw.commit(logger);
}
}
private void writeContextMethods(SourceWriter sw) {
for (ContextMethod method : model.getMethods()) {
// public FooService foo() {
sw.println("public %s %s() {", method.getQualifiedSourceName(),
method.getMethodName());
// return new FooServiceImpl(this);
sw.indentln("return new %s(this);", method.getQualifiedSourceName());
sw.println("}");
}
}
private void writeTypeMap(SourceWriter sw) {
sw.println("private static final %1$s<String, Class<?>> tokensToTypes"
+ " = new %1$s<String, Class<?>>();", HashMap.class.getCanonicalName());
sw.println("private static final %1$s<Class<?>, String> typesToTokens"
+ " = new %1$s<Class<?>, String>();", HashMap.class.getCanonicalName());
sw.println(
"private static final %1$s<Class<?>> entityProxyTypes = new %1$s<Class<?>>();",
HashSet.class.getCanonicalName());
sw.println(
"private static final %1$s<Class<?>> valueProxyTypes = new %1$s<Class<?>>();",
HashSet.class.getCanonicalName());
sw.println("static {");
sw.indent();
for (EntityProxyModel type : model.getAllProxyModels()) {
// tokensToTypes.put("Foo", Foo.class);
sw.println("tokensToTypes.put(\"%s\", %s.class);",
type.getQualifiedBinaryName(), type.getQualifiedSourceName());
// typesToTokens.put(Foo.class, Foo);
sw.println("typesToTokens.put(%s.class, \"%s\");",
type.getQualifiedSourceName(), type.getQualifiedBinaryName());
// fooProxyTypes.add(MyFooProxy.class);
sw.println("%s.add(%s.class);", type.getType().equals(Type.ENTITY)
? "entityProxyTypes" : "valueProxyTypes",
type.getQualifiedSourceName());
}
sw.outdent();
sw.println("}");
// Write instance methods
sw.println("@Override protected Class getTypeFromToken(String typeToken) {");
sw.indentln("return tokensToTypes.get(typeToken);");
sw.println("}");
sw.println("@Override protected String getTypeToken(Class type) {");
sw.indentln("return typesToTokens.get(type);");
sw.println("}");
sw.println("@Override public boolean isEntityType(Class<?> type) {");
sw.indentln("return entityProxyTypes.contains(type);");
sw.println("}");
sw.println("@Override public boolean isValueType(Class<?> type) {");
sw.indentln("return valueProxyTypes.contains(type);");
sw.println("}");
}
}