| /* |
| * 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; |
| |
| 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.user.rebind.ClassSourceFileComposerFactory; |
| import com.google.gwt.user.rebind.SourceWriter; |
| import com.google.web.bindery.autobean.gwt.rebind.model.JBeanMethod; |
| 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.shared.AutoBeanFactory.Category; |
| import com.google.web.bindery.autobean.shared.AutoBeanFactory.NoWrap; |
| import com.google.web.bindery.autobean.shared.impl.EnumMap.ExtraEnums; |
| import com.google.web.bindery.requestfactory.gwt.client.impl.AbstractClientRequestFactory; |
| import com.google.web.bindery.requestfactory.gwt.rebind.model.AcceptsModelVisitor; |
| 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; |
| import com.google.web.bindery.requestfactory.shared.BaseProxy; |
| import com.google.web.bindery.requestfactory.shared.EntityProxyId; |
| import com.google.web.bindery.requestfactory.shared.JsonRpcContent; |
| 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.AbstractRequestContext.Dialect; |
| import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestFactory; |
| import com.google.web.bindery.requestfactory.shared.impl.BaseProxyCategory; |
| import com.google.web.bindery.requestfactory.shared.impl.EntityProxyCategory; |
| import com.google.web.bindery.requestfactory.shared.impl.RequestData; |
| import com.google.web.bindery.requestfactory.shared.impl.ValueProxyCategory; |
| import com.google.web.bindery.requestfactory.vm.impl.OperationKey; |
| |
| 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.web.bindery.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; |
| } |
| |
| @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(); |
| 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); |
| } |
| |
| void visitExtraTypes(HasExtraTypes x) { |
| if (x.getExtraTypes() != null) { |
| for (EntityProxyModel extra : x.getExtraTypes()) { |
| extra.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); |
| } else { |
| JParameterizedType parameterized = type.isParameterized(); |
| if (parameterized != null) { |
| for (JClassType arg : parameterized.getTypeArgs()) { |
| maybeVisit(arg); |
| } |
| } |
| } |
| } |
| }); |
| 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); |
| models.addAll(x.getSuperProxyTypes()); |
| } |
| }); |
| 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<FooProxy, Return> implements Request<Return> { |
| sw.println("class X extends %s<%s, %s> implements %s {", |
| AbstractRequest.class.getCanonicalName(), |
| request.getInstanceType() == null ? BaseProxy.class.getCanonicalName() : |
| request.getInstanceType().getQualifiedSourceName(), |
| 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()); |
| String elementType = |
| request.isCollectionType() ? request.getCollectionElementType() |
| .getQualifiedSourceName() |
| + ".class" : "null"; |
| String returnTypeBaseQualifiedName = |
| ModelUtils.ensureBaseType(request.getDataType()).getQualifiedSourceName(); |
| // return new RequestData("ABC123", {parameters}, propertyRefs, |
| // List.class, FooProxy.class); |
| 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);", OperationKey.hash(type |
| .getQualifiedBinaryName()), type.getQualifiedSourceName()); |
| // typesToTokens.put(Foo.class, Foo); |
| sw.println("typesToTokens.put(%s.class, \"%s\");", type.getQualifiedSourceName(), |
| OperationKey.hash(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 public String getFactoryTypeToken() {"); |
| sw.indentln("return \"%s\";", model.getFactoryType().getQualifiedBinaryName()); |
| sw.println("}"); |
| 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("}"); |
| } |
| } |