com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator.java

Source

/*
 * 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.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);
                }
            }
        });
        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<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());
                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("}");
    }
}