All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory Maven / Gradle / Ivy

Go to download

Vaadin is a web application framework for Rich Internet Applications (RIA). Vaadin enables easy development and maintenance of fast and secure rich web applications with a stunning look and feel and a wide browser support. It features a server-side architecture with the majority of the logic running on the server. Ajax technology is used at the browser-side to ensure a rich and interactive user experience.

There is a newer version: 8.27.1
Show newest version
/*
 * Copyright (C) 2000-2023 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.server.widgetsetutils;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
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.TreeLogger.Type;
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.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.communication.JsonDecoder;
import com.vaadin.client.metadata.ConnectorBundleLoader;
import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo;
import com.vaadin.client.metadata.InvokationHandler;
import com.vaadin.client.metadata.OnStateChangeMethod;
import com.vaadin.client.metadata.ProxyHandler;
import com.vaadin.client.metadata.TypeData;
import com.vaadin.client.metadata.TypeDataStore;
import com.vaadin.client.metadata.TypeDataStore.MethodAttribute;
import com.vaadin.client.ui.UnknownComponentConnector;
import com.vaadin.client.ui.UnknownExtensionConnector;
import com.vaadin.server.widgetsetutils.metadata.ClientRpcVisitor;
import com.vaadin.server.widgetsetutils.metadata.ConnectorBundle;
import com.vaadin.server.widgetsetutils.metadata.ConnectorInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.GeneratedSerializer;
import com.vaadin.server.widgetsetutils.metadata.OnStateChangeVisitor;
import com.vaadin.server.widgetsetutils.metadata.Property;
import com.vaadin.server.widgetsetutils.metadata.RendererVisitor;
import com.vaadin.server.widgetsetutils.metadata.ServerRpcVisitor;
import com.vaadin.server.widgetsetutils.metadata.StateInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.TypeVisitor;
import com.vaadin.server.widgetsetutils.metadata.WidgetInitVisitor;
import com.vaadin.shared.annotations.DelegateToWidget;
import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.Connect.LoadStyle;
import com.vaadin.tools.CvalAddonsChecker;
import com.vaadin.tools.CvalChecker;
import com.vaadin.tools.CvalChecker.InvalidCvalException;

public class ConnectorBundleLoaderFactory extends Generator {
    /**
     * Special SourceWriter that approximates the number of written bytes to
     * support splitting long methods into shorter chunks to avoid hitting the
     * 65535 byte limit.
     */
    private class SplittingSourceWriter implements SourceWriter {
        private final SourceWriter target;
        private final String baseName;
        private final int splitSize;
        private final List methodNames;

        // Seems to be undercounted by about 15%
        private int approximateChars = 0;
        private int wrapCount = 0;

        public SplittingSourceWriter(SourceWriter target, String baseName,
                int splitSize) {
            this.target = target;
            this.baseName = baseName;
            this.splitSize = splitSize;
            methodNames = new ArrayList();
            methodNames.add(baseName);
        }

        @Override
        public void beginJavaDocComment() {
            target.beginJavaDocComment();
            addChars(10);
        }

        private void addChars(int i) {
            approximateChars += i;
        }

        private void addChars(String s) {
            addChars(s.length());
        }

        private void addChars(String s, Object[] args) {
            addChars(String.format(s, args));
        }

        @Override
        public void commit(TreeLogger logger) {
            target.commit(logger);
        }

        @Override
        public void endJavaDocComment() {
            target.endJavaDocComment();
            addChars(10);
        }

        @Override
        public void indent() {
            target.indent();
            addChars(10);
        }

        @Override
        public void indentln(String s) {
            target.indentln(s);
            addChars(s);
        }

        @Override
        public void indentln(String s, Object... args) {
            target.indentln(s, args);
            addChars(s, args);
        }

        @Override
        public void outdent() {
            target.outdent();
        }

        @Override
        public void print(String s) {
            target.print(s);
            addChars(s);
        }

        @Override
        public void print(String s, Object... args) {
            target.print(s, args);
            addChars(s, args);
        }

        @Override
        public void println() {
            target.println();
            addChars(5);
        }

        @Override
        public void println(String s) {
            target.println(s);
            addChars(s);
        }

        @Override
        public void println(String s, Object... args) {
            target.println(s, args);
            addChars(s, args);
        }

        public void splitIfNeeded() {
            splitIfNeeded(false, null);
        }

        public void splitIfNeeded(boolean isNative, String params) {
            if (approximateChars > splitSize) {
                String newMethod = baseName + wrapCount++;
                String args = params == null ? "" : params;
                if (isNative) {
                    outdent();
                    println("}-*/;");
                    // To support fields of type long (#13692)
                    println("@com.google.gwt.core.client.UnsafeNativeLong");
                    println("private native void %s(%s) /*-{", newMethod, args);
                } else {
                    println("%s();", newMethod);
                    outdent();
                    println("}");
                    println("private void %s(%s) {", newMethod, args);
                }
                methodNames.add(newMethod);
                indent();

                approximateChars = 0;
            }
        }

        public List getMethodNames() {
            return Collections.unmodifiableList(methodNames);
        }

    }

    private CvalAddonsChecker cvalChecker = new CvalAddonsChecker();

    @Override
    public String generate(TreeLogger logger, GeneratorContext context,
            String typeName) throws UnableToCompleteException {
        TypeOracle typeOracle = context.getTypeOracle();

        try {
            JClassType classType = typeOracle.getType(typeName);
            String packageName = classType.getPackage().getName();
            String className = classType.getSimpleSourceName() + "Impl";

            generateClass(logger, context, packageName, className, typeName);

            return packageName + "." + className;
        } catch (UnableToCompleteException e) {
            // Just rethrow
            throw e;
        } catch (Exception e) {
            logger.log(Type.ERROR, getClass() + " failed", e);
            throw new UnableToCompleteException();
        }
    }

    private void generateClass(TreeLogger logger, GeneratorContext context,
            String packageName, String className, String requestedType)
            throws Exception {
        PrintWriter printWriter = context.tryCreate(logger, packageName,
                className);
        if (printWriter == null) {
            return;
        }

        List cvalInfos = null;
        try {
            if (cvalChecker != null) {
                cvalInfos = cvalChecker.run();
                // Don't run twice
                cvalChecker = null;
            }
        } catch (InvalidCvalException e) {
            System.err.println("\n\n\n\n" + CvalChecker.LINE);
            for (String line : e.getMessage().split("\n")) {
                System.err.println(line);
            }
            System.err.println(CvalChecker.LINE + "\n\n\n\n");
            System.exit(1);
            throw new UnableToCompleteException();
        }

        List bundles = buildBundles(logger,
                context.getTypeOracle());

        ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
                packageName, className);
        composer.setSuperclass(requestedType);

        SourceWriter w = composer.createSourceWriter(context, printWriter);

        w.println("public void init() {");
        w.indent();

        for (ConnectorBundle bundle : bundles) {
            detectBadProperties(bundle, logger);

            String name = bundle.getName();
            boolean isEager = name
                    .equals(ConnectorBundleLoader.EAGER_BUNDLE_NAME);

            w.print("addAsyncBlockLoader(new AsyncBundleLoader(\"");
            w.print(escape(name));
            w.print("\", ");

            w.print("new String[] {");
            for (Entry> entry : bundle.getIdentifiers()
                    .entrySet()) {
                Set identifiers = entry.getValue();
                for (String id : identifiers) {
                    w.print("\"");
                    w.print(escape(id));
                    w.print("\",");
                }
            }
            w.println("}) {");
            w.indent();

            w.print("protected void load(final ");
            w.print(TypeDataStore.class.getName());
            w.println(" store) {");
            w.indent();

            if (!isEager) {
                w.print(GWT.class.getName());
                w.print(".runAsync(");
            }

            w.println("new %s() {", RunAsyncCallback.class.getName());
            w.indent();

            w.println("public void onSuccess() {");
            w.indent();

            w.println("load();");
            w.println("%s.get().setLoaded(getName());",
                    ConnectorBundleLoader.class.getName());

            // Close onSuccess method
            w.outdent();
            w.println("}");

            w.println("private void load() {");
            w.indent();

            String loadNativeJsBundle = "loadJsBundle";
            printBundleData(logger, w, bundle, loadNativeJsBundle);

            // Close load method
            w.outdent();
            w.println("}");

            // Separate method for loading native JS stuff (e.g. callbacks)
            String loadNativeJsMethodName = "loadNativeJs";
            // To support fields of type long (#13692)
            w.println("@com.google.gwt.core.client.UnsafeNativeLong");
            w.println("private native void %s(%s store) /*-{",
                    loadNativeJsMethodName, TypeDataStore.class.getName());
            w.indent();
            List jsMethodNames = printJsBundleData(logger, w, bundle,
                    loadNativeJsMethodName);

            w.outdent();
            w.println("}-*/;");

            // Call all generated native method inside one Java method to avoid
            // refercences inside native methods to each other
            w.println("private void %s(%s store) {", loadNativeJsBundle,
                    TypeDataStore.class.getName());
            w.indent();
            printLoadJsBundleData(w, loadNativeJsBundle, jsMethodNames);
            w.outdent();
            w.println("}");

            // onFailure method declaration starts
            w.println("public void onFailure(Throwable reason) {");
            w.indent();

            w.println("%s.get().setLoadFailure(getName(), reason);",
                    ConnectorBundleLoader.class.getName());

            w.outdent();
            w.println("}");

            // Close new RunAsyncCallback() {}
            w.outdent();
            w.print("}");

            if (isEager) {
                w.println(".onSuccess();");
            } else {
                w.println(");");
            }

            // Close load method
            w.outdent();
            w.println("}");

            // Close add(new ...
            w.outdent();
            w.println("});");
        }

        if (cvalInfos != null && !cvalInfos.isEmpty()) {
            w.println("{");
            for (CValUiInfo c : cvalInfos) {
                if ("evaluation".equals(c.type)) {
                    w.println("cvals.add(new CValUiInfo(\"" + c.product
                            + "\", \"" + c.version + "\", \"" + c.widgetset
                            + "\", null));");
                }
            }
            w.println("}");
        }

        w.outdent();
        w.println("}");

        w.commit(logger);
    }

    private void printLoadJsBundleData(SourceWriter w, String methodName,
            List methods) {
        SplittingSourceWriter writer = new SplittingSourceWriter(w, methodName,
                30000);

        for (String method : methods) {
            writer.println("%s(store);", method);
            writer.splitIfNeeded();
        }
    }

    private void detectBadProperties(ConnectorBundle bundle, TreeLogger logger)
            throws UnableToCompleteException {
        Map> definedProperties = new HashMap>();

        for (Property property : bundle.getNeedsProperty()) {
            JClassType beanType = property.getBeanType();
            Set usedPropertyNames = definedProperties.get(beanType);
            if (usedPropertyNames == null) {
                usedPropertyNames = new HashSet();
                definedProperties.put(beanType, usedPropertyNames);
            }

            String name = property.getName();
            if (!usedPropertyNames.add(name)) {
                logger.log(Type.ERROR, beanType.getQualifiedSourceName()
                        + " has multiple properties with the name " + name
                        + ". This can happen if there are multiple "
                        + "setters with identical names ignoring case.");
                throw new UnableToCompleteException();
            }
            if (!property.hasAccessorMethods()) {
                logger.log(Type.ERROR,
                        beanType.getQualifiedSourceName()
                                + " has the property '" + name
                                + "' without getter defined.");
                throw new UnableToCompleteException();
            }
        }
    }

    private List printJsBundleData(TreeLogger logger, SourceWriter w,
            ConnectorBundle bundle, String methodName) {
        SplittingSourceWriter writer = new SplittingSourceWriter(w, methodName,
                30000);
        Set needsProperty = bundle.getNeedsProperty();
        for (Property property : needsProperty) {
            writer.println("var data = {");
            writer.indent();

            if (property.getAnnotation(NoLayout.class) != null) {
                writer.println("noLayout: 1, ");
            }

            writer.println("setter: function(bean, value) {");
            writer.indent();
            property.writeSetterBody(logger, writer, "bean", "value");
            writer.outdent();
            writer.println("},");

            writer.println("getter: function(bean) {");
            writer.indent();
            property.writeGetterBody(logger, writer, "bean");
            writer.outdent();
            writer.println("}");

            writer.outdent();
            writer.println("};");

            // Method declaration
            writer.print(
                    "store.@%s::setPropertyData(Ljava/lang/Class;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)",
                    TypeDataStore.class.getName());
            writer.println("(@%s::class, '%s', data);",
                    property.getBeanType().getQualifiedSourceName(),
                    property.getName());
            writer.println();
            writer.splitIfNeeded(true,
                    String.format("%s store", TypeDataStore.class.getName()));
        }
        return writer.getMethodNames();
    }

    private void printBundleData(TreeLogger logger, SourceWriter sourceWriter,
            ConnectorBundle bundle, String loadNativeJsMethodName)
            throws UnableToCompleteException {
        // Split into new load method when reaching approximately 30000 bytes
        SplittingSourceWriter w = new SplittingSourceWriter(sourceWriter,
                "load", 30000);

        writeSuperClasses(w, bundle);
        writeIdentifiers(w, bundle);
        writeGwtConstructors(w, bundle);
        writeReturnTypes(w, bundle);
        writeInvokers(logger, w, bundle);
        writeParamTypes(w, bundle);
        writeProxys(w, bundle);
        writeMethodAttributes(logger, w, bundle);

        w.println("%s(store);", loadNativeJsMethodName);

        // Must use Java code to generate Type data (because of Type[]), doing
        // this after the JS property data has been initialized
        writePropertyTypes(logger, w, bundle);
        writeSerializers(logger, w, bundle);
        writePresentationTypes(w, bundle);
        writeDelegateToWidget(logger, w, bundle);
        writeOnStateChangeHandlers(logger, w, bundle);
    }

    private void writeOnStateChangeHandlers(TreeLogger logger,
            SplittingSourceWriter w, ConnectorBundle bundle)
            throws UnableToCompleteException {
        Map> needsOnStateChangeHandler = bundle
                .getNeedsOnStateChangeHandler();
        for (Entry> entry : needsOnStateChangeHandler
                .entrySet()) {
            JClassType connector = entry.getKey();

            TreeLogger typeLogger = logger.branch(Type.DEBUG,
                    "Generating @OnStateChange support for "
                            + connector.getName());

            // Build map to speed up error checking
            HashMap stateProperties = new HashMap();
            JClassType stateType = ConnectorBundle
                    .findInheritedMethod(connector, "getState").getReturnType()
                    .isClassOrInterface();
            for (Property property : bundle.getProperties(stateType)) {
                stateProperties.put(property.getName(), property);
            }

            for (JMethod method : entry.getValue()) {
                TreeLogger methodLogger = typeLogger.branch(Type.DEBUG,
                        "Processing method " + method.getName());

                if (method.isPublic() || method.isProtected()) {
                    methodLogger.log(Type.ERROR,
                            "@OnStateChange is only supported for methods with private or default visibility.");
                    throw new UnableToCompleteException();
                }

                OnStateChange onStateChange = method
                        .getAnnotation(OnStateChange.class);

                String[] properties = onStateChange.value();

                if (properties.length == 0) {
                    methodLogger.log(Type.ERROR,
                            "There are no properties to listen to");
                    throw new UnableToCompleteException();
                }

                // Verify that all properties do exist
                for (String propertyName : properties) {
                    if (!stateProperties.containsKey(propertyName)) {
                        methodLogger.log(Type.ERROR,
                                "State class has no property named "
                                        + propertyName);
                        throw new UnableToCompleteException();
                    }
                }

                if (method.getParameters().length != 0) {
                    methodLogger.log(Type.ERROR,
                            "Method should accept zero parameters");
                    throw new UnableToCompleteException();
                }

                // new OnStateChangeMethod(Class declaringClass, String
                // methodName, String[], properties)
                w.print("store.addOnStateChangeMethod(%s, new %s(",
                        getClassLiteralString(connector),
                        OnStateChangeMethod.class.getName());
                if (!connector.equals(method.getEnclosingType())) {
                    w.print("%s, ",
                            getClassLiteralString(method.getEnclosingType()));
                }
                w.print("\"%s\", ", method.getName());

                w.print("new String[] {");
                for (String propertyName : properties) {
                    w.print("\"%s\", ", propertyName);
                }
                w.print("}");

                w.println("));");

                w.splitIfNeeded();
            }
        }
    }

    private void writeSuperClasses(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        List needsSuperclass = new ArrayList(
                bundle.getNeedsSuperclass());
        // Emit in hierarchy order to ensure superclass is defined when
        // referenced
        Collections.sort(needsSuperclass, new Comparator() {

            @Override
            public int compare(JClassType type1, JClassType type2) {
                int depthDiff = getDepth(type1) - getDepth(type2);
                if (depthDiff != 0) {
                    return depthDiff;
                } else {
                    // Just something to get a stable compare
                    return type1.getName().compareTo(type2.getName());
                }
            }

            private int getDepth(JClassType type) {
                int depth = 0;
                while (type != null) {
                    depth++;
                    type = type.getSuperclass();
                }
                return depth;
            }
        });

        for (JClassType jClassType : needsSuperclass) {
            JClassType superclass = jClassType.getSuperclass();
            while (superclass != null && !superclass.isPublic()) {
                superclass = superclass.getSuperclass();
            }
            String classLiteralString;
            if (superclass == null) {
                classLiteralString = "null";
            } else {
                classLiteralString = getClassLiteralString(superclass);
            }
            w.println("store.setSuperClass(%s, %s);",
                    getClassLiteralString(jClassType), classLiteralString);
        }
    }

    private void writeDelegateToWidget(TreeLogger logger,
            SplittingSourceWriter w, ConnectorBundle bundle) {
        Map> needsDelegateToWidget = bundle
                .getNeedsDelegateToWidget();
        for (Entry> entry : needsDelegateToWidget
                .entrySet()) {
            JClassType beanType = entry.getKey();
            for (Property property : entry.getValue()) {
                w.println("store.setDelegateToWidget(%s, \"%s\", \"%s\");",
                        getClassLiteralString(beanType), // property.getBeanType()),
                        property.getName(),
                        property.getAnnotation(DelegateToWidget.class).value());
            }
            w.splitIfNeeded();
        }
    }

    private void writeSerializers(TreeLogger logger, SplittingSourceWriter w,
            ConnectorBundle bundle) throws UnableToCompleteException {
        Map serializers = bundle.getSerializers();
        for (Entry entry : serializers.entrySet()) {
            JType type = entry.getKey();
            GeneratedSerializer serializer = entry.getValue();

            w.print("store.setSerializerFactory(");
            writeClassLiteral(w, type);
            w.print(", ");
            w.println("new Invoker() {");
            w.indent();

            w.println("public Object invoke(Object target, Object[] params) {");
            w.indent();

            serializer.writeSerializerInstantiator(logger, w);

            w.outdent();
            w.println("}");

            w.outdent();
            w.print("}");
            w.println(");");

            w.splitIfNeeded();
        }
    }

    private void writePresentationTypes(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Map presentationTypes = bundle
                .getPresentationTypes();
        for (Entry entry : presentationTypes.entrySet()) {

            w.print("store.setPresentationType(");
            writeClassLiteral(w, entry.getKey());
            w.print(", ");
            writeClassLiteral(w, entry.getValue());
            w.println(");");
            w.splitIfNeeded();
        }
    }

    private void writePropertyTypes(TreeLogger logger, SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Set properties = bundle.getNeedsProperty();
        for (Property property : properties) {
            w.print("store.setPropertyType(");
            writeClassLiteral(w, property.getBeanType());
            w.print(", \"");
            w.print(escape(property.getName()));
            w.print("\", ");
            writeTypeCreator(w, property.getPropertyType());
            w.println(");");

            w.splitIfNeeded();
        }
    }

    private void writeMethodAttributes(TreeLogger logger,
            SplittingSourceWriter w, ConnectorBundle bundle) {
        for (Entry>> typeEntry : bundle
                .getMethodAttributes().entrySet()) {
            JClassType type = typeEntry.getKey();
            for (Entry> methodEntry : typeEntry
                    .getValue().entrySet()) {
                JMethod method = methodEntry.getKey();
                Set attributes = methodEntry.getValue();
                for (MethodAttribute attribute : attributes) {
                    w.println("store.setMethodAttribute(%s, \"%s\", %s.%s);",
                            getClassLiteralString(type), method.getName(),
                            MethodAttribute.class.getCanonicalName(),
                            attribute.name());
                    w.splitIfNeeded();
                }
            }
        }
    }

    private void writeProxys(SplittingSourceWriter w, ConnectorBundle bundle) {
        Set needsProxySupport = bundle.getNeedsProxySupport();
        for (JClassType type : needsProxySupport) {
            w.print("store.setProxyHandler(");
            writeClassLiteral(w, type);
            w.print(", new ");
            w.print(ProxyHandler.class.getCanonicalName());
            w.println("() {");
            w.indent();

            w.println("public Object createProxy(final "
                    + InvokationHandler.class.getName() + " handler) {");
            w.indent();

            w.print("return new ");
            w.print(type.getQualifiedSourceName());
            w.println("() {");
            w.indent();

            JMethod[] methods = type.getOverridableMethods();
            for (JMethod method : methods) {
                if (method.isAbstract()) {
                    w.print("public ");
                    w.print(method.getReturnType().getQualifiedSourceName());
                    w.print(" ");
                    w.print(method.getName());
                    w.print("(");

                    JType[] types = method.getParameterTypes();
                    for (int i = 0; i < types.length; i++) {
                        if (i != 0) {
                            w.print(", ");
                        }
                        w.print(types[i].getQualifiedSourceName());
                        w.print(" p");
                        w.print(Integer.toString(i));
                    }

                    w.println(") {");
                    w.indent();

                    if (!method.getReturnType().getQualifiedSourceName()
                            .equals("void")) {
                        w.print("return ");
                    }

                    w.print("handler.invoke(this, ");
                    w.print(TypeData.class.getCanonicalName());
                    w.print(".getType(");
                    writeClassLiteral(w, type);
                    w.print(").getMethod(\"");
                    w.print(escape(method.getName()));
                    w.print("\"), new Object [] {");
                    for (int i = 0; i < types.length; i++) {
                        w.print("p" + i + ", ");
                    }
                    w.println("});");

                    w.outdent();
                    w.println("}");
                }
            }

            w.outdent();
            w.println("};");

            w.outdent();
            w.println("}");

            w.outdent();
            w.println("});");

            w.splitIfNeeded();
        }
    }

    private void writeParamTypes(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Map> needsParamTypes = bundle
                .getNeedsParamTypes();
        for (Entry> entry : needsParamTypes
                .entrySet()) {
            JClassType type = entry.getKey();

            Set methods = entry.getValue();
            for (JMethod method : methods) {
                w.print("store.setParamTypes(");
                writeClassLiteral(w, type);
                w.print(", \"");
                w.print(escape(method.getName()));
                w.print("\", new Type[] {");

                for (JType parameter : method.getParameterTypes()) {
                    ConnectorBundleLoaderFactory.writeTypeCreator(w, parameter);
                    w.print(", ");
                }

                w.println("});");

                w.splitIfNeeded();
            }
        }
    }

    private void writeInvokers(TreeLogger logger, SplittingSourceWriter w,
            ConnectorBundle bundle) throws UnableToCompleteException {
        Map> needsInvoker = bundle.getNeedsInvoker();
        for (Entry> entry : needsInvoker.entrySet()) {
            JClassType type = entry.getKey();

            TreeLogger typeLogger = logger.branch(Type.DEBUG,
                    "Creating invokers for " + type);

            Set methods = entry.getValue();
            for (JMethod method : methods) {
                w.print("store.setInvoker(");
                writeClassLiteral(w, type);
                w.print(", \"");
                w.print(escape(method.getName()));
                w.print("\",");

                if (method.isPublic()) {
                    typeLogger.log(Type.DEBUG,
                            "Invoking " + method.getName() + " using java");

                    writeJavaInvoker(w, type, method);
                } else {
                    TreeLogger methodLogger = typeLogger.branch(Type.DEBUG,
                            "Invoking " + method.getName() + " using jsni");
                    // Must use JSNI to access non-public methods
                    writeJsniInvoker(methodLogger, w, type, method);
                }

                w.println(");");

                w.splitIfNeeded();
            }
        }
    }

    private void writeJsniInvoker(TreeLogger logger, SplittingSourceWriter w,
            JClassType type, JMethod method) throws UnableToCompleteException {
        w.println("new JsniInvoker() {");
        w.indent();

        w.println(
                "protected native Object jsniInvoke(Object target, %s params) /*-{ ",
                JsArrayObject.class.getName());
        w.indent();

        JType returnType = method.getReturnType();
        boolean hasReturnType = !"void"
                .equals(returnType.getQualifiedSourceName());

        // Note that void is also a primitive type
        boolean hasPrimitiveReturnType = hasReturnType
                && returnType.isPrimitive() != null;

        if (hasReturnType) {
            w.print("return ");

            if (hasPrimitiveReturnType) {
                // Integer.valueOf(expression);
                w.print("@%s::valueOf(%s)(",
                        returnType.isPrimitive().getQualifiedBoxedSourceName(),
                        returnType.getJNISignature());

                // Implementation tested briefly, but I don't dare leave it
                // enabled since we are not using it in the framework and we
                // have not tests for it.
                logger.log(Type.ERROR,
                        "JSNI invocation is not yet supported for methods with "
                                + "primitive return types. Change your method "
                                + "to public to be able to use conventional"
                                + " Java invoking instead.");
                throw new UnableToCompleteException();
            }
        }

        JType[] parameterTypes = method.getParameterTypes();

        w.print("target.@%s::" + method.getName() + "(*)(",
                method.getEnclosingType().getQualifiedSourceName());
        for (int i = 0; i < parameterTypes.length; i++) {
            if (i != 0) {
                w.print(", ");
            }

            w.print("params[" + i + "]");

            JPrimitiveType primitive = parameterTypes[i].isPrimitive();
            if (primitive != null) {
                // param.intValue();
                w.print(".@%s::%sValue()()",
                        primitive.getQualifiedBoxedSourceName(),
                        primitive.getQualifiedSourceName());
            }
        }

        if (hasPrimitiveReturnType) {
            assert hasReturnType;
            w.print(")");
        }

        w.println(");");

        if (!hasReturnType) {
            w.println("return null;");
        }

        w.outdent();
        w.println("}-*/;");

        w.outdent();
        w.print("}");
    }

    private void writeJavaInvoker(SplittingSourceWriter w, JClassType type,
            JMethod method) {
        w.println("new Invoker() {");
        w.indent();

        w.println("public Object invoke(Object target, Object[] params) {");
        w.indent();

        JType returnType = method.getReturnType();
        boolean hasReturnType = !"void"
                .equals(returnType.getQualifiedSourceName());
        if (hasReturnType) {
            w.print("return ");
        }

        JType[] parameterTypes = method.getParameterTypes();

        w.print("((" + type.getQualifiedSourceName() + ") target)."
                + method.getName() + "(");
        for (int i = 0; i < parameterTypes.length; i++) {
            JType parameterType = parameterTypes[i];
            if (i != 0) {
                w.print(", ");
            }
            String parameterTypeName = getBoxedTypeName(parameterType);

            if (parameterTypeName.startsWith("elemental.json.Json")) {
                // Need to pass through native method to allow casting Object to
                // JSO if the value is a string
                w.print("%s.<%s>obj2jso(params[%d])",
                        JsonDecoder.class.getCanonicalName(), parameterTypeName,
                        i);
            } else {
                w.print("(" + parameterTypeName + ") params[" + i + "]");
            }
        }
        w.println(");");

        if (!hasReturnType) {
            w.println("return null;");
        }

        w.outdent();
        w.println("}");

        w.outdent();
        w.print("}");
    }

    private void writeReturnTypes(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Map> methodReturnTypes = bundle
                .getMethodReturnTypes();
        for (Entry> entry : methodReturnTypes
                .entrySet()) {
            JClassType type = entry.getKey();

            Set methods = entry.getValue();
            for (JMethod method : methods) {
                // setReturnType(Class type, String methodName, Type
                // returnType)
                w.print("store.setReturnType(");
                writeClassLiteral(w, type);
                w.print(", \"");
                w.print(escape(method.getName()));
                w.print("\", ");
                writeTypeCreator(w, method.getReturnType());
                w.println(");");

                w.splitIfNeeded();
            }
        }
    }

    private void writeGwtConstructors(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Set constructors = bundle.getGwtConstructors();
        for (JClassType type : constructors) {
            w.print("store.setConstructor(");
            writeClassLiteral(w, type);
            w.println(", new Invoker() {");
            w.indent();

            w.println("public Object invoke(Object target, Object[] params) {");
            w.indent();

            w.print("return ");
            w.print(GWT.class.getName());
            w.print(".create(");
            writeClassLiteral(w, type);
            w.println(");");

            w.outdent();
            w.println("}");

            w.outdent();
            w.println("});");

            w.splitIfNeeded();
        }
    }

    public static void writeClassLiteral(SourceWriter w, JType type) {
        w.print(getClassLiteralString(type));
    }

    public static String getClassLiteralString(JType type) {
        return type.getQualifiedSourceName() + ".class";
    }

    private void writeIdentifiers(SplittingSourceWriter w,
            ConnectorBundle bundle) {
        Map> identifiers = bundle.getIdentifiers();
        for (Entry> entry : identifiers.entrySet()) {
            Set ids = entry.getValue();
            JClassType type = entry.getKey();
            for (String id : ids) {
                w.print("store.setClass(\"");
                w.print(escape(id));
                w.print("\", ");
                writeClassLiteral(w, type);
                w.println(");");

                w.splitIfNeeded();
            }
        }
    }

    private List buildBundles(TreeLogger logger,
            TypeOracle typeOracle)
            throws NotFoundException, UnableToCompleteException {

        Map> connectorsByLoadStyle = new HashMap>();
        for (LoadStyle loadStyle : LoadStyle.values()) {
            connectorsByLoadStyle.put(loadStyle, new ArrayList());
        }

        // Find all types with a valid mapping
        Collection selectedTypes = getConnectorsForWidgetset(logger,
                typeOracle);

        // Group by load style
        for (JClassType connectorSubtype : selectedTypes) {
            LoadStyle loadStyle = getLoadStyle(connectorSubtype);
            if (loadStyle != null) {
                connectorsByLoadStyle.get(loadStyle).add(connectorSubtype);
            }
        }

        List bundles = new ArrayList();

        Collection visitors = getVisitors(typeOracle);

        ConnectorBundle eagerBundle = new ConnectorBundle(
                ConnectorBundleLoader.EAGER_BUNDLE_NAME, visitors, typeOracle);
        TreeLogger eagerLogger = logger.branch(Type.TRACE,
                "Populating eager bundle");

        // Eager connectors and all RPC interfaces are loaded by default
        eagerBundle.processTypes(eagerLogger,
                connectorsByLoadStyle.get(LoadStyle.EAGER));
        eagerBundle.processType(eagerLogger, typeOracle
                .findType(UnknownComponentConnector.class.getCanonicalName()));
        eagerBundle.processType(eagerLogger, typeOracle
                .findType(UnknownExtensionConnector.class.getCanonicalName()));
        eagerBundle.processSubTypes(eagerLogger,
                typeOracle.getType(ClientRpc.class.getName()));
        eagerBundle.processSubTypes(eagerLogger,
                typeOracle.getType(ServerRpc.class.getName()));

        bundles.add(eagerBundle);

        ConnectorBundle deferredBundle = new ConnectorBundle(
                ConnectorBundleLoader.DEFERRED_BUNDLE_NAME, eagerBundle);
        TreeLogger deferredLogger = logger.branch(Type.TRACE,
                "Populating deferred bundle");
        deferredBundle.processTypes(deferredLogger,
                connectorsByLoadStyle.get(LoadStyle.DEFERRED));

        bundles.add(deferredBundle);

        Collection lazy = connectorsByLoadStyle.get(LoadStyle.LAZY);
        for (JClassType type : lazy) {
            ConnectorBundle bundle = new ConnectorBundle(type.getName(),
                    eagerBundle);
            TreeLogger subLogger = logger.branch(Type.TRACE,
                    "Populating " + type.getName() + " bundle");
            bundle.processType(subLogger, type);

            bundles.add(bundle);
        }

        return bundles;
    }

    /**
     * Returns the connector types that should be included in the widgetset.
     * This method can be overridden to create a widgetset only containing
     * selected connectors.
     * 

* The default implementation finds all type implementing * {@link ServerConnector} that have a @{@link Connect} annotation. It also * checks that multiple connectors aren't connected to the same server-side * class. * * @param logger * the logger to which information can be logged * @param typeOracle * the type oracle that can be used for finding types * @return a collection of all the connector types that should be included * in the widgetset * @throws UnableToCompleteException * if the operation fails */ protected Collection getConnectorsForWidgetset( TreeLogger logger, TypeOracle typeOracle) throws UnableToCompleteException { JClassType serverConnectorType; try { serverConnectorType = typeOracle .getType(ServerConnector.class.getName()); } catch (NotFoundException e) { logger.log(Type.ERROR, "Can't find " + ServerConnector.class.getName()); throw new UnableToCompleteException(); } JClassType[] types = serverConnectorType.getSubtypes(); Map mappings = new TreeMap(); // Keep track of what has happened to avoid logging intermediate state Map> replaced = new TreeMap>( ConnectorBundle.jClassComparator); for (JClassType type : types) { Connect connectAnnotation = type.getAnnotation(Connect.class); if (connectAnnotation == null) { continue; } String identifier = connectAnnotation.value().getCanonicalName(); JClassType previousMapping = mappings.put(identifier, type); if (previousMapping != null) { // There are multiple mappings, pick the subclass JClassType subclass; JClassType superclass; if (previousMapping.isAssignableFrom(type)) { subclass = type; superclass = previousMapping; } else if (type.isAssignableFrom(previousMapping)) { subclass = previousMapping; superclass = type; } else { // Neither inherits from the other - this is a conflict logger.log(Type.ERROR, "Conflicting @Connect mappings detected for " + identifier + ": " + type.getQualifiedSourceName() + " and " + previousMapping.getQualifiedSourceName() + ". There can only be multiple @Connect mappings for the same server-side type if one is the subclass of the other."); throw new UnableToCompleteException(); } mappings.put(identifier, subclass); // Inherit any previous replacements List previousReplacements = replaced .remove(superclass); if (previousReplacements == null) { previousReplacements = new ArrayList(); } previousReplacements.add(superclass); replaced.put(subclass, previousReplacements); } } // Log the final set of replacements for (Entry> entry : replaced.entrySet()) { String msg = entry.getKey().getQualifiedSourceName() + " replaces "; List list = entry.getValue(); for (int i = 0; i < list.size(); i++) { if (i != 0) { msg += ", "; } msg += list.get(i).getQualifiedSourceName(); } logger.log(Type.INFO, msg); } // Return the types of the final mapping return mappings.values(); } private Collection getVisitors(TypeOracle oracle) throws NotFoundException { List visitors = Arrays. asList( new ConnectorInitVisitor(), new StateInitVisitor(), new WidgetInitVisitor(), new RendererVisitor(), new ClientRpcVisitor(), new ServerRpcVisitor(), new OnStateChangeVisitor()); for (TypeVisitor typeVisitor : visitors) { typeVisitor.init(oracle); } return visitors; } protected LoadStyle getLoadStyle(JClassType connectorType) { Connect annotation = connectorType.getAnnotation(Connect.class); return annotation.loadStyle(); } public static String getBoxedTypeName(JType type) { if (type.isPrimitive() != null) { // Used boxed types for primitives return type.isPrimitive().getQualifiedBoxedSourceName(); } else { return type.getErasedType().getQualifiedSourceName(); } } public static void writeTypeCreator(SourceWriter sourceWriter, JType type) { String typeName = ConnectorBundleLoaderFactory.getBoxedTypeName(type); JParameterizedType parameterized = type.isParameterized(); if (parameterized != null) { sourceWriter.print("new Type(\"" + typeName + "\", "); sourceWriter.print("new Type[] {"); JClassType[] typeArgs = parameterized.getTypeArgs(); for (JClassType jClassType : typeArgs) { writeTypeCreator(sourceWriter, jClassType); sourceWriter.print(", "); } sourceWriter.print("}"); } else { sourceWriter.print("new Type(" + typeName + ".class"); } sourceWriter.print(")"); } }