Package org.timepedia.exporter.rebind

Source Code of org.timepedia.exporter.rebind.ClassExporter

package org.timepedia.exporter.rebind;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.io.PrintWriter;

/**
* This class performs the generation of export methods for a single class
*
* @author Ray Cromwell <ray@timepedia.org>
*/
public class ClassExporter {
    private TreeLogger logger;
    private GeneratorContext ctx;
    private ExportableTypeOracle xTypeOracle;
    private SourceWriter sw;
    private ArrayList<JExportableClassType> exported;
    private HashSet<String> visited;
    private static final String ARG_PREFIX = "arg";

    public ClassExporter(TreeLogger logger, GeneratorContext ctx) {
       this(logger, ctx, new HashSet<String>());
    }

    public ClassExporter(TreeLogger logger, GeneratorContext ctx,
                         HashSet<String> visited) {
        this.logger = logger;
        this.ctx = ctx;
        // a type oracle that can answer questions about whether types are
        // exportable
        xTypeOracle = new ExportableTypeOracle(ctx.getTypeOracle(), logger);
        this.visited = visited;
        exported = new ArrayList<JExportableClassType>();
            
    }

    /**
     * This method generates an implementation of the specified interface
     * that accepts a JavaScriptObject in its constructor containing a callback.
     * It then delegates the single-method of the interface to this callback.
     * <p/>
     * For example:
     * <p/>
     * <p/>
     * / **
     * * @gwt.exportClosure
     * * /
     * public interface ClickListener implements Exportable {
     * public void onClick(Sender s);
     * }
     * <p/>
     * generates a delegation class
     * <p/>
     * public class ClickListenerImpl implements Exporter, ClickListener {
     * <p/>
     * private JavaScriptObject jso;
     * public ClickListenerClosure(JavaScriptObject jso) {
     * this.jso = jso;
     * }
     * <p/>
     * public void onClick(Sender s) {
     * invoke(jso, ExporterBase.wrap(s));
     * }
     * <p/>
     * public native void invoke(JavaScriptObject closure,
     * JavascriptObject s) {
     * closure(s);
     * }
     * <p/>
     * }
     *
     * @param requestedType
     */
    public void exportClosure(JExportableClassType requestedType)
            throws UnableToCompleteException {

        if (requestedType == null) {
            logger.log(
                    TreeLogger.ERROR, "Type '"
                    + requestedType.getQualifiedSourceName() +
                    "' does not implement Exportable", null
            );
            throw new UnableToCompleteException();
        }

        // get the name of the Java class implementing Exporter
        String genName = requestedType.getExporterImplementationName();




        sw.indent();

        // export constructor
        sw.println("private " + ExportableTypeOracle.JSO_CLASS + " jso;");
        sw.println();
        sw.println("public "+genName+"() {}");

        sw.println(
                "public " + genName + "(" + ExportableTypeOracle.JSO_CLASS +
                        " jso) {"
        );
        sw.indent();
        sw.println("this.jso = jso;");
        sw.outdent();
        sw.println("}");
        sw.println();

        // export static factory method
        sw.println(
                "public static " + genName + " makeClosure(" +
                        ExportableTypeOracle.JSO_CLASS + " closure) {"
        );
        sw.indent();
        sw.println("return new " + genName + "(closure);");
        sw.outdent();
        sw.println("}");
        sw.println();

        JExportableMethod[] methods = requestedType.getExportableMethods();

        if (methods.length != 1) {
            logger.log(
                    TreeLogger.ERROR, "Interface " +
                    requestedType.getQualifiedSourceName() +
                    " has more than one " +
                    "declared method. @gwt.exportClosure only currently works for " +
                    "single method interfaces.", null
            );
            throw new UnableToCompleteException();
        }

        JExportableMethod method = methods[0];
        JExportableType retType = method.getExportableReturnType();
        if (retType == null) {
            logger.log(
                    TreeLogger.ERROR,
                    "Return type of method " + method + " is not exportable.",
                    null
            );
            throw new UnableToCompleteException();
        }

        if (retType.needsExport() &&
                !exported.contains(retType.getQualifiedSourceName())) {
            if(exportDependentClass(retType.getQualifiedSourceName()))
              exported.add((JExportableClassType) retType);
        }

        exportDependentParams(method);

        boolean isVoid = retType.getQualifiedSourceName().equals("void");
        boolean noParams = method.getExportableParameters().length == 0;
        sw.print(
                "public " +
                        method.getExportableReturnType()
                                .getQualifiedSourceName()
        );

        sw.print(" " + method.getName() + "(");
        declareParameters(method, true);
        sw.println(") {");
        sw.indent();
        sw.print(( isVoid ? "" : "return " ) + "invoke(jso" +
                (noParams ? "" : ","));
        declareJavaPassedValues(method, false);
        sw.println(");");
        sw.outdent();
        sw.println("}");
        sw.println();
        sw.print(
                "public native " + ( isVoid ? "void" :
                        method.getExportableReturnType()
                                .getQualifiedSourceName() )
        );
        sw.print(" invoke(" + ExportableTypeOracle.JSO_CLASS + " closure");
        if (method.getExportableParameters().length > 0) {
            sw.print(", ");
        }

        declareParameters(method, true);
        sw.println(") /*-{");
        sw.indent();
        sw.print((!isVoid ? "var result= " : "")+"closure(");
        declareJavaPassedValues(method, true);
        sw.println(");");
        if (retType.needsExport() && !isVoid) {
            sw.println(
                    "if(result != null && result != undefined) " +
                            "result=result.instance;"
            );
            sw.println("else if(result == undefined) result=null;");
        }
        if (!isVoid) {
            sw.println("return result;");
        }
        sw.outdent();
        sw.println("}-*/;");
        sw.println();
        sw.outdent();
    }


