org.rstudio.core.rebind.command.CommandBundleGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.core.rebind.command.CommandBundleGenerator.java

Source

/*
 * CommandBundleGenerator.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.rebind.command;

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.JMethod;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

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

/**
 * This generator runs at compile time (like all GWT Generators) and creates
 * implementations for any subinterfaces of CommandBundle that are found in
 * client code.
 */
public class CommandBundleGenerator extends Generator {
    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String typeName)
            throws UnableToCompleteException {
        try {
            return new CommandBundleGeneratorHelper(logger, context, typeName).generate();
        } catch (UnableToCompleteException e) {
            throw e;
        } catch (Exception e) {
            logger.log(TreeLogger.Type.ERROR, "Barf", e);
            throw new UnableToCompleteException();
        }
    }
}

/**
 * The actual logic for type generation is moved into this separate class
 * so we can access a lot of the common info as fields instead of passing
 * them around.
 */
class CommandBundleGeneratorHelper {
    CommandBundleGeneratorHelper(TreeLogger logger, GeneratorContext context, String typeName) throws Exception {
        logger_ = logger;
        context_ = context;
        bundleType_ = context_.getTypeOracle().getType(typeName);
        commandMethods_ = getMethods(true, false, false);
        menuMethods_ = getMethods(false, true, false);
        shortcutsMethods_ = getMethods(false, false, true);
        packageName_ = bundleType_.getPackage().getName();
    }

    /**
     * Generates the impl class and returns its name.
     */
    public String generate() throws Exception {
        ImageResourceInfo images = generateImageBundle();

        simpleName_ = bundleType_.getName().replace('.', '_') + "__Impl";

        PrintWriter printWriter = context_.tryCreate(logger_, packageName_, simpleName_);
        if (printWriter != null) {
            // I don't fully understand why images is sometimes null but we better
            // not get into a situation where we are generating this type but don't
            // know what images we can use. Empirically it seems like images is
            // always null only when printWriter is also null.
            assert images != null;

            ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(packageName_, simpleName_);
            factory.setSuperclass(bundleType_.getName());
            factory.addImport("org.rstudio.core.client.command.AppCommand");
            factory.addImport("org.rstudio.core.client.command.MenuCallback");
            factory.addImport("org.rstudio.core.client.command.ShortcutManager");
            SourceWriter writer = factory.createSourceWriter(context_, printWriter);

            emitConstructor(writer, images);
            emitCommandFields(writer);
            emitMenus(writer);
            emitShortcuts(writer);
            emitCommandAccessors(writer);

            // Close the class and commit it
            writer.outdent();
            writer.println("}");
            context_.commit(logger_, printWriter);
        }
        return packageName_ + "." + simpleName_;
    }

    private void emitConstructor(SourceWriter writer, ImageResourceInfo images) throws UnableToCompleteException {
        writer.println("public " + simpleName_ + "() {");

        // Get additional properties from XML resource file, if exists
        Map<String, Element> props = getCommandProperties();
        // Implement the methods for the commands
        for (JMethod method : commandMethods_)
            emitCommandInitializers(writer, props, method, images);

        writer.println();
        writer.indentln("__registerShortcuts();");

        writer.println("}");
    }

    private void emitCommandFields(SourceWriter writer) throws UnableToCompleteException {
        // Declare the fields for the commands
        for (JMethod method : commandMethods_) {
            String name = method.getName();
            writer.println("private AppCommand " + name + "_;");
        }
    }

    private void emitMenus(SourceWriter writer) throws UnableToCompleteException {
        for (JMethod method : menuMethods_) {
            String name = method.getName();
            NodeList nodes = getConfigDoc("/commands/menu[@id='" + name + "']");
            if (nodes.getLength() == 0) {
                logger_.log(TreeLogger.Type.ERROR, "Unable to find config info for menu " + name);
                throw new UnableToCompleteException();
            } else if (nodes.getLength() > 1) {
                logger_.log(TreeLogger.Type.ERROR, "Duplicate menu entries for menu " + name);
            }

            String menuClass = new MenuEmitter(logger_, context_, bundleType_, (Element) nodes.item(0)).generate();

            writer.println("public void " + name + "(MenuCallback callback) {");
            writer.indentln("new " + menuClass + "(this).createMenu(callback);");
            writer.println("}");
        }
    }

    private void emitShortcuts(SourceWriter writer) throws UnableToCompleteException {
        writer.println("private void __registerShortcuts() {");
        writer.indent();
        NodeList nodes = getConfigDoc("/commands/shortcuts");
        for (int i = 0; i < nodes.getLength(); i++) {
            NodeList groups = nodes.item(i).getChildNodes();
            for (int j = 0; j < groups.getLength(); j++) {
                if (groups.item(j).getNodeType() != Node.ELEMENT_NODE)
                    continue;
                String groupName = ((Element) groups.item(j)).getAttribute("name");
                new ShortcutsEmitter(logger_, groupName, (Element) groups.item(j)).generate(writer);
            }
        }
        writer.outdent();
        writer.println("}");
    }

    private JMethod[] getMethods(boolean includeCommands, boolean includeMenus, boolean includeShortcuts)
            throws UnableToCompleteException {
        ArrayList<JMethod> methods = new ArrayList<JMethod>();
        for (JMethod method : bundleType_.getMethods()) {
            if (!method.isAbstract())
                continue;
            validateMethod(method);
            if (!includeCommands && isCommandMethod(method))
                continue;
            if (!includeMenus && isMenuMethod(method))
                continue;
            if (!includeShortcuts && isShortcutsMethod(method))
                continue;
            methods.add(method);
        }
        return methods.toArray(new JMethod[methods.size()]);
    }

    // Log and throw if anything is awry about the declaration
    private void validateMethod(JMethod method) throws UnableToCompleteException {
        if (isMenuMethod(method)) {
            if (method.getParameters().length != 1) {
                logger_.log(TreeLogger.Type.ERROR,
                        "Method " + method + " had the wrong number of parameters (expected 1)");
                throw new UnableToCompleteException();
            }

            String paramType = method.getParameters()[0].getType().getQualifiedSourceName();
            if (!paramType.equals("org.rstudio.core.client.command.MenuCallback")) {
                logger_.log(TreeLogger.Type.ERROR, "Method " + method + " had wrong parameter type (expected "
                        + "org.rstudio.core.client.command.MenuCallback)");
                throw new UnableToCompleteException();
            }
        } else {
            if (method.getParameters().length != 0) {
                logger_.log(TreeLogger.Type.ERROR, "Method " + method + " had parameters where none were expected");
                throw new UnableToCompleteException();
            }
        }

        if (!isCommandMethod(method) && !isMenuMethod(method) && !isShortcutsMethod(method)) {
            logger_.log(TreeLogger.Type.ERROR, "Method " + method + " had an unexpected return type");
            throw new UnableToCompleteException();
        }
    }

    private boolean isCommandMethod(JMethod method) {
        return method.getReturnType().getQualifiedSourceName().equals("org.rstudio.core.client.command.AppCommand");
    }

    private boolean isMenuMethod(JMethod method) {
        String sourceName = method.getReturnType().getQualifiedSourceName();
        return sourceName.equals("void");
    }

    private boolean isShortcutsMethod(JMethod method) {
        String sourceName = method.getReturnType().getQualifiedSourceName();
        return sourceName.equals("org.rstudio.core.client.command.ShortcutManager");
    }

    /**
     * Emit the getter method for the command--it is implemented as a cached
     * lazy-load. e.g.:
     *
     * public AppCommand newSourceDoc() {
     *   if (newSourceDoc_ == null) {
     *     newSourceDoc_ = new AppCommand();
     *     // call various setters...
     *   }
     *   return newSourceDoc_;
     * }
     */
    private void emitCommandInitializers(SourceWriter writer, Map<String, Element> props, JMethod method,
            ImageResourceInfo images) {
        String name = method.getName();
        writer.println(name + "_ = new AppCommand();");

        setProperty(writer, name, props.get(name), "id");
        setProperty(writer, name, props.get(name), "desc");
        setProperty(writer, name, props.get(name), "label");
        setProperty(writer, name, props.get(name), "buttonLabel");
        setProperty(writer, name, props.get(name), "menuLabel");
        // Any additional textual properties would be added here...

        setPropertyBool(writer, name, props.get(name), "visible");
        setPropertyBool(writer, name, props.get(name), "enabled");
        setPropertyBool(writer, name, props.get(name), "preventShortcutWhenDisabled");
        setPropertyBool(writer, name, props.get(name), "checkable");
        setPropertyBool(writer, name, props.get(name), "checked");

        if (images.hasImage(name)) {
            writer.println(name + "_.setImageResource(" + images.getImageRef(name) + ");");
        }

        writer.println("addCommand(\"" + Generator.escape(name) + "\", " + name + "_);");
        writer.println();
    }

    private void emitCommandAccessors(SourceWriter writer) {
        for (JMethod method : commandMethods_) {
            String name = method.getName();
            writer.println("public AppCommand " + name + "() {");
            writer.indent();
            writer.println("return " + name + "_;");
            writer.outdent();
            writer.println("}");
        }
    }

    private void setProperty(SourceWriter writer, String name, Element props, String propertyName) {
        if (props == null)
            return;
        // This check is important because getAttribute() returns empty string
        // even if the attribute isn't present, which is not what we want. In
        // the command system, empty string is distinct from null.
        if (!props.hasAttribute(propertyName))
            return;

        String value = props.getAttribute(propertyName);

        String setter = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
        writer.println(name + "_." + setter + "(\"" + Generator.escape(value) + "\");");
    }

    private void setPropertyBool(SourceWriter writer, String name, Element props, String propertyName) {
        if (props == null)
            return;
        // This check is important because getAttribute() returns empty string
        // even if the attribute isn't present, which is not what we want. In
        // the command system, empty string is distinct from null.
        if (!props.hasAttribute(propertyName))
            return;

        String value = props.getAttribute(propertyName);

        String setter = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
        writer.println(name + "_." + setter + "(" + value + ");");
    }

    private ImageResourceInfo generateImageBundle() {
        String className = bundleType_.getSimpleSourceName() + "__AutoGenResources";
        String pathToInstance = packageName_ + "." + className + ".INSTANCE";
        ImageResourceInfo iri = new ImageResourceInfo(pathToInstance);

        PrintWriter printWriter = context_.tryCreate(logger_, packageName_, className);
        if (printWriter == null)
            return null;

        ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(packageName_, className);
        factory.addImport("com.google.gwt.core.client.GWT");
        factory.addImport("com.google.gwt.resources.client.*");
        factory.makeInterface();
        factory.addImplementedInterface("ClientBundle");
        SourceWriter writer = factory.createSourceWriter(context_, printWriter);

        Set<String> resourceNames = context_.getResourcesOracle().getPathNames();
        for (JMethod method : commandMethods_) {
            String commandId = method.getName();

            String key = packageName_.replace('.', '/') + "/" + commandId + ".png";
            if (resourceNames.contains(key)) {
                writer.println("ImageResource " + commandId + "();");
                iri.addImage(commandId);
            }
        }
        writer.println("public static final " + className + " INSTANCE = " + "(" + className + ")GWT.create("
                + className + ".class);");

        writer.outdent();
        writer.println("}");
        context_.commit(logger_, printWriter);

        return iri;

    }

    public Map<String, Element> getCommandProperties() throws UnableToCompleteException {
        Map<String, Element> properties = new HashMap<String, Element>();

        NodeList nodes = getConfigDoc("/commands/cmd");

        for (int i = 0; i < nodes.getLength(); i++) {
            Element cmd = (Element) nodes.item(i);
            String id = cmd.getAttribute("id");

            properties.put(id, cmd);
        }

        return properties;
    }

    private NodeList getConfigDoc(String xpath) throws UnableToCompleteException {
        try {
            String resourceName = bundleType_.getQualifiedSourceName().replace('.', '/') + ".cmd.xml";
            Resource resource = context_.getResourcesOracle().getResource(resourceName);
            if (resource == null)
                return null;

            Object result = XPathFactory.newInstance().newXPath().evaluate(xpath,
                    new InputSource(resource.getLocation()), XPathConstants.NODESET);
            return (NodeList) result;
        } catch (Exception e) {
            logger_.log(TreeLogger.Type.ERROR, "Barf", e);
            throw new UnableToCompleteException();
        }
    }

    private final TreeLogger logger_;
    private final GeneratorContext context_;
    private final JClassType bundleType_;
    private final JMethod[] commandMethods_;
    private final JMethod[] menuMethods_;
    @SuppressWarnings("unused")
    private final JMethod[] shortcutsMethods_;
    private final String packageName_;
    private String simpleName_;
}

class ImageResourceInfo {
    public ImageResourceInfo(String imagesRefPath) {
        this.imagesRefPath_ = imagesRefPath;
    }

    public void addImage(String commandId) {
        imageIds_.add(commandId);
    }

    public boolean hasImage(String commandId) {
        return imageIds_.contains(commandId);
    }

    public String getImageRef(String commandId) {
        assert hasImage(commandId);
        return imagesRefPath_ + "." + commandId + "()";
    }

    private final String imagesRefPath_;
    private final HashSet<String> imageIds_ = new HashSet<String>();
}