/* * 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"); factory.addImport("org.rstudio.core.client.resources.ImageResource2x"); 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"); setProperty(writer, name, props.get(name), "windowMode"); setProperty(writer, name, props.get(name), "context"); // 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), "checkable"); setPropertyBool(writer, name, props.get(name), "checked"); setPropertyBool(writer, name, props.get(name), "rebindable"); if (images.hasImage(name)) { String resourceName = images.getImageRef(name); if (images.hasImage(name + "2x")) resourceName = "new ImageResource2x(" + images.getImageRef(name + "2x") + ")"; writer.println(name + "_.setImageResource(" + resourceName + ");"); } 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; if (resourceNames.contains(key + ".png")) { writer.println("ImageResource " + commandId + "();"); iri.addImage(commandId); } if (resourceNames.contains(key + "_2x.png")) { writer.println("@Source(\"" + commandId + "_2x.png\")"); writer.println("ImageResource " + commandId + "2x();"); iri.addImage(commandId + "2x"); } } 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>(); }