    /**
     * This method generates an implementation class that implements Exporter
     * and returns the fully qualified name of the class.
     *
     * @param requestedClass
     * @return
     * @throws UnableToCompleteException
     */
    public String exportClass(String requestedClass)
            throws UnableToCompleteException {

        // JExportableClassType is a wrapper around JClassType
        // which provides only the information and logic neccessary for
        // the generator
        JExportableClassType requestedType =
                xTypeOracle.findExportableClassType(requestedClass);

        // add this so we don't try to recursively reexport ourselves later
        exported.add(requestedType);
        visited.add(requestedType.getQualifiedSourceName());
       
        if (requestedType == null) {
            logger.log(
                    TreeLogger.ERROR, "Type '"
                    + requestedClass + "' does not implement Exportable", null
            );
            throw new UnableToCompleteException();
        }

        // get the name of the Java class implementing Exporter
        String genName = requestedType.getExporterImplementationName();

        // get the package name of the Exporter implementation
        String packageName = requestedType.getPackageName();

        // get a fully qualified reference to the Exporter implementation
        String qualName =
                requestedType.getQualifiedExporterImplementationName();

        boolean isClosure=xTypeOracle.isClosure(requestedClass);

        // try to construct a sourcewriter for the qualified name
        if (isClosure) {
             sw = getSourceWriter(
                    logger, ctx, packageName,
                    genName, "Exporter",
                    requestedType.getQualifiedSourceName());

        } else {
            sw = getSourceWriter(
                    logger, ctx, packageName,
                    genName, "Exporter");
        }
        if (sw == null) {
            return qualName; // null, already generated
        }

        if(isClosure) exportClosure(requestedType);

        sw.indent();

        // here we define a JSNI Javascript method called export0()
        sw.println("public native void export0() /*-{");
        sw.indent();

        // if not defined, we create a Javascript package hierarchy
        // foo.bar.baz to hold the Javascript bridge
        declarePackages(requestedType);

        // export Javascript constructors
        exportConstructor(requestedType);

        // export all static fields
        exportFields(requestedType);

        // export all exportable methods
        exportMethods(requestedType);

        // add map from TypeName to JS constructor in ExporterBase
        registerTypeMap(requestedType);

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

        sw.println();

        // the Javascript constructors refer to static factory methods
        // on the Exporter implementation, referenced via JSNI
        // We generate them here
        exportStaticFactoryConstructors(requestedType);

        // finally, generate the Exporter.export() method
        // which invokes recursively, via GWT.create(),
        // every other Exportable type we encountered in the exported ArrayList
        // ending with a call to export0()

        genExportMethod(requestedType, exported);
        sw.outdent();

        sw.commit(logger);

        // return the name of the generated Exporter implementation class
        return qualName;
    }

    private void registerTypeMap(JExportableClassType requestedType) {
        sw.print(
                "@org.timepedia.exporter.client.ExporterBase::addTypeMap(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(" +
//                        "Ljavg/lang/String;" +
//                        "Lcom/google/gwt/core/client/JavaScriptObject;)(" +
                        "\"" + requestedType.getQualifiedSourceName() + "\", " +
                        "$wnd." + requestedType.getJSQualifiedExportName() +
                        ");"
        );
    }

    /**
     * Exports a static factory method corresponding to each exportable
     * constructor of the class
     *
     * @param requestedType
     */
    private void exportStaticFactoryConstructors(
            JExportableClassType requestedType) {

        JExportableConstructor[] constructors =
                requestedType.getExportableConstructors();

        for (JExportableConstructor constructor : constructors) {
            exportStaticFactoryConstructor(constructor);
        }

    }

    /**
     * Exports all exportable methods of a class
     *
     * @param requestedType
     * @throws UnableToCompleteException
     */
    private void exportMethods(JExportableClassType requestedType)
            throws UnableToCompleteException {
        HashMap<String, JExportableMethod> visited =
                new HashMap<String, JExportableMethod>();
        for (JExportableMethod method : requestedType.getExportableMethods()) {
            exportMethod(method, visited);
        }
    }


    /**
     * Exports a Javascript constructor as
     * $wnd.packageName.classname = function(args) {
     * if(arg0 is GWT type) { this.instance = arg0; }
     * else this.instance = invoke static factory method with args
     * }
     *
     * @param requestedType
     */
    private void exportConstructor(JExportableClassType requestedType)
            throws UnableToCompleteException {
        // constructor.getJSQualifiedExportName() returns fully qualified package
        // + exported class name
        sw.print(
                "$wnd." + requestedType.getJSQualifiedExportName() +
                        " = function("
        );

        // for every parameter 0..n of the constructor, we generate
        // arg0, ..., argn
//        declareJSParameters(constructor);
        sw.println(") {");
        sw.indent();
        // check if this is being used to wrap GWT types
        // e.g. code is calling constructor as
        // new $wnd.package.className(opaqueGWTobject)
        // if so, we store the opaque reference in this.instance
        sw.println(
                "if(arguments.length == 1 && (arguments[0] != null && " +
                        "@com.google.gwt.core.client.GWT::getTypeName(Ljava/lang/Object;)(arguments[0]) == '" +
                        requestedType.getQualifiedSourceName() + "')) {"
        );
        sw.indent();

        sw.println(" this.instance = arguments[0];");
        sw.outdent();
        sw.println("}");

        JExportableConstructor[] constructors =
                requestedType.getExportableConstructors();

        // used to hold arity of constructors that have been generated
        HashMap<Integer, JExportableConstructor> arity =
                new HashMap<Integer, JExportableConstructor>();

        for (JExportableConstructor constructor : constructors) {
            int numArguments = constructor.getExportableParameters().length;
            JExportableConstructor conflicting = arity.get(numArguments);
            if (conflicting != null) {
                logger.log(
                        TreeLogger.ERROR,
                        "Constructor " + conflicting + " with " + numArguments +
                                " " +
                                "arguments conflicts with " + constructor +
                                "." +
                                "Two constructors may not have identical numbers of " +
                                "arguments.",
                        null
                );
                throw new UnableToCompleteException();
            }
            arity.put(numArguments, constructor);
            sw.println("else if(arguments.length == " + numArguments + ") {");
            sw.indent();

            // else someone is calling the constructor normally
            // we generate a JSNI call to the matching static factory method
            // and store it in this.instance
            sw.print(
                    "this.instance = @" +
                            constructor.getStaticFactoryJSNIReference() + "("
            );

            // pass arguments[0], ..., arguments[n] to the JSNI call
            declareJSPassedValues(constructor, true);
            sw.println(");");
            sw.outdent();
            sw.println("}");


        }

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

        JExportableClassType superClass =
                requestedType.getExportableSuperClassType();


        if (superClass != null && superClass.needsExport() &&
                !exported.contains(superClass)) {
            if(exportDependentClass(superClass.getQualifiedSourceName()));
               exported.add(superClass);
        }
        // we assign the prototype of the class to underscore so we can use it
        // later to define a bunch of methods
        sw.print(
                "var _=$wnd." + requestedType.getJSQualifiedExportName() +
                        ".prototype = "
        );
        sw.println(
                superClass == null ? "new Object();" :
                        "new $wnd." + superClass.getJSQualifiedExportName() +
                                "();"
        );

    }

    /**
     * We create a static factory method
     * public static [typeName] ___create(args) that just invokes
     * the real constructor with the args
     *
     * @param constructor
     */
    private void exportStaticFactoryConstructor(
            JExportableConstructor constructor) {
        JExportableClassType consType =
                (JExportableClassType) constructor.getExportableReturnType();
        String typeName = consType
                .getQualifiedSourceName();
        sw.print(
                "public static " + typeName + " " +
                        constructor.getStaticFactoryMethodName() + "("
        );
        declareParameters(constructor);
        sw.println(") {");
        sw.indent();
        sw.print("return new " + typeName + "(");
        declareJavaPassedValues(constructor, false);
        sw.println(");");
        sw.outdent();
        sw.println("}");

    }

    /**
     * Generate comma separated list of argnames, arg0, ..., arg_n where
     * n = number of parameters of method
     *
     * @param method
     * @param useArgumentsArray use arguments[n] instead of argn
     */
    private void declareJSPassedValues(JExportableMethod method,
                                       boolean useArgumentsArray) {
        JExportableParameter params[] = method.getExportableParameters();
        for (int i = 0; i < params.length; i++) {
            sw.print(
                    params[i].getExportParameterValue(
                            useArgumentsArray ?
                                    "arguments[" + i + "]" : ARG_PREFIX + i
                    )
            );
            if (i < params.length - 1) {
                sw.print(", ");
            }
        }
    }

    /**
     * Generate comma separated list of argnames, arg0, ..., arg_n where
     * n = number of parameters of method
     *
     * @param method
     * @param wrap whether to wrap the passed value with ExporterBase::wrap
     */
    private void declareJavaPassedValues(JExportableMethod method, boolean wrap) {
        JExportableParameter params[] = method.getExportableParameters();
        for (int i = 0; i < params.length; i++) {
            JExportableType eType = params[i].getExportableType();
            boolean needExport = eType != null && eType.needsExport();
            if(wrap && needExport)
               sw.print("@org.timepedia.exporter.client.ExporterBase::wrap(Lorg/timepedia/exporter/client/Exportable;)(");
            sw.print(ARG_PREFIX + i);
            if(wrap && needExport) sw.print(")");
            if (i < params.length - 1) {
                sw.print(", ");
            }
        }
    }

    /**
     * Generate comma separated list of argnames, arg0, ..., arg_n where
     * n = number of parameters of constructor
     *
     * @param method
     * @param includeTypes true if arg names should have declared types
     */
    private void declareParameters(JExportableMethod method,
                                   boolean includeTypes) {
        JExportableParameter params[] = method.getExportableParameters();
        for (int i = 0; i < params.length; i++) {
            sw.print(
                    ( includeTypes ? params[i].getTypeName() : "" ) + " " +
                            ARG_PREFIX + i
            );
            if (i < params.length - 1) {
                sw.print(", ");
            }
        }
    }


    /**
     * declare java typed Java method parameters
     *
     * @param method
     */
    private void declareParameters(JExportableMethod method) {
        declareParameters(method, true);
    }

    /**
     * declare type-less Javascript method parameters
     *
     * @param method
     */
    private void declareJSParameters(JExportableMethod method) {
        declareParameters(method, false);
    }

    /**
     * For each exportable field Foo, we generate the following Javascript:
     * $wnd.package.className.Foo = JSNI Reference to Foo
     *
     * @param requestedType
     * @throws UnableToCompleteException
     */
    private void exportFields(JExportableClassType requestedType)
            throws UnableToCompleteException {
        for (JExportableField field : requestedType.getExportableFields()) {

            sw.print("$wnd." + field.getJSQualifiedExportName() + " = ");
            sw.println("@" + field.getJSNIReference() + ";");
        }

    }


    /**
     * Export a method
     * If the return type of the method is Exportable, we invoke
     * ClassExporter recursively on this type
     * <p/>
     * For static methods, the Javascript looks like this:
     * $wnd.package.className.staticMethod = function(args) {
     * // body
     * }
     * <p/>
     * for regular methods, it looks like
     * <p/>
     * _.methodName = function(args) {
     * //body
     * }
     * <p/>
     * where _ is previously assigned to $wnd.package.className.prototype
     * <p/>
     * For methods returning Exportable types, the body looks like
     * <p/>
     * return new $wnd.package.className(this.instance.@methodNameJSNI(args));
     * <p/>
     * which wraps the returned type, otherwise it looks like this
     * <p/>
     * return this.instance.@methodNameJSNI(args);
     * <p/>
     * for primitives, String, subclasses of Number, and JavaScriptObject
     *
     * @param method
     * @param visited
     * @throws UnableToCompleteException
     */
    private void exportMethod(JExportableMethod method,
                              HashMap<String, JExportableMethod> visited)
            throws UnableToCompleteException {
        JExportableType retType = method.getExportableReturnType();

        if (retType == null) {
            logger.log(
                    TreeLogger.ERROR, "Return type of method " +
                    method.toString() + " is not Exportable.", null
            );
            throw new UnableToCompleteException();
        }

        int arity = method.getExportableParameters().length;
        String name = method.getUnqualifiedExportName();
        String key = name + "_" + arity;

        JExportableMethod conflicting = visited.get(key);

        if (conflicting != null) {
            logger.log(
                    TreeLogger.ERROR, "Method " + method + " having " + arity +
                    " arguments conflicts with " + conflicting + ". " +
                    "Two exportable methods cannot have the same number of arguments. " +
                    "Use @gwt.export <newName> on one of the methods to disambiguate.",
                    null
            );
            throw new UnableToCompleteException();
        } else {
            visited.put(key, method);
        }

        // return type needs to be exported if it is not a primitive
        // String,Number,JSO, etc and it hasn't already been exported
        // we need to export it because we need it to wrap the returned value
        if (retType != null && retType.needsExport() && !exported.contains(
                retType
        )) {
            if(exportDependentClass(retType.getQualifiedSourceName()));
            exported.add((JExportableClassType) retType);
        }

        exportDependentParams(method);


        if (method.isStatic()) {
            sw.print(
                    "$wnd." + method.getJSQualifiedExportName() +
                            "= function("
            );
        } else {
            sw.print("_." + method.getUnqualifiedExportName() + "= function(");
        }
        declareJSParameters(method);
        sw.print(") { ");
        boolean isVoid = retType.getQualifiedSourceName().equals("void");


        sw.print(
                "var x=" + ( method.isStatic() ? "@" : "this.instance.@" ) +
                        method.getJSNIReference() + "("
        );


        declareJSPassedValues(method, false);

        // end method call
        sw.print(");");

        if (!retType.needsExport()) {
            sw.print((isVoid ? "" : "return ")+"(");
        } else {

            sw.print((isVoid ? "" : "return ")+
                    "@org.timepedia.exporter.client.ExporterBase::wrap(Lorg/timepedia/exporter/client/Exportable;)("

            );
        }

        // end wrap() or non-exportable return case call
        sw.println("x); }");
    }

    private void exportDependentParams(JExportableMethod method)
            throws UnableToCompleteException {
        // for convenience to the developer, let's export any exportable
        // parameters
        for (JExportableParameter param : method.getExportableParameters()) {
            JExportableType eType = param.getExportableType();
            if (eType != null && eType.needsExport() &&
                    !exported.contains(eType)) {
                if(exportDependentClass(eType.getQualifiedSourceName()))
                   exported.add((JExportableClassType) eType);
            }
        }
    }

    private boolean exportDependentClass(String qualifiedSourceName)
            throws UnableToCompleteException {

        if(visited.contains(qualifiedSourceName)) return false;
        visited.add(qualifiedSourceName);
        ClassExporter exporter = new ClassExporter(logger, ctx, visited);
        exporter.exportClass(qualifiedSourceName);
        return true;

    }


    /**
     * For each subpackage of sub1.sub2.sub3... we create a chain of
     * objects $wnd.sub1.sub2.sub3
     *
     * @param requestedClassType
     */
    private void declarePackages(JExportableClassType requestedClassType) {
        String requestedPackageName = requestedClassType.getJSExportPackage();
        String superPackages[] = requestedPackageName.split("\\.");
        String prefix = "";
        for (int i = 0; i < superPackages.length; i++) {
            if (!superPackages[i].equals("client")) {
                sw.println(
                        "if(!$wnd." + prefix + "" + superPackages[i] +
                                ") $wnd." + prefix + superPackages[i] + " = {} "
                );
                prefix += superPackages[i] + ".";
            }

        }
//        sw.println(
//                "if(!$wnd.___gwtwrapper) $wnd.___gwtwrapper = function(arg) {" +
//                        " return { instance: arg }; }"
//        );

    }

    /**
     * Generate the main export method
     * <p/>
     * <p/>
     * We generate a method that looks like:
     * <p/>
     * public void export() {
     * Exporter export1 = (Exporter)GWT.create(ExportableDependency1.class)
     * export1.export();
     * <p/>
     * Exporter export2 = (Exporter)GWT.create(ExportableDependency2.class)
     * export2.export();
     * <p/>
     * ...
     * export0();
     * }
     *
     * @param requestedType
     * @param exported      a list of other types that we depend on to be exported
     */
    private void genExportMethod(JExportableClassType requestedType,
                                 ArrayList<JExportableClassType> exported) {
        sw.println("public void export() { ");
        sw.indent();
        // first, export our dependencies
        int exprCount = 0;
        for (JExportableClassType classType : exported) {
            if (requestedType.getQualifiedSourceName().equals(
                    classType.getQualifiedSourceName()
            )) {
                continue;
            }
            String qualName =
                    classType.getQualifiedSourceName();

            String var = "export" + exprCount++;
            sw.println(
                    ExportableTypeOracle.EXPORTER_CLASS + " " + var + " = (" +
                            ExportableTypeOracle
                                    .EXPORTER_CLASS +
                            ") GWT.create(" + qualName + ".class);"
            );
            sw.println(var+".export();");
        }

        // now export our class
        sw.println("export0();");

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


    /**
     * Get SourceWriter for following class and preamble
     * package packageName;
     * import com.google.gwt.core.client.GWT;
     * import org.timepedia.exporter.client.Exporter;
     * public class className implements interfaceName (usually Exporter) {
     * <p/>
     * }
     *
     * @param logger
     * @param context
     * @param packageName
     * @param className
     * @param interfaceNames vararg list of interfaces
     * @return
     */
    protected SourceWriter getSourceWriter(
            TreeLogger logger, GeneratorContext context,
            String packageName, String className, String... interfaceNames) {
        PrintWriter printWriter = context.tryCreate(
                logger, packageName, className
        );
        if (printWriter == null) {
            return null;
        }
        ClassSourceFileComposerFactory composerFactory =
                new ClassSourceFileComposerFactory(packageName, className);
        composerFactory.addImport("com.google.gwt.core.client.GWT");
        for (String interfaceName : interfaceNames) {
            composerFactory.addImplementedInterface(interfaceName);
        }

        composerFactory.addImport("org.timepedia.exporter.client.Exporter");
        return composerFactory.createSourceWriter(context, printWriter);
    }
}
TOP

Related Classes of org.timepedia.exporter.rebind.ClassExporter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.