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

com.google.gwt.uibinder.rebind.UiBinderWriter Maven / Gradle / Ivy

/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.uibinder.rebind;

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.JParameter;
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.JTypeParameter;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.TagName;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.DomEvent.Type;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.rg.GssResourceGenerator.GssOptions;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
import com.google.gwt.uibinder.client.LazyDomElement;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.uibinder.client.UiRenderer;
import com.google.gwt.uibinder.client.impl.AbstractUiRenderer;
import com.google.gwt.uibinder.elementparsers.AttributeMessageParser;
import com.google.gwt.uibinder.elementparsers.BeanParser;
import com.google.gwt.uibinder.elementparsers.ElementParser;
import com.google.gwt.uibinder.elementparsers.IsEmptyParser;
import com.google.gwt.uibinder.elementparsers.UiChildParser;
import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
import com.google.gwt.uibinder.rebind.model.HtmlTemplateMethodWriter;
import com.google.gwt.uibinder.rebind.model.HtmlTemplatesWriter;
import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
import com.google.gwt.uibinder.rebind.model.OwnerClass;
import com.google.gwt.uibinder.rebind.model.OwnerField;
import com.google.gwt.user.client.ui.IsRenderable;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.RenderableStamper;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.beans.Introspector;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Writer for UiBinder generated classes.
 */
public class UiBinderWriter implements Statements {

  private static final String SAFE_VAR_PREFIX =
    "somethingUnlikelyToCollideWithParamNamesWefio";

  private static final String UI_RENDERER_DISPATCHER_PREFIX = "UiRendererDispatcherFor";

  private static final String PACKAGE_URI_SCHEME = "urn:import:";

  // TODO(rjrjr) Another place that we need a general anonymous field
  // mechanism
  private static final String CLIENT_BUNDLE_FIELD =
      "clientBundleFieldNameUnlikelyToCollideWithUserSpecifiedFieldOkay";

  public static String asCommaSeparatedList(String... args) {
    StringBuilder b = new StringBuilder();
    for (String arg : args) {
      if (b.length() > 0) {
        b.append(", ");
      }
      b.append(arg);
    }

    return b.toString();
  }

  /**
   * Escape text that will be part of a string literal to be interpreted at
   * runtime as an HTML attribute value.
   */
  public static String escapeAttributeText(String text) {
    text = escapeText(text, false);

    /*
     * Escape single-quotes to make them safe to be interpreted at runtime as an
     * HTML attribute value (for which we by convention use single quotes).
     */
    text = text.replaceAll("'", "'");
    return text;
  }

  /**
   * Escape text that will be part of a string literal to be interpreted at
   * runtime as HTML, optionally preserving whitespace.
   */
  public static String escapeText(String text, boolean preserveWhitespace) {
    // Replace reserved XML characters with entities. Note that we *don't*
    // replace single- or double-quotes here, because they're safe in text
    // nodes.
    text = text.replaceAll("&", "&");
    text = text.replaceAll("<", "<");
    text = text.replaceAll(">", ">");

    if (!preserveWhitespace) {
      text = text.replaceAll("\\s+", " ");
    }

    return escapeTextForJavaStringLiteral(text);
  }

  /**
   * Escape characters that would mess up interpretation of this string as a
   * string literal in generated code (that is, protect \, \n and " ).
   */
  public static String escapeTextForJavaStringLiteral(String text) {
    text = text.replace("\\", "\\\\");
    text = text.replace("\"", "\\\"");
    text = text.replace("\n", "\\n");

    return text;
  }

  /**
   * Returns a list of the given type and all its superclasses and implemented
   * interfaces in a breadth-first traversal.
   *
   * @param type the base type
   * @return a breadth-first collection of its type hierarchy
   */
  static Iterable getClassHierarchyBreadthFirst(JClassType type) {
    LinkedList list = new LinkedList();
    LinkedList q = new LinkedList();

    q.add(type);
    while (!q.isEmpty()) {
      // Pop the front of the queue and add it to the result list.
      JClassType curType = q.removeFirst();
      list.add(curType);

      // Add implemented interfaces to the back of the queue (breadth first,
      // remember?)
      for (JClassType intf : curType.getImplementedInterfaces()) {
        q.add(intf);
      }
      // Add then add superclasses
      JClassType superClass = curType.getSuperclass();
      if (superClass != null) {
        q.add(superClass);
      }
    }

    return list;
  }

  private static String capitalizePropName(String propName) {
    return propName.substring(0, 1).toUpperCase(Locale.ROOT) + propName.substring(1);
  }

  /**
   * Searches for methods named onBrowserEvent in a {@code type}.
   */
  private static JMethod[] findEventMethods(JClassType type) {
    List methods = new ArrayList(Arrays.asList(type.getInheritableMethods()));

    for (Iterator iterator = methods.iterator(); iterator.hasNext();) {
      JMethod jMethod = iterator.next();
      if (!jMethod.getName().equals("onBrowserEvent")) {
        iterator.remove();
      }
    }

    return methods.toArray(new JMethod[methods.size()]);
  }

  /**
   * Scan the base class for the getter methods. Assumes getters begin with
   * "get". See {@link #validateRendererGetters(JClassType)} for a method that
   * guarantees this method will succeed.
   */
  private static List findGetterNames(JClassType owner) {
    List ret = new ArrayList();
    for (JMethod jMethod : owner.getInheritableMethods()) {
      String getterName = jMethod.getName();
      if (getterName.startsWith("get")) {
        ret.add(jMethod);
      }
    }
    return ret;
  }

  /**
   * Scans a class for a method named "render". Returns its parameters except
   * for the first one. See {@link #validateRenderParameters(JClassType)} for a
   * method that guarantees this method will succeed.
   */
  private static JParameter[] findRenderParameters(JClassType owner) {
    JMethod[] methods = owner.getInheritableMethods();
    JMethod renderMethod = null;

    for (JMethod jMethod : methods) {
      if (jMethod.getName().equals("render")) {
        renderMethod = jMethod;
      }
    }

    JParameter[] parameters = renderMethod.getParameters();
    return Arrays.copyOfRange(parameters, 1, parameters.length);
  }

  /**
   * Finds methods annotated with {@code @UiHandler} in a {@code type}.
   */
  private static JMethod[] findUiHandlerMethods(JClassType type) {
    ArrayList result = new ArrayList();
    JMethod[] allMethods = type.getInheritableMethods();

    for (JMethod jMethod : allMethods) {
      if (jMethod.getAnnotation(UiHandler.class) != null) {
        result.add(jMethod);
      }
    }

    return result.toArray(new JMethod[result.size()]);
  }

  private static String formatMethodError(JMethod eventMethod) {
    return "\"" + eventMethod.getReadableDeclaration(true, true, true, true, true) + "\""
        + " of " + eventMethod.getEnclosingType().getQualifiedSourceName();
  }

  /**
   * Determine the field name a getter is trying to retrieve. Assumes getters
   * begin with "get".
   */
  private static String getterToFieldName(String name) {
    String fieldName = name.substring(3);
    return Introspector.decapitalize(fieldName);
  }

  private static String renderMethodParameters(JParameter[] renderParameters) {
    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < renderParameters.length; i++) {
      JParameter parameter = renderParameters[i];
      builder.append("final ");
      builder.append(parameter.getType().getQualifiedSourceName());
      builder.append(" ");
      builder.append(parameter.getName());
      if (i < renderParameters.length - 1) {
        builder.append(", ");
      }
    }

    return builder.toString();
  }

  private final MortalLogger logger;

  /**
   * Class names of parsers for various ui types, keyed by the classname of the
   * UI class they can build.
   */
  private final Map elementParsers = new HashMap();

  private final List initStatements = new ArrayList();
  private final List statements = new ArrayList();
  private final HandlerEvaluator handlerEvaluator;
  private final MessagesWriter messages;
  private final DesignTimeUtils designTime;
  private final Tokenator tokenator = new Tokenator();

  private final String templatePath;
  private final TypeOracle oracle;
  /**
   * The type we have been asked to generated, e.g. MyUiBinder
   */
  private final JClassType baseClass;

  /**
   * The name of the class we're creating, e.g. MyUiBinderImpl
   */
  private final String implClassName;

  private final JClassType uiOwnerType;

  private final JClassType uiRootType;

  private final JClassType isRenderableClassType;

  private final JClassType lazyDomElementClass;

  private final OwnerClass ownerClass;

  private final FieldManager fieldManager;

  private final HtmlTemplatesWriter htmlTemplates;

  private final ImplicitClientBundle bundleClass;

  private final boolean useLazyWidgetBuilders;

  private final boolean useSafeHtmlTemplates;

  private int domId = 0;

  private int fieldIndex;

  private String gwtPrefix;

  private int renderableStamper = 0;

  private String rendered;
  /**
   * Stack of element variable names that have been attached.
   */
  private final LinkedList attachSectionElements = new LinkedList();
  /**
   * Maps from field element name to the temporary attach record variable name.
   */
  private final Map attachedVars = new HashMap();

  private int nextAttachVar = 0;
  /**
   * Stack of statements to be executed after we detach the current attach
   * section.
   */
  private final LinkedList> detachStatementsStack = new LinkedList>();
  private final AttributeParsers attributeParsers;

  private final UiBinderContext uiBinderCtx;

  private final String binderUri;
  private final boolean isRenderer;

  private final ResourceOracle resourceOracle;

  private final GssOptions gssOptions;

  public UiBinderWriter(JClassType baseClass, String implClassName, String templatePath,
      TypeOracle oracle, MortalLogger logger, FieldManager fieldManager,
      MessagesWriter messagesWriter, DesignTimeUtils designTime, UiBinderContext uiBinderCtx,
      boolean useSafeHtmlTemplates, boolean useLazyWidgetBuilders, String binderUri,
      ResourceOracle resourceOracle, GssOptions gssOptions) throws UnableToCompleteException {
    this.baseClass = baseClass;
    this.implClassName = implClassName;
    this.oracle = oracle;
    this.logger = logger;
    this.templatePath = templatePath;
    this.fieldManager = fieldManager;
    this.messages = messagesWriter;
    this.designTime = designTime;
    this.uiBinderCtx = uiBinderCtx;
    this.useSafeHtmlTemplates = useSafeHtmlTemplates;
    this.useLazyWidgetBuilders = useLazyWidgetBuilders;
    this.binderUri = binderUri;
    this.resourceOracle = resourceOracle;
    this.gssOptions = gssOptions;

    this.htmlTemplates = new HtmlTemplatesWriter(fieldManager, logger);

    // Check for possible misuse 'GWT.create(UiBinder.class)'
    JClassType uibinderItself = oracle.findType(UiBinder.class.getCanonicalName());
    if (uibinderItself.equals(baseClass)) {
      die("You must use a subtype of UiBinder in GWT.create(). E.g.,\n"
          + "  interface Binder extends UiBinder {}\n"
          + "  GWT.create(Binder.class);");
    }

    JClassType[] uiBinderTypes = baseClass.getImplementedInterfaces();
    if (uiBinderTypes.length == 0) {
      throw new RuntimeException("No implemented interfaces for " + baseClass.getName());
    }
    JClassType uiBinderType = uiBinderTypes[0];

    JClassType[] typeArgs = uiBinderType.isParameterized() == null
        ? new JClassType[0] : uiBinderType.isParameterized().getTypeArgs();

    String binderType = uiBinderType.getName();

    JClassType uiRendererClass = getOracle().findType(UiRenderer.class.getName());
    if (uiBinderType.isAssignableTo(uibinderItself)) {
      if (typeArgs.length < 2) {
        throw new RuntimeException("Root and owner type parameters are required for type %s"
            + binderType);
      }
      uiRootType = typeArgs[0];
      uiOwnerType = typeArgs[1];
      isRenderer = false;
    } else if (uiBinderType.isAssignableTo(uiRendererClass)) {
      if (typeArgs.length >= 1) {
        throw new RuntimeException("UiRenderer is not a parameterizable type in " + binderType);
      }
      if (!useSafeHtmlTemplates) {
        die("Configuration property UiBinder.useSafeHtmlTemplates\n"
            + "  must be set to true to generate a UiRenderer");
      }
      if (!useLazyWidgetBuilders) {
        die("Configuration property UiBinder.useLazyWidgetBuilders\n"
            + "  must be set to true to generate a UiRenderer");
      }

      // UiRenderers do not need owners but UiBinder generation needs some type here
      uiOwnerType = uiBinderType;
      uiRootType = null;
      isRenderer = true;
    } else {
      die(baseClass.getName() + " must implement UiBinder or UiRenderer");
      // This is unreachable in practice, but silences not initialized errors
      throw new UnableToCompleteException();
    }

    isRenderableClassType = oracle.findType(IsRenderable.class.getCanonicalName());
    lazyDomElementClass = oracle.findType(LazyDomElement.class.getCanonicalName());

    ownerClass = new OwnerClass(uiOwnerType, logger, uiBinderCtx);
    bundleClass =
        new ImplicitClientBundle(baseClass.getPackage().getName(), this.implClassName,
            CLIENT_BUNDLE_FIELD, logger);
    handlerEvaluator = new HandlerEvaluator(ownerClass, logger, oracle, useLazyWidgetBuilders);

    attributeParsers = new AttributeParsers(oracle, fieldManager, logger);
  }

  /**
   * Add a statement to be executed right after the current attached element is
   * detached. This is useful for doing things that might be expensive while the
   * element is attached to the DOM.
   *
   * @param format
   * @param args
   * @see #beginAttachedSection(String)
   */
  public void addDetachStatement(String format, Object... args) {
    detachStatementsStack.getFirst().add(String.format(format, args));
  }

  /**
   * Add a statement to be run after everything has been instantiated, in the
   * style of {@link String#format}.
   */
  public void addInitStatement(String format, Object... params) {
    initStatements.add(formatCode(format, params));
  }

  /**
   * Adds a statement to the block run after fields are declared, in the style
   * of {@link String#format}.
   */
  public void addStatement(String format, Object... args) {
    String code = formatCode(format, args);

    if (useLazyWidgetBuilders) {
      /**
       * I'm intentionally over-simplifying this and assuming that the input
       * comes always in the format: field.somestatement(); Thus, field can be
       * extracted easily and the element parsers don't need to be changed all
       * at once.
       */
      int idx = code.indexOf(".");
      String fieldName = code.substring(0, idx);
      fieldManager.require(fieldName).addStatement(format, args);
    } else {
      statements.add(code);
    }
  }

  /**
   * Begin a section where a new attachable element is being parsed--that is,
   * one that will be constructed as a big innerHTML string, and then briefly
   * attached to the dom to allow fields accessing its to be filled (at the
   * moment, HasHTMLParser, HTMLPanelParser, and DomElementParser.).
   * 

* Succeeding calls made to {@link #ensureAttached} and * {@link #ensureCurrentFieldAttached} must refer to children of this element, * until {@link #endAttachedSection} is called. * * @param element Java expression for the generated code that will return the * dom element to be attached. */ public void beginAttachedSection(String element) { attachSectionElements.addFirst(element); detachStatementsStack.addFirst(new ArrayList()); } /** * Declare a field that will hold an Element instance. Returns a token that * the caller must set as the id attribute of that element in whatever * innerHTML expression will reproduce it at runtime. *

* In the generated code, this token will be replaced by an expression to * generate a unique dom id at runtime. Further code will be generated to be * run after widgets are instantiated, to use that dom id in a getElementById * call and assign the Element instance to its field. * * @param fieldName The name of the field being declared * @param ancestorField The name of fieldName parent */ public String declareDomField(XMLElement source, String fieldName, String ancestorField) throws UnableToCompleteException { ensureAttached(); String name = declareDomIdHolder(fieldName); if (useLazyWidgetBuilders) { // Create and initialize the dom field with LazyDomElement. FieldWriter field = fieldManager.require(fieldName); /** * But if the owner field is an instance of LazyDomElement then the code * can be optimized, no cast is needed and the getter doesn't need to be * called in its ancestral. */ if (isOwnerFieldLazyDomElement(fieldName)) { field.setInitializer(formatCode("new %s(%s)", field.getQualifiedSourceName(), fieldManager.convertFieldToGetter(name))); } else { field.setInitializer(formatCode("new %s(%s).get().cast()", LazyDomElement.class.getCanonicalName(), fieldManager.convertFieldToGetter(name))); // The dom must be created by its ancestor. fieldManager.require(ancestorField).addAttachStatement( fieldManager.convertFieldToGetter(fieldName) + ";"); } } else { setFieldInitializer(fieldName, "null"); addInitStatement("%s = com.google.gwt.dom.client.Document.get().getElementById(%s).cast();", fieldName, name); addInitStatement("%s.removeAttribute(\"id\");", fieldName); } return tokenForStringExpression(source, fieldManager.convertFieldToGetter(name)); } /** * Declare a variable that will be filled at runtime with a unique id, safe * for use as a dom element's id attribute. For {@code UiRenderer} based code, * elements corresponding to a ui:field, need and id initialized to a value * that depends on the {@code fieldName}. For all other cases let * {@code fieldName} be {@code null}. * * @param fieldName name of the field corresponding to this variable. * @return that variable's name. */ public String declareDomIdHolder(String fieldName) throws UnableToCompleteException { String domHolderName = "domId" + domId++; FieldWriter domField = fieldManager.registerField(FieldWriterType.DOM_ID_HOLDER, oracle.findType(String.class.getName()), domHolderName); if (isRenderer && fieldName != null) { domField.setInitializer("buildInnerId(\"" + fieldName + "\", uiId)"); } else { domField.setInitializer("com.google.gwt.dom.client.Document.get().createUniqueId()"); } return domHolderName; } /** * If this element has a gwt:field attribute, create a field for it of the * appropriate type, and return the field name. If no gwt:field attribute is * found, do nothing and return null * * @return The new field name, or null if no field is created */ public String declareFieldIfNeeded(XMLElement elem) throws UnableToCompleteException { String fieldName = getFieldName(elem); if (fieldName != null) { /** * We can switch types if useLazyWidgetBuilders is enabled and the * respective owner field is a LazyDomElement. */ if (useLazyWidgetBuilders && isOwnerFieldLazyDomElement(fieldName)) { fieldManager.registerFieldForLazyDomElement(findFieldType(elem), ownerClass.getUiField(fieldName)); } else { fieldManager.registerField(findFieldType(elem), fieldName); } } return fieldName; } /** * Declare a {@link RenderableStamper} instance that will be filled at runtime * with a unique token. This instance can then be used to stamp a single * {@link IsRenderable}. * * @return that variable's name. */ public String declareRenderableStamper() throws UnableToCompleteException { String renderableStamperName = "renderableStamper" + renderableStamper++; FieldWriter domField = fieldManager.registerField(FieldWriterType.RENDERABLE_STAMPER, oracle.findType(RenderableStamper.class.getName()), renderableStamperName); domField.setInitializer(formatCode( "new %s(com.google.gwt.dom.client.Document.get().createUniqueId())", RenderableStamper.class.getName())); return renderableStamperName; } /** * Writes a new SafeHtml template to the generated BinderImpl. * * @return The invocation of the SafeHtml template function with the arguments * filled in */ public String declareTemplateCall(String html, String fieldName) throws IllegalArgumentException { if (!useSafeHtmlTemplates) { return '"' + html + '"'; } FieldWriter w = fieldManager.lookup(fieldName); HtmlTemplateMethodWriter templateMethod = htmlTemplates.addSafeHtmlTemplate(html, tokenator); if (useLazyWidgetBuilders) { w.setHtml(templateMethod.getIndirectTemplateCall()); } else { w.setHtml(templateMethod.getDirectTemplateCall()); } return w.getHtml(); } /** * Given a string containing tokens returned by * {@link #tokenForStringExpression}, {@link #tokenForSafeHtmlExpression} or * {@link #declareDomField}, return a string with those tokens replaced by the * appropriate expressions. (It is not normally necessary for an * {@link XMLElement.Interpreter} or {@link ElementParser} to make this call, * as the tokens are typically replaced by the TemplateWriter itself.) */ public String detokenate(String betokened) { return tokenator.detokenate(betokened); } /** * Post an error message and halt processing. This method always throws an * {@link UnableToCompleteException} */ public void die(String message) throws UnableToCompleteException { logger.die(message); } /** * Post an error message and halt processing. This method always throws an * {@link UnableToCompleteException} */ public void die(String message, Object... params) throws UnableToCompleteException { logger.die(message, params); } /** * Post an error message about a specific XMLElement and halt processing. This * method always throws an {@link UnableToCompleteException} */ public void die(XMLElement context, String message, Object... params) throws UnableToCompleteException { logger.die(context, message, params); } /** * End the current attachable section. This will detach the element if it was * ever attached and execute any detach statements. * * @see #beginAttachedSection(String) */ public void endAttachedSection() { String elementVar = attachSectionElements.removeFirst(); List detachStatements = detachStatementsStack.removeFirst(); if (attachedVars.containsKey(elementVar)) { String attachedVar = attachedVars.remove(elementVar); addInitStatement("%s.detach();", attachedVar); for (String statement : detachStatements) { addInitStatement(statement); } } } /** * Ensure that the specified element is attached to the DOM. * * @see #beginAttachedSection(String) */ public void ensureAttached() { String attachSectionElement = attachSectionElements.getFirst(); if (!attachedVars.containsKey(attachSectionElement)) { String attachedVar = "attachRecord" + nextAttachVar; addInitStatement("UiBinderUtil.TempAttachment %s = UiBinderUtil.attachToDom(%s);", attachedVar, attachSectionElement); attachedVars.put(attachSectionElement, attachedVar); nextAttachVar++; } } /** * Ensure that the specified field is attached to the DOM. The field must hold * an object that responds to Element getElement(). Convenience wrapper for * {@link #ensureAttached}(field + ".getElement()"). * * @see #beginAttachedSection(String) */ public void ensureCurrentFieldAttached() { ensureAttached(); } /** * Finds the JClassType that corresponds to this XMLElement, which must be a * Widget or an Element. * * @throws UnableToCompleteException If no such widget class exists * @throws RuntimeException if asked to handle a non-widget, non-DOM element */ public JClassType findFieldType(XMLElement elem) throws UnableToCompleteException { String tagName = elem.getLocalName(); if (!isImportedElement(elem)) { return findDomElementTypeForTag(tagName); } String ns = elem.getNamespaceUri(); String packageName = ns.substring(PACKAGE_URI_SCHEME.length()); String className = tagName; while (true) { JClassType rtn = getOracle().findType(packageName + "." + className); if (rtn != null) { return rtn; } // Try again: shift one element of the class name onto the package name. // If the class name has only one element left, fail. int index = className.indexOf("."); if (index == -1) { die(elem, "No class matching \"%s\" in %s", tagName, ns); } packageName = packageName + "." + className.substring(0, index); className = className.substring(index + 1); } } /** * Generates the code to set a property value (assumes that 'value' is a valid * Java expression). */ public void genPropertySet(String fieldName, String propName, String value) { addStatement("%1$s.set%2$s(%3$s);", fieldName, capitalizePropName(propName), value); } /** * Generates the code to set a string property. */ public void genStringPropertySet(String fieldName, String propName, String value) { genPropertySet(fieldName, propName, "\"" + value + "\""); } /** * The type we have been asked to generated, e.g. MyUiBinder */ public JClassType getBaseClass() { return baseClass; } public ImplicitClientBundle getBundleClass() { return bundleClass; } /** * Returns the {@link DesignTimeUtils}, not null. */ public DesignTimeUtils getDesignTime() { return designTime; } public FieldManager getFieldManager() { return fieldManager; } /** * Returns the logger, at least until we get get it handed off to parsers via * constructor args. */ public MortalLogger getLogger() { return logger; } /** * Get the {@link MessagesWriter} for this UI, generating it if necessary. */ public MessagesWriter getMessages() { return messages; } /** * Gets the type oracle. */ public TypeOracle getOracle() { return oracle; } public OwnerClass getOwnerClass() { return ownerClass; } public String getUiFieldAttributeName() { return gwtPrefix + ":field"; } public boolean isBinderElement(XMLElement elem) { String uri = elem.getNamespaceUri(); return uri != null && binderUri.equals(uri); } public boolean isElementAssignableTo(XMLElement elem, Class possibleSuperclass) throws UnableToCompleteException { JClassType classType = oracle.findType(possibleSuperclass.getCanonicalName()); return isElementAssignableTo(elem, classType); } public boolean isElementAssignableTo(XMLElement elem, JClassType possibleSupertype) throws UnableToCompleteException { /* * Things like */ JTypeParameter typeParameter = possibleSupertype.isTypeParameter(); if (typeParameter != null) { JClassType[] bounds = typeParameter.getBounds(); for (JClassType bound : bounds) { if (!isElementAssignableTo(elem, bound)) { return false; } } return true; } /* * Binder fields are always declared raw, so we're cheating if the user is * playing with parameterized types. We're happy enough if the raw types * match, and rely on them to make sure the specific types really do work. */ JParameterizedType parameterized = possibleSupertype.isParameterized(); if (parameterized != null) { return isElementAssignableTo(elem, parameterized.getRawType()); } JClassType fieldtype = findFieldType(elem); if (fieldtype == null) { return false; } return fieldtype.isAssignableTo(possibleSupertype); } public boolean isImportedElement(XMLElement elem) { String uri = elem.getNamespaceUri(); return uri != null && uri.startsWith(PACKAGE_URI_SCHEME); } /** * Checks whether the given owner field name is a LazyDomElement or not. */ public boolean isOwnerFieldLazyDomElement(String fieldName) { OwnerField ownerField = ownerClass.getUiField(fieldName); if (ownerField == null) { return false; } return lazyDomElementClass.isAssignableFrom(ownerField.getType().getRawType()); } public boolean isRenderableElement(XMLElement elem) throws UnableToCompleteException { return findFieldType(elem).isAssignableTo(isRenderableClassType); } public boolean isRenderer() { return isRenderer; } public boolean isWidgetElement(XMLElement elem) throws UnableToCompleteException { return isElementAssignableTo(elem, IsWidget.class); } /** * Parses the object associated with the specified element, and returns the * field writer that will hold it. The element is likely to make recursive * calls back to this method to have its children parsed. * * @param elem the xml element to be parsed * @return the field holder just created */ public FieldWriter parseElementToField(XMLElement elem) throws UnableToCompleteException { if (elementParsers.isEmpty()) { registerParsers(); } // Get the class associated with this element. JClassType type = findFieldType(elem); // Declare its field. FieldWriter field = declareField(elem, type.getQualifiedSourceName()); /* * Push the field that will hold this widget on top of the parsedFieldStack * to ensure that fields registered by its parsers will be noted as * dependencies of the new widget. (See registerField.) Also push the * element being parsed, so that the fieldManager can hold that info for * later error reporting when field reference left hand sides are validated. */ fieldManager.push(elem, field); // Give all the parsers a chance to generate their code. for (ElementParser parser : getParsersForClass(type)) { parser.parse(elem, field.getName(), type, this); } fieldManager.pop(); return field; } /** * Gives the writer the initializer to use for this field instead of the * default GWT.create call. * * @throws IllegalStateException if an initializer has already been set */ public void setFieldInitializer(String fieldName, String factoryMethod) { fieldManager.lookup(fieldName).setInitializer(factoryMethod); } /** * Instructs the writer to initialize the field with a specific constructor * invocation, instead of the default GWT.create call. * * @param fieldName the field to initialize * @param args arguments to the constructor call */ public void setFieldInitializerAsConstructor(String fieldName, String... args) { JClassType assignableType = fieldManager.lookup(fieldName).getAssignableType(); setFieldInitializer(fieldName, formatCode("new %s(%s)", assignableType.getQualifiedSourceName(), asCommaSeparatedList(args))); } /** * Like {@link #tokenForStringExpression}, but used for runtime expressions * that we trust to be safe to interpret at runtime as HTML without escaping, * like translated messages with simple formatting. Wrapped in a call to * {@link com.google.gwt.safehtml.shared.SafeHtmlUtils#fromSafeConstant} to * keep the expression from being escaped by the SafeHtml template. * * @param expression must resolve to trusted HTML string */ public String tokenForSafeConstant(XMLElement source, String expression) { if (!useSafeHtmlTemplates) { return tokenForStringExpression(source, expression); } expression = "SafeHtmlUtils.fromSafeConstant(" + expression + ")"; htmlTemplates.noteSafeConstant(expression); return nextToken(source, expression); } /** * Like {@link #tokenForStringExpression}, but used for runtime * {@link com.google.gwt.safehtml.shared.SafeHtml SafeHtml} instances. * * @param expression must resolve to SafeHtml object */ public String tokenForSafeHtmlExpression(XMLElement source, String expression) { if (!useSafeHtmlTemplates) { return tokenForStringExpression(source, expression + ".asString()"); } htmlTemplates.noteSafeConstant(expression); return nextToken(source, expression); } /** * Like {@link #tokenForStringExpression}, but used for runtime * {@link com.google.gwt.safehtml.shared.SafeUri SafeUri} instances. * * @param expression must resolve to SafeUri object */ public String tokenForSafeUriExpression(XMLElement source, String expression) { if (!useSafeHtmlTemplates) { return tokenForStringExpression(source, expression); } htmlTemplates.noteUri(expression); return nextToken(source, expression); } /** * Returns a string token that can be used in place the given expression * inside any string literals. Before the generated code is written, the * expression will be stitched back into the generated code in place of the * token, surrounded by plus signs. This is useful in strings to be handed to * setInnerHTML() and setText() calls, to allow a unique dom id attribute or * other runtime expression in the string. * * @param expression must resolve to String */ public String tokenForStringExpression(XMLElement source, String expression) { return nextToken(source, "\" + " + expression + " + \""); } public boolean useLazyWidgetBuilders() { return useLazyWidgetBuilders; } /** * @return true of SafeHtml integration is in effect */ public boolean useSafeHtmlTemplates() { return useSafeHtmlTemplates; } /** * Post a warning message. */ public void warn(String message) { logger.warn(message); } /** * Post a warning message. */ public void warn(String message, Object... params) { logger.warn(message, params); } /** * Post a warning message. */ public void warn(XMLElement context, String message, Object... params) { logger.warn(context, message, params); } /** * Entry point for the code generation logic. It generates the * implementation's superstructure, and parses the root widget (leading to all * of its children being parsed as well). * * @param doc TODO */ void parseDocument(Document doc, PrintWriter printWriter) throws UnableToCompleteException { Element documentElement = doc.getDocumentElement(); gwtPrefix = documentElement.lookupPrefix(binderUri); XMLElement elem = new XMLElementProviderImpl(attributeParsers, oracle, logger, designTime).get(documentElement); this.rendered = tokenator.detokenate(parseDocumentElement(elem)); printWriter.print(rendered); } private void addElementParser(String gwtClass, String parser) { elementParsers.put(gwtClass, parser); } private void addWidgetParser(String className) { String gwtClass = "com.google.gwt.user.client.ui." + className; String parser = "com.google.gwt.uibinder.elementparsers." + className + "Parser"; addElementParser(gwtClass, parser); } /** * Declares a field of the given type name, returning the name of the declared * field. If the element has a field or id attribute, use its value. * Otherwise, create and return a new, private field name for it. */ private FieldWriter declareField(XMLElement source, String typeName) throws UnableToCompleteException { JClassType type = oracle.findType(typeName); if (type == null) { die(source, "Unknown type %s", typeName); } String fieldName = getFieldName(source); if (fieldName == null) { // TODO(rjrjr) could collide with user declared name, as is // also a worry in HandlerEvaluator. Need a general scheme for // anonymous fields. See the note in HandlerEvaluator and do // something like that, but in FieldManager. fieldName = "f_" + source.getLocalName() + ++fieldIndex; } fieldName = normalizeFieldName(fieldName); return fieldManager.registerField(type, fieldName); } private void dieGettingEventTypeName(JMethod jMethod, Exception e) throws UnableToCompleteException { die("Could not obtain DomEvent.Type object for first parameter of %s (%s)", formatMethodError(jMethod), e.getMessage()); } /** * Ensures that all of the internal data structures are cleaned up correctly * at the end of parsing the document. * * @throws IllegalStateException */ private void ensureAttachmentCleanedUp() { if (!attachSectionElements.isEmpty()) { throw new IllegalStateException("Attachments not cleaned up: " + attachSectionElements); } if (!detachStatementsStack.isEmpty()) { throw new IllegalStateException("Detach not cleaned up: " + detachStatementsStack); } } /** * Add call to {@code com.google.gwt.resources.client.CssResource#ensureInjected()} * on each CSS resource field. */ private void ensureInjectedCssFields() { for (ImplicitCssResource css : bundleClass.getCssMethods()) { String fieldName = css.getName(); FieldWriter cssField = fieldManager.require(fieldName); cssField.addStatement("%s.ensureInjected();", fieldName); } } /** * Evaluate whether all @UiField attributes are also defined in the template. * Dies if not. */ private void evaluateUiFields() throws UnableToCompleteException { if (designTime.isDesignTime()) { return; } for (OwnerField ownerField : getOwnerClass().getUiFields()) { String fieldName = ownerField.getName(); FieldWriter fieldWriter = fieldManager.lookup(fieldName); if (fieldWriter == null) { die("Template %s has no %s attribute for %s.%s#%s", templatePath, getUiFieldAttributeName(), uiOwnerType.getPackage().getName(), uiOwnerType.getName(), fieldName); } } } /** * Given a DOM tag name, return the corresponding JSO subclass. */ private JClassType findDomElementTypeForTag(String tag) { JClassType elementClass = oracle.findType("com.google.gwt.dom.client.Element"); JClassType[] types = elementClass.getSubtypes(); for (JClassType type : types) { TagName annotation = type.getAnnotation(TagName.class); if (annotation != null) { for (String annotationTag : annotation.value()) { if (annotationTag.equals(tag)) { return type; } } } } return elementClass; } /** * Calls {@code getType().getName()} on subclasses of {@code DomEvent}. */ private String findEventTypeName(JMethod jMethod) throws UnableToCompleteException { // Get the event class name (i.e. ClickEvent) String eventTypeName = jMethod.getParameterTypes()[0].getQualifiedSourceName(); Class domType; // Get the class instance try { domType = Class.forName(eventTypeName); } catch (ClassNotFoundException e) { die("Could not find type %s in %s", eventTypeName, formatMethodError(jMethod)); return null; } // Reflectively obtain the type (i.e. ClickEvent.getType()) try { return ((Type) domType.getMethod("getType", (Class[]) null).invoke(null, (Object[]) null)).getName(); } catch (IllegalArgumentException e) { dieGettingEventTypeName(jMethod, e); } catch (SecurityException e) { dieGettingEventTypeName(jMethod, e); } catch (IllegalAccessException e) { dieGettingEventTypeName(jMethod, e); } catch (InvocationTargetException e) { dieGettingEventTypeName(jMethod, e); } catch (NoSuchMethodException e) { dieGettingEventTypeName(jMethod, e); } // Unreachable, but appeases the compiler return null; } /** * Use this method to format code. It forces the use of the en-US locale, so * that things like decimal format don't get mangled. */ private String formatCode(String format, Object... params) { String r = String.format(Locale.US, format, params); return r; } /** * Inspects this element for a gwt:field attribute. If one is found, the * attribute is consumed and its value returned. * * @return The field name declared by an element, or null if none is declared */ private String getFieldName(XMLElement elem) throws UnableToCompleteException { String fieldName = null; boolean hasOldSchoolId = false; if (elem.hasAttribute("id") && isWidgetElement(elem)) { hasOldSchoolId = true; // If an id is specified on the element, use that. fieldName = elem.consumeRawAttribute("id"); warn(elem, "Deprecated use of id=\"%1$s\" for field name. " + "Please switch to gwt:field=\"%1$s\" instead. " + "This will soon be a compile error!", fieldName); } if (elem.hasAttribute(getUiFieldAttributeName())) { if (hasOldSchoolId) { die(elem, "Cannot declare both id and field on the same element"); } fieldName = elem.consumeRawAttribute(getUiFieldAttributeName()); } return fieldName; } private Class getParserForClass(JClassType uiClass) { // Find the associated parser. String uiClassName = uiClass.getQualifiedSourceName(); String parserClassName = elementParsers.get(uiClassName); if (parserClassName == null) { return null; } // And instantiate it. try { return Class.forName(parserClassName).asSubclass(ElementParser.class); } catch (ClassNotFoundException e) { throw new RuntimeException("Unable to instantiate parser", e); } catch (ClassCastException e) { throw new RuntimeException(parserClassName + " must extend ElementParser"); } } /** * Find a set of element parsers for the given ui type. * * The list of parsers will be returned in order from most- to least-specific. */ private Iterable getParsersForClass(JClassType type) { List parsers = new ArrayList(); /* * Let this non-widget parser go first (it finds elements). * Any other such should land here too. * * TODO(rjrjr) Need a scheme to associate these with a namespace uri or * something? */ parsers.add(new AttributeMessageParser()); parsers.add(new UiChildParser(uiBinderCtx)); for (JClassType curType : getClassHierarchyBreadthFirst(type)) { try { Class cls = getParserForClass(curType); if (cls != null) { ElementParser parser = cls.newInstance(); parsers.add(parser); } } catch (InstantiationException e) { throw new RuntimeException("Unable to instantiate " + curType.getName(), e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to instantiate " + curType.getName(), e); } } parsers.add(new BeanParser(uiBinderCtx)); parsers.add(new IsEmptyParser()); return parsers; } /** * Writes a field setter if the field is not provided and the field class is * compatible with its respective template field. */ private void maybeWriteFieldSetter(IndentedWriter niceWriter, OwnerField ownerField, JClassType templateClass, String templateField) throws UnableToCompleteException { JClassType fieldType = ownerField.getType().getRawType(); if (!ownerField.isProvided()) { /* * Normally check that the type the template created can be slammed into * the @UiField annotated field in the owning class */ if (!templateClass.isAssignableTo(fieldType)) { die("In @UiField %s, template field and owner field types don't match: %s is not assignable to %s", ownerField.getName(), templateClass.getQualifiedSourceName(), fieldType.getQualifiedSourceName()); } /* * And initialize the field */ niceWriter.write("owner.%1$s = %2$s;", ownerField.getName(), templateField); } else { /* * But with @UiField(provided=true) the user builds it, so reverse the * direction of the assignability check and do no init. */ if (!fieldType.isAssignableTo(templateClass)) { die("In UiField(provided = true) %s, template field and field types don't match: " + "@UiField(provided=true)%s is not assignable to %s", ownerField.getName(), fieldType.getQualifiedSourceName(), templateClass.getQualifiedSourceName()); } } } private String nextToken(XMLElement source, String expression) { String nextToken = tokenator.nextToken(source, expression); return nextToken; } private String normalizeFieldName(String fieldName) { // If a field name has a '.' in it, replace it with '$' to make it a legal // identifier. This can happen with the field names associated with nested // classes. return fieldName.replace('.', '$'); } /** * Parse the document element and return the source of the Java class that * will implement its UiBinder. */ private String parseDocumentElement(XMLElement elem) throws UnableToCompleteException { fieldManager.registerFieldOfGeneratedType(oracle.findType(ClientBundle.class.getName()), bundleClass.getPackageName(), bundleClass.getClassName(), bundleClass.getFieldName()); // Allow GWT.create() to init the field, the default behavior FieldWriter rootField = new UiBinderParser(this, messages, fieldManager, oracle, bundleClass, binderUri, uiBinderCtx, resourceOracle, gssOptions).parse(elem); fieldManager.validate(); StringWriter stringWriter = new StringWriter(); IndentedWriter niceWriter = new IndentedWriter(new PrintWriter(stringWriter)); if (isRenderer) { ensureInjectedCssFields(); writeRenderer(niceWriter, rootField); } else if (useLazyWidgetBuilders) { ensureInjectedCssFields(); writeBinderForRenderableStrategy(niceWriter, rootField); } else { writeBinder(niceWriter, rootField); } ensureAttachmentCleanedUp(); return stringWriter.toString(); } private void registerParsers() { // TODO(rjrjr): Allow third-party parsers to register themselves // automagically addElementParser("com.google.gwt.dom.client.Element", "com.google.gwt.uibinder.elementparsers.DomElementParser"); // Register widget parsers. addWidgetParser("UIObject"); addWidgetParser("HasText"); addWidgetParser("HasHTML"); addWidgetParser("HasTreeItems"); addWidgetParser("HasWidgets"); addWidgetParser("HTMLPanel"); addWidgetParser("FlowPanel"); addWidgetParser("AbsolutePanel"); addWidgetParser("DockPanel"); addWidgetParser("StackPanel"); addWidgetParser("DisclosurePanel"); addWidgetParser("TabPanel"); addWidgetParser("MenuItem"); addWidgetParser("MenuBar"); addWidgetParser("CellPanel"); addWidgetParser("CustomButton"); addWidgetParser("DialogBox"); addWidgetParser("LayoutPanel"); addWidgetParser("DockLayoutPanel"); addWidgetParser("StackLayoutPanel"); addWidgetParser("TabLayoutPanel"); addWidgetParser("Image"); addWidgetParser("ListBox"); addWidgetParser("Grid"); addWidgetParser("HasAlignment"); addWidgetParser("DateLabel"); addWidgetParser("NumberLabel"); if (useLazyWidgetBuilders) { addWidgetParser("LazyPanel"); addWidgetParser("RenderablePanel"); } } /** * Validates each {@code eventMethod} (e.g. {@code onBrowserEvent(HandlerType o, NativeEvent e, * Element parent, A a, B b, ...)}). *

    *
  • The second parameter type is {@code NativeEvent} *
  • The third parameter type is {@code Element} *
  • All the handler methods in the type of the first parameter * (any methods annotated with {@code @UiHandler}) * have a signature compatible with the {@code eventMethod} *
*/ private void validateEventMethod(JMethod eventMethod) throws UnableToCompleteException { JParameter[] parameters = eventMethod.getParameters(); if (parameters.length < 3) { die("Too few parameters in %s", formatMethodError(eventMethod)); } String nativeEventName = NativeEvent.class.getCanonicalName(); JClassType nativeEventType = oracle.findType(nativeEventName); if (!nativeEventType.equals(parameters[1].getType())) { die("Second parameter must be of type %s in %s", nativeEventName, formatMethodError(eventMethod)); } String elementName = com.google.gwt.dom.client.Element.class.getCanonicalName(); JClassType elementType = oracle.findType(elementName); if (!elementType.equals(parameters[2].getType())) { die("Third parameter must be of type %s in %s", elementName, formatMethodError(eventMethod)); } if (parameters[0].getType().isClassOrInterface() == null) { die("First parameter must be a class or interface in %s", formatMethodError(eventMethod)); } JClassType eventReceiver = parameters[0].getType().isClassOrInterface(); validateEventReceiver(parameters, eventReceiver, eventMethod); } /** * Validates the signature of all methods annotated with {@code @UiHandler} * in the {@code eventReceiver} type. All event handlers must have the same signature * where: *
    *
  • The annotation must list valid {@code ui:field}s *
  • The first parameter must be assignable to * {@link com.google.gwt.event.dom.client.DomEvent DomEvent} *
  • If present, the second parameter must be of type * {@link com.google.gwt.dom.client.Element Element} *
  • For all other parameters in position {@code n} must be of the same type as * {@code parameters[n + 1]} *
*/ private void validateEventReceiver(JParameter[] onBrowserEventParameters, JClassType eventReceiver, JMethod sourceMethod) throws UnableToCompleteException { // Pre-compute the expected parameter types (after the first one, that is) JType[] onBrowserEventParamTypes = new JType[onBrowserEventParameters.length - 2]; // If present, second parameter must be an Element onBrowserEventParamTypes[0] = oracle.findType(com.google.gwt.dom.client.Element.class .getCanonicalName()); // And the rest must be the same type for (int i = 3; i < onBrowserEventParameters.length; i++) { onBrowserEventParamTypes[i - 2] = onBrowserEventParameters[i].getType(); } for (JMethod jMethod : eventReceiver.getInheritableMethods()) { Class annotationClass = UiHandler.class; UiHandler annotation = jMethod.getAnnotation(annotationClass); // Ignore methods not annotated with @UiHandler if (annotation == null) { continue; } // Are the fields in @UiHandler known? String[] fields = annotation.value(); if (fields == null) { die("@UiHandler returns null from its value in %s", formatMethodError(jMethod)); } for (String fieldName : fields) { FieldWriter field = fieldManager.lookup(fieldName); if (field == null) { die("\"%s\" is not a known field name as listed in the @UiHandler annotation in %s", fieldName, formatMethodError(jMethod)); } } // First parameter JParameter[] eventHandlerParameters = jMethod.getParameters(); JClassType domEventType = oracle.findType(DomEvent.class.getCanonicalName()); JClassType firstParamType = eventHandlerParameters[0].getType().isClassOrInterface(); if (firstParamType == null || !firstParamType.isAssignableTo(domEventType)) { die("First parameter must be assignable to com.google.gwt.dom.client.DomEvent in %s", formatMethodError(jMethod)); } // All others if (onBrowserEventParamTypes.length < eventHandlerParameters.length - 1) { die("Too many parameters in %s", formatMethodError(jMethod)); } for (int i = 1; i < eventHandlerParameters.length; i++) { if (!eventHandlerParameters[i].getType().equals(onBrowserEventParamTypes[i - 1])) { die("Parameter %s in %s is not of the same type as parameter %s in %s", eventHandlerParameters[i].getName(), formatMethodError(jMethod), onBrowserEventParameters[i + 1].getName(), formatMethodError(sourceMethod)); } } } } /** * Scan the base class for the getter methods. Assumes getters begin with * "get" and validates that each corresponds to a field declared with * {@code ui:field}. If the getter return type is assignable to * {@code Element}, the getter must have a single parameter and the parameter * must be assignable to {@code Element}. If the getter return type is assignable * to {@code com.google.gwt.resources.client.CssResource}, the getter must * have no parameters. */ private void validateRendererGetters(JClassType owner) throws UnableToCompleteException { for (JMethod jMethod : owner.getInheritableMethods()) { String getterName = jMethod.getName(); if (getterName.startsWith("get")) { String fieldName = getterToFieldName(getterName); FieldWriter field = fieldManager.lookup(fieldName); if (field == null || (!FieldWriterType.DEFAULT.equals(field.getFieldType()) && !FieldWriterType.GENERATED_CSS.equals(field.getFieldType()))) { die("%s does not match a \"ui:field='%s'\" declaration in %s, " + "or '%s' refers to something other than a ui:style" + " or an HTML element in the template", getterName, fieldName, owner.getQualifiedSourceName(), fieldName); } if (FieldWriterType.DEFAULT.equals(field.getFieldType()) && jMethod.getParameterTypes().length != 1) { die("Field getter %s must have exactly one parameter in %s", getterName, owner.getQualifiedSourceName()); } else if (FieldWriterType.GENERATED_CSS.equals(field.getFieldType()) && jMethod.getParameterTypes().length != 0) { die("Style getter %s must have no parameters in %s", getterName, owner.getQualifiedSourceName()); } else if (jMethod.getParameterTypes().length == 1) { String elementClassName = com.google.gwt.dom.client.Element.class.getCanonicalName(); JClassType elementType = oracle.findType(elementClassName); JClassType getterParamType = jMethod.getParameterTypes()[0].getErasedType().isClassOrInterface(); if (!elementType.isAssignableFrom(getterParamType)) { die("Getter %s must have exactly one parameter of type assignable to %s in %s", getterName, elementClassName, owner.getQualifiedSourceName()); } } } else if (!getterName.equals("render") && !getterName.equals("onBrowserEvent") && !getterName.equals("isParentOrRenderer")) { die("Unexpected method \"%s\" found in %s", getterName, owner.getQualifiedSourceName()); } } } /** * Scans a class to validate that it contains a single method called render, * which has a {@code void} return type, and its first parameter is of type * {@code SafeHtmlBuilder}. */ private void validateRenderParameters(JClassType owner) throws UnableToCompleteException { JMethod[] methods = owner.getInheritableMethods(); JMethod renderMethod = null; for (JMethod jMethod : methods) { if (jMethod.getName().equals("render")) { if (renderMethod == null) { renderMethod = jMethod; } else { die("%s declares more than one method named render", owner.getQualifiedSourceName()); } } } if (renderMethod == null || renderMethod.getParameterTypes().length < 1 || !renderMethod.getParameterTypes()[0].getErasedType().getQualifiedSourceName().equals( SafeHtmlBuilder.class.getCanonicalName())) { die("%s does not declare a render(SafeHtmlBuilder ...) method", owner.getQualifiedSourceName()); } if (!JPrimitiveType.VOID.equals(renderMethod.getReturnType())) { die("%s#render(SafeHtmlBuilder ...) does not return void", owner.getQualifiedSourceName()); } } /** * Write statements that parsers created via calls to {@link #addStatement}. * Such statements will assume that {@link #writeGwtFields} has already been * called. */ private void writeAddedStatements(IndentedWriter niceWriter) { for (String s : statements) { niceWriter.write(s); } } /** * Writes the UiBinder's source. */ private void writeBinder(IndentedWriter w, FieldWriter rootField) throws UnableToCompleteException { writePackage(w); writeImports(w); w.newline(); writeClassOpen(w); writeStatics(w); w.newline(); // Create SafeHtml Template writeTemplatesInterface(w); w.newline(); // createAndBindUi method w.write("public %s createAndBindUi(final %s owner) {", uiRootType.getParameterizedQualifiedSourceName(), uiOwnerType.getParameterizedQualifiedSourceName()); w.indent(); w.newline(); writeGwtFields(w); w.newline(); designTime.writeAttributes(this); writeAddedStatements(w); w.newline(); writeInitStatements(w); w.newline(); writeHandlers(w); w.newline(); writeOwnerFieldSetters(w); writeCssInjectors(w); w.write("return %s;", rootField.getNextReference()); w.outdent(); w.write("}"); // Close class w.outdent(); w.write("}"); } /** * Writes a different optimized UiBinder's source for the renderable strategy. */ private void writeBinderForRenderableStrategy(IndentedWriter w, FieldWriter rootField) throws UnableToCompleteException { writePackage(w); writeImports(w); w.newline(); writeClassOpen(w); writeStatics(w); w.newline(); writeTemplatesInterface(w); w.newline(); // createAndBindUi method w.write("public %s createAndBindUi(final %s owner) {", uiRootType.getParameterizedQualifiedSourceName(), uiOwnerType.getParameterizedQualifiedSourceName()); w.indent(); w.newline(); designTime.writeAttributes(this); w.newline(); w.write("return new Widgets(owner).%s;", rootField.getNextReference()); w.outdent(); w.write("}"); // Writes the inner class Widgets. w.newline(); w.write("/**"); w.write(" * Encapsulates the access to all inner widgets"); w.write(" */"); w.write("class Widgets {"); w.indent(); String ownerClassType = uiOwnerType.getParameterizedQualifiedSourceName(); w.write("private final %s owner;", ownerClassType); w.newline(); writeHandlers(w); w.newline(); w.write("public Widgets(final %s owner) {", ownerClassType); w.indent(); w.write("this.owner = owner;"); fieldManager.initializeWidgetsInnerClass(w, getOwnerClass()); w.outdent(); w.write("}"); w.newline(); htmlTemplates.writeTemplateCallers(w); evaluateUiFields(); fieldManager.writeFieldDefinitions(w, getOracle(), getOwnerClass(), getDesignTime()); w.outdent(); w.write("}"); // Close class w.outdent(); w.write("}"); } private void writeClassOpen(IndentedWriter w) { if (!isRenderer) { w.write("public class %s implements UiBinder<%s, %s>, %s {", implClassName, uiRootType.getParameterizedQualifiedSourceName(), uiOwnerType.getParameterizedQualifiedSourceName(), baseClass.getParameterizedQualifiedSourceName()); } else { w.write("public class %s extends %s implements %s {", implClassName, AbstractUiRenderer.class.getName(), baseClass.getParameterizedQualifiedSourceName()); } w.indent(); } private void writeCssInjectors(IndentedWriter w) { for (ImplicitCssResource css : bundleClass.getCssMethods()) { w.write("%s.%s().ensureInjected();", bundleClass.getFieldName(), css.getName()); } w.newline(); } /** * Write declarations for variables or fields to hold elements declared with * gwt:field in the template. For those that have not had constructor * generation suppressed, emit GWT.create() calls instantiating them (or die * if they have no default constructor). * * @throws UnableToCompleteException on constructor problem */ private void writeGwtFields(IndentedWriter niceWriter) throws UnableToCompleteException { // For each provided field in the owner class, initialize from the owner Collection ownerFields = getOwnerClass().getUiFields(); for (OwnerField ownerField : ownerFields) { if (ownerField.isProvided()) { String fieldName = ownerField.getName(); FieldWriter fieldWriter = fieldManager.lookup(fieldName); // TODO why can this be null? if (fieldWriter != null) { String initializer; if (designTime.isDesignTime()) { String typeName = ownerField.getType().getRawType().getQualifiedSourceName(); initializer = designTime.getProvidedField(typeName, ownerField.getName()); } else { initializer = formatCode("owner.%1$s", fieldName); } fieldManager.lookup(fieldName).setInitializer(initializer); } } } fieldManager.writeGwtFieldsDeclaration(niceWriter); } private void writeHandlers(IndentedWriter w) throws UnableToCompleteException { if (designTime.isDesignTime()) { return; } handlerEvaluator.run(w, fieldManager, "owner"); } private void writeImports(IndentedWriter w) { w.write("import com.google.gwt.core.client.GWT;"); w.write("import com.google.gwt.dom.client.Element;"); if (!(htmlTemplates.isEmpty())) { w.write("import com.google.gwt.safehtml.client.SafeHtmlTemplates;"); w.write("import com.google.gwt.safehtml.shared.SafeHtml;"); w.write("import com.google.gwt.safehtml.shared.SafeHtmlUtils;"); w.write("import com.google.gwt.safehtml.shared.SafeHtmlBuilder;"); w.write("import com.google.gwt.safehtml.shared.SafeUri;"); w.write("import com.google.gwt.safehtml.shared.UriUtils;"); w.write("import com.google.gwt.uibinder.client.UiBinderUtil;"); } if (!isRenderer) { w.write("import com.google.gwt.uibinder.client.UiBinder;"); w.write("import com.google.gwt.uibinder.client.UiBinderUtil;"); w.write("import %s.%s;", uiRootType.getPackage().getName(), uiRootType.getName()); } else { w.write("import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;"); } } /** * Write statements created by {@link #addInitStatement}. This code must be * placed after all instantiation code. */ private void writeInitStatements(IndentedWriter niceWriter) { for (String s : initStatements) { niceWriter.write(s); } } /** * Write the statements to fill in the fields of the UI owner. */ private void writeOwnerFieldSetters(IndentedWriter niceWriter) throws UnableToCompleteException { if (designTime.isDesignTime()) { return; } for (OwnerField ownerField : getOwnerClass().getUiFields()) { String fieldName = ownerField.getName(); FieldWriter fieldWriter = fieldManager.lookup(fieldName); if (fieldWriter != null) { // ownerField is a widget. JClassType type = fieldWriter.getInstantiableType(); if (type != null) { maybeWriteFieldSetter(niceWriter, ownerField, fieldWriter.getInstantiableType(), fieldName); } else { // Must be a generated type if (!ownerField.isProvided()) { niceWriter.write("owner.%1$s = %1$s;", fieldName); } } } else { // ownerField was not found as bundle resource or widget, must die. die("Template %s has no %s attribute for %s.%s#%s", templatePath, getUiFieldAttributeName(), uiOwnerType.getPackage().getName(), uiOwnerType.getName(), fieldName); } } } private void writePackage(IndentedWriter w) { String packageName = baseClass.getPackage().getName(); if (packageName.length() > 0) { w.write("package %1$s;", packageName); w.newline(); } } /** * Writes the UiRenderer's source for the renderable strategy. */ private void writeRenderer(IndentedWriter w, FieldWriter rootField) throws UnableToCompleteException { validateRendererGetters(baseClass); validateRenderParameters(baseClass); JMethod[] eventMethods = findEventMethods(baseClass); for (JMethod jMethod : eventMethods) { validateEventMethod(jMethod); } writePackage(w); writeImports(w); w.newline(); writeClassOpen(w); writeStatics(w); w.newline(); // Create SafeHtml Template writeTemplatesInterface(w); w.newline(); htmlTemplates.writeTemplateCallers(w); w.newline(); JParameter[] renderParameters = findRenderParameters(baseClass); for (JParameter param : renderParameters) { // Prevent fields from render() parameters from being optimized. fieldManager.disableOptimization(param.getName()); } // public UiRendererImplClass() { w.write("public %s() {", implClassName); w.indent(); w.write("build_fields();"); w.outdent(); // } w.write("}"); w.newline(); // private build_fields() { w.write("private void build_fields() {"); w.indent(); fieldManager.initializeWidgetsInnerClass(w, getOwnerClass()); w.outdent(); // } w.write("}"); w.newline(); String renderParameterDeclarations = renderMethodParameters(renderParameters); w.write("public void render(final %s sb%s%s) {", SafeHtmlBuilder.class.getName(), renderParameterDeclarations.length() != 0 ? ", " : "", renderParameterDeclarations); w.indent(); w.newline(); writeRenderParameterInitializers(w, renderParameters); w.write("uiId = com.google.gwt.dom.client.Document.get().createUniqueId();"); w.newline(); w.write("build_fields();"); w.newline(); String safeHtml = rootField.getSafeHtml(); // TODO(rchandia) it should be possible to add the attribute when parsing // the UiBinder file w.write( "sb.append(stampUiRendererAttribute(%s, RENDERED_ATTRIBUTE, uiId));", safeHtml); w.outdent(); w.write("}"); w.newline(); fieldManager.writeFieldDefinitions(w, getOracle(), getOwnerClass(), getDesignTime()); writeRendererGetters(w, baseClass, rootField.getName()); writeRendererEventMethods(w, eventMethods, rootField.getName()); // Close class w.outdent(); w.write("}"); } private void writeRendererDispatcher(IndentedWriter w, String dispatcherName, JClassType targetType, String rootFieldName, JMethod[] uiHandlerMethods, JMethod sourceMethod) throws UnableToCompleteException { // static class UiRendererDispatcherForFoo extends UiRendererDispatcher { w.write("static class %s extends UiRendererDispatcher<%s> {", dispatcherName, targetType.getQualifiedSourceName()); w.indent(); writeRendererDispatcherTableInit(w, rootFieldName, uiHandlerMethods, dispatcherName); writeRendererDispatcherExtraParameters(w, sourceMethod); writeRendererDispatcherFire(w, sourceMethod); w.write("@SuppressWarnings(\"rawtypes\")"); w.write("@Override"); // public void fireEvent(GwtEvent somethingUnlikelyToCollideWithParamNames) { w.write("public void fireEvent(com.google.gwt.event.shared.GwtEvent %sEvent) {", SAFE_VAR_PREFIX); w.indent(); // switch (getMethodIndex()) { w.write("switch (getMethodIndex()) {"); w.indent(); for (int j = 0; j < uiHandlerMethods.length; j++) { JMethod uiMethod = uiHandlerMethods[j]; // case 0: w.write("case %s:", j); w.indent(); // getEventTarget().onClickRoot((ClickEvent) somethingUnlikelyToCollideWithParamNames, // getRoot(), a, b); StringBuffer sb = new StringBuffer(); JParameter[] sourceParameters = sourceMethod.getParameters(); // Cat the extra parameters i.e. ", a, b" JType[] uiHandlerParameterTypes = uiMethod.getParameterTypes(); if (uiHandlerParameterTypes.length >= 2) { sb.append(", getRoot()"); } for (int k = 2; k < uiHandlerParameterTypes.length; k++) { JParameter sourceParam = sourceParameters[k + 1]; sb.append(", "); sb.append(sourceParam.getName()); } w.write("getEventTarget().%s((%s) %sEvent%s);", uiMethod.getName(), uiHandlerParameterTypes[0].getQualifiedSourceName(), SAFE_VAR_PREFIX, sb.toString()); // break; w.write("break;"); w.newline(); w.outdent(); } // default: w.write("default:"); w.indent(); // break; w.write("break;"); w.outdent(); w.outdent(); w.write("}"); w.outdent(); w.write("}"); w.outdent(); w.write("}"); } private void writeRendererDispatcherExtraParameters(IndentedWriter w, JMethod sourceMethod) { for (int i = 3; i < sourceMethod.getParameters().length; i++) { JParameter param = sourceMethod.getParameters()[i]; // private int a; // private String b; w.write("private %s %s;", param.getType().getParameterizedQualifiedSourceName(), param.getName()); } } private void writeRendererDispatcherFire(IndentedWriter w, JMethod sourceMethod) { // public void fire(Foo o, NativeEvent e, Element parent, int a, String b) { w.write("public void fire("); w.indent(); JParameter[] sourceParameters = sourceMethod.getParameters(); for (int i = 0; i < sourceParameters.length; i++) { JParameter param = sourceParameters[i]; w.write(i == 0 ? "%s %s" : ", %s %s", param.getType().getQualifiedSourceName(), param.getName()); } w.write(") {"); w.indent(); // this.a = a; for (int i = 3; i < sourceParameters.length; i++) { JParameter sourceParam = sourceParameters[i]; w.write("this.%s = %s;", sourceParam.getName(), sourceParam.getName()); } // fireEvent(o, e, parent); w.write("fireEvent(%s, %s, %s);", sourceParameters[0].getName(), sourceParameters[1].getName(), sourceParameters[2].getName()); w.outdent(); w.write("}"); w.newline(); } private void writeRendererDispatcherTableInit(IndentedWriter w, String rootFieldName, JMethod[] uiHandlerMethods, String dispatcherName) throws UnableToCompleteException { ArrayList keys = new ArrayList(); ArrayList values = new ArrayList(); // Collect the event types and field names to form the dispatch table for (int i = 0; i < uiHandlerMethods.length; i++) { JMethod jMethod = uiHandlerMethods[i]; String eventType = findEventTypeName(jMethod); String[] fieldNames = jMethod.getAnnotation(UiHandler.class).value(); for (String fieldName : fieldNames) { if (rootFieldName.equals(fieldName)) { fieldName = AbstractUiRenderer.ROOT_FAKE_NAME; } keys.add(eventType + AbstractUiRenderer.UI_ID_SEPARATOR + fieldName); values.add(i); } } // private static String[] somethingUnlikelyToCollideWithParamNames_keys; w.write("private static String[] %s_keys;", SAFE_VAR_PREFIX); // private static Integer[] somethingUnlikelyToCollideWithParamNames_values; w.write("private static Integer[] %s_values;", SAFE_VAR_PREFIX); w.write("static {"); w.indent(); // private static String[] somethingUnlikelyToCollideWithParamNames_keys = new String[] { w.write("%s_keys = new String[] {", SAFE_VAR_PREFIX); w.indent(); for (String key : keys) { // "click:aField", w.write("\"%s\",", key); } w.outdent(); w.write("};"); w.newline(); // somethingUnlikelyToCollideWithParamNames_values = {0,1}; w.write("%s_values = new Integer[] {", SAFE_VAR_PREFIX); w.indent(); StringBuffer commaSeparatedValues = new StringBuffer(); for (Integer value : values) { commaSeparatedValues.append(value); commaSeparatedValues.append(","); } // "0,0,0,1,1,", w.write("%s", commaSeparatedValues.toString()); w.outdent(); w.write("};"); w.newline(); w.outdent(); w.write("}"); w.newline(); // public Foo() { w.write("public %s() {", dispatcherName); w.indent(); // initDispatchTable(keys, values); w.write("initDispatchTable(%s_keys, %s_values);", SAFE_VAR_PREFIX, SAFE_VAR_PREFIX); // This ensures the DomEvent#TYPE fields are properly initialized and registered // ClickEvent.getType(); HashSet eventTypes = new HashSet(); for (JMethod uiMethod : uiHandlerMethods) { eventTypes.add(uiMethod.getParameterTypes()[0].getQualifiedSourceName()); } for (String eventType : eventTypes) { w.write("%s.getType();", eventType); } w.outdent(); w.write("}"); w.newline(); } private void writeRendererEventMethods(IndentedWriter w, JMethod[] eventMethods, String rootField) throws UnableToCompleteException { for (JMethod jMethod : eventMethods) { JClassType eventTargetType = jMethod.getParameterTypes()[0].isClassOrInterface(); String eventTargetSimpleName = eventTargetType.getSimpleSourceName(); String dispatcherClassName = UI_RENDERER_DISPATCHER_PREFIX + eventTargetSimpleName; JMethod[] uiHandlerMethods = findUiHandlerMethods(eventTargetType); // public void onBrowserEvent(Foo f, NativeEvent event, Element parent, A a, B b ...) { w.write("@Override"); w.write("public %s {", jMethod.getReadableDeclaration(true, true, true, true, true)); if (uiHandlerMethods.length != 0) { w.indent(); // if (singletonUiRendererDispatcherForFoo == null) { w.write("if (singleton%s == null) {", dispatcherClassName); w.indent(); // singletonUiRendererDispatcherForFoo = new UiRendererDispatcherForFoo(); w.write("singleton%s = new %s();", dispatcherClassName, dispatcherClassName); w.outdent(); w.write("}"); // singletonUiRendererDispatcherForFoo.fire(o, event, parent, a, b); StringBuffer sb = new StringBuffer(); JParameter[] parameters = jMethod.getParameters(); for (int i = 0; i < parameters.length; i++) { JParameter callParam = parameters[i]; if (i != 0) { sb.append(", "); } sb.append(callParam.getName()); } w.write("singleton%s.fire(%s);", dispatcherClassName, sb.toString()); w.outdent(); } w.write("}"); w.newline(); if (uiHandlerMethods.length != 0) { // private static UiRendererDispatcherForFoo singletonUiRendererDispatcherForFoo; w.write("private static %s singleton%s;", dispatcherClassName, dispatcherClassName); writeRendererDispatcher(w, dispatcherClassName, eventTargetType, rootField, uiHandlerMethods, jMethod); } } } private void writeRendererGetters(IndentedWriter w, JClassType owner, String rootFieldName) { List getters = findGetterNames(owner); // For every requested getter for (JMethod getter : getters) { // public ElementSubclass getFoo(Element parent) { w.write("%s {", getter.getReadableDeclaration(false, false, false, false, true)); w.indent(); String getterFieldName = getterToFieldName(getter.getName()); // Is this a CSS resource field? FieldWriter fieldWriter = fieldManager.lookup(getterFieldName); if (FieldWriterType.GENERATED_CSS.equals(fieldWriter.getFieldType())) { // return (CssResourceSubclass) get_styleField; w.write("return (%s) %s;", getter.getReturnType().getErasedType().getQualifiedSourceName(), FieldManager.getFieldGetter(getterFieldName)); } else { // Else the non-root elements are found by id String elementParameter = getter.getParameters()[0].getName(); if (!getterFieldName.equals(rootFieldName)) { // return (ElementSubclass) findInnerField(parent, "foo", RENDERED_ATTRIBUTE); w.write("return (%s) findInnerField(%s, \"%s\", RENDERED_ATTRIBUTE);", getter.getReturnType().getErasedType().getQualifiedSourceName(), elementParameter, getterFieldName); } else { // return (ElementSubclass) findRootElement(parent); w.write("return (%s) findRootElement(%s, RENDERED_ATTRIBUTE);", getter.getReturnType().getErasedType().getQualifiedSourceName(), elementParameter); } } w.outdent(); w.write("}"); } } private void writeRenderParameterInitializers(IndentedWriter w, JParameter[] renderParameters) { for (int i = 0; i < renderParameters.length; i++) { JParameter parameter = renderParameters[i]; if (fieldManager.lookup(parameter.getName()) != null) { w.write("this.%s = %s;", parameter.getName(), parameter.getName()); w.newline(); } } } private void writeStaticMessagesInstance(IndentedWriter niceWriter) { if (messages.hasMessages()) { niceWriter.write(messages.getDeclaration()); } } private void writeStatics(IndentedWriter w) { writeStaticMessagesInstance(w); designTime.addDeclarations(w); } /** * Write statements created by {@link HtmlTemplatesWriter#addSafeHtmlTemplate} * . This code must be placed after all instantiation code. */ private void writeTemplatesInterface(IndentedWriter w) { if (!(htmlTemplates.isEmpty())) { assert useSafeHtmlTemplates : "SafeHtml is off, but templates were made."; htmlTemplates.writeInterface(w); w.newline(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy