blob: 41414dd5af50ccc092676aebbbbc33dd8c054fef [file] [log] [blame]
/*
* 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.core.shared.impl.StringCase;
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.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("<", "&lt;");
text = text.replaceAll(">", "&gt;");
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<JClassType> getClassHierarchyBreadthFirst(JClassType type) {
LinkedList<JClassType> list = new LinkedList<JClassType>();
LinkedList<JClassType> q = new LinkedList<JClassType>();
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 StringCase.toUpper(propName.substring(0, 1)) + propName.substring(1);
}
/**
* Searches for methods named onBrowserEvent in a {@code type}.
*/
private static JMethod[] findEventMethods(JClassType type) {
List<JMethod> methods = new ArrayList<JMethod>(Arrays.asList(type.getInheritableMethods()));
for (Iterator<JMethod> 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<JMethod> findGetterNames(JClassType owner) {
List<JMethod> ret = new ArrayList<JMethod>();
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<JMethod> result = new ArrayList<JMethod>();
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<String, String> elementParsers = new HashMap<String, String>();
private final List<String> initStatements = new ArrayList<String>();
private final List<String> statements = new ArrayList<String>();
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<String> attachSectionElements = new LinkedList<String>();
/**
* Maps from field element name to the temporary attach record variable name.
*/
private final Map<String, String> attachedVars = new HashMap<String, String>();
private int nextAttachVar = 0;
/**
* Stack of statements to be executed after we detach the current attach
* section.
*/
private final LinkedList<List<String>> detachStatementsStack = new LinkedList<List<String>>();
private final AttributeParsers attributeParsers;
private final UiBinderContext uiBinderCtx;
private final String binderUri;
private final boolean isRenderer;
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)
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.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<Widget, MyClass> {}\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.).
* <p>
* 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<String>());
}
/**
* 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.
* <P>
* 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<String> 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}<code>(field + ".getElement()")</code>.
*
* @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 <code>null</code>.
*/
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 <W extends IsWidget & IsPlaid>
*/
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<? extends ElementParser> 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<ElementParser> getParsersForClass(JClassType type) {
List<ElementParser> parsers = new ArrayList<ElementParser>();
/*
* Let this non-widget parser go first (it finds <m:attribute/> 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<? extends ElementParser> 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).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, ...)}).
* <ul>
* <li> The second parameter type is {@code NativeEvent}
* <li> The third parameter type is {@code Element}
* <li> 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}
* </ul>
*/
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:
* <ul>
* <li> The annotation must list valid {@code ui:field}s
* <li> The first parameter must be assignable to
* {@link com.google.gwt.event.dom.client.DomEvent DomEvent}
* <li> If present, the second parameter must be of type
* {@link com.google.gwt.dom.client.Element Element}
* <li> For all other parameters in position {@code n} must be of the same type as
* {@code parameters[n + 1]}
* </ul>
*/
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<UiHandler> 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<OwnerField> 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<Foo> {
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<String> keys = new ArrayList<String>();
ArrayList<Integer> values = new ArrayList<Integer>();
// 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<String> eventTypes = new HashSet<String>();
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<JMethod> 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();
}
}
}