blob: 743ee19516280045dc3240ef332c67e9bc20c071 [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.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dom.client.TagName;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.parsers.AttributeMessageParser;
import com.google.gwt.uibinder.parsers.AttributeParser;
import com.google.gwt.uibinder.parsers.BeanParser;
import com.google.gwt.uibinder.parsers.BundleAttributeParser;
import com.google.gwt.uibinder.parsers.ElementParser;
import com.google.gwt.uibinder.parsers.StrictAttributeParser;
import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
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 org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* Writer for UiBinder generated classes.
*
* TODO(rdamazio): Refactor this, extract model classes, improve ordering
* guarantees, etc.
*
* TODO(rjrjr): Improve error messages
*/
@SuppressWarnings("deprecation")
public class UiBinderWriter {
private static final String BINDER_URI = "urn:ui:com.google.gwt.uibinder";
private static final String BUNDLE_URI_SCHEME = "urn:with:";
private static final String PACKAGE_URI_SCHEME = "urn:import:";
private static int domId = 0;
// 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.replaceAll("\"", "\\\\\"");
text = text.replaceAll("\n", "\\\\n");
return text;
}
private static String capitalizePropName(String propName) {
return propName.substring(0, 1).toUpperCase() + propName.substring(1);
}
private static AttributeParser getAttributeParserByClassName(
String parserClassName) {
try {
Class<? extends AttributeParser> parserClass = Class.forName(
parserClassName).asSubclass(AttributeParser.class);
return parserClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to instantiate parser", e);
} catch (ClassCastException e) {
throw new RuntimeException(parserClassName
+ " must extend AttributeParser");
} catch (InstantiationException e) {
throw new RuntimeException("Unable to instantiate parser", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to instantiate parser", e);
}
}
/**
* 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
*/
private 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 the superclass and implemented interfaces to the back of the queue.
JClassType superClass = curType.getSuperclass();
if (superClass != null) {
q.add(superClass);
}
for (JClassType intf : curType.getImplementedInterfaces()) {
q.add(intf);
}
}
return list;
}
private final MortalLogger logger;
/**
* Class names of parsers for values of attributes with no namespace prefix,
* keyed by method parameter signatures.
*
* TODO(rjrjr) Seems like the attribute parsers belong in BeanParser, which is
* the only thing that uses them.
*/
private final Map<String, String> attributeParsers = new HashMap<String, String>();
/**
* 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>();
/**
* Map of bundle parsers, keyed by bundle class name.
*/
private final Map<String, BundleAttributeParser> bundleParsers = new HashMap<String, BundleAttributeParser>();
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 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 OwnerClass ownerClass;
private final FieldManager fieldManager;
private final ImplicitClientBundle bundleClass;
private int fieldIndex;
private String gwtPrefix;
private String rendered;
UiBinderWriter(JClassType baseClass, String implClassName,
String templatePath, TypeOracle oracle, MortalLogger logger)
throws UnableToCompleteException {
this.baseClass = baseClass;
this.implClassName = implClassName;
this.oracle = oracle;
this.logger = logger;
this.templatePath = templatePath;
this.messages = new MessagesWriter(BINDER_URI, logger, templatePath,
baseClass.getPackage().getName(), this.implClassName);
JClassType uiBinderType = baseClass.getImplementedInterfaces()[0];
JClassType[] typeArgs = uiBinderType.isParameterized().getTypeArgs();
uiRootType = typeArgs[0];
uiOwnerType = typeArgs[1];
ownerClass = new OwnerClass(uiOwnerType, logger);
bundleClass = new ImplicitClientBundle(baseClass.getPackage().getName(),
this.implClassName, CLIENT_BUNDLE_FIELD, logger);
handlerEvaluator = new HandlerEvaluator(ownerClass, logger, oracle);
fieldManager = new FieldManager(logger);
}
/**
* 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(String.format(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) {
statements.add(String.format(format, args));
}
/**
* 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 parentElementExpression an expression to be evaluated at runtime,
* which will return an Element that is an ancestor of this one
* (needed by the getElementById call mentioned above).
*/
public String declareDomField(String fieldName, String parentElementExpression)
throws UnableToCompleteException {
String name = declareDomIdHolder();
setFieldInitializer(fieldName, "null");
addInitStatement(
"%s = UiBinderUtil.attachToDomAndGetChild(%s, %s).cast();", fieldName,
parentElementExpression, name);
addInitStatement("%s.removeAttribute(\"id\");", fieldName);
return tokenForExpression(name);
}
/**
* Declare a variable that will be filled at runtime with a unique id, safe
* for use as a dom element's id attribute.
*
* @return that variable's name.
*/
public String declareDomIdHolder() throws UnableToCompleteException {
String domHolderName = "domId" + domId++;
FieldWriter domField = fieldManager.registerField(
oracle.findType(String.class.getName()), domHolderName);
domField.setInitializer("com.google.gwt.dom.client.Document.get().createUniqueId()");
return domHolderName;
}
/**
* 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.
*/
public String declareField(String typeName, XMLElement elem)
throws UnableToCompleteException {
JClassType type = oracle.findType(typeName);
if (type == null) {
die("In %s, unknown type %s", elem, typeName);
}
String fieldName = getFieldName(elem);
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_" + elem.getLocalName() + (++fieldIndex));
}
fieldManager.registerField(type, fieldName);
return fieldName;
}
/**
* 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) {
fieldManager.registerField(findFieldType(elem), fieldName);
}
return fieldName;
}
/**
* Given a string containing tokens returned by {@link #tokenForExpression} 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);
}
/**
* 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 (!isWidgetElement(elem)) {
return findGwtDomElementTypeForTag(tagName);
}
String ns = elem.getNamespaceUri();
JPackage pkg = parseNamespacePackage(ns);
if (pkg == null) {
throw new RuntimeException("No such package: " + ns);
}
JClassType rtn = null;
if (pkg != null) {
rtn = pkg.findType(tagName);
if (rtn == null) {
die("No class matching \"%s\" in %s", tagName, ns);
}
}
return rtn;
}
/**
* 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 + "\"");
}
/**
* Find and return an appropriate attribute parser for a set of parameters, or
* return null.
*/
public AttributeParser getAttributeParser(JParameter... params) {
String paramTypeNames = getParametersKey(params);
String parserClassName = attributeParsers.get(paramTypeNames);
if (parserClassName != null) {
return getAttributeParserByClassName(parserClassName);
}
if (params.length == 1) {
return new StrictAttributeParser();
}
return null;
}
/**
* Find and return an appropriate attribute parser for an attribute and set of
* parameters, or return null.
* <p>
* If params is of size one, a parser of some kind is guaranteed to be
* returned.
*/
public AttributeParser getAttributeParser(XMLAttribute attribute,
JParameter... params) throws UnableToCompleteException {
AttributeParser parser = getBundleAttributeParser(attribute);
if (parser == null) {
parser = getAttributeParser(params);
}
return parser;
}
/**
* Finds an attribute {@link BundleAttributeParser} for the given xml
* attribute, if any, based on its namespace uri.
*
* @return the parser or null
* @deprecated exists only to support {@link BundleAttributeParser}, which
* will be leaving us soon.
*/
@Deprecated
public BundleAttributeParser getBundleAttributeParser(XMLAttribute attribute)
throws UnableToCompleteException {
if (attribute.getNamespaceUri() == null) {
return null;
}
String attributePrefixUri = attribute.getNamespaceUri();
if (!attributePrefixUri.startsWith(BUNDLE_URI_SCHEME)) {
return null;
}
String bundleClassName = attributePrefixUri.substring(BUNDLE_URI_SCHEME.length());
BundleAttributeParser parser = bundleParsers.get(bundleClassName);
if (parser == null) {
JClassType bundleClassType = getOracle().findType(bundleClassName);
if (bundleClassType == null) {
die("No such resource class: " + bundleClassName);
}
parser = createBundleParser(bundleClassType, attribute);
bundleParsers.put(bundleClassName, parser);
}
return parser;
}
public ImplicitClientBundle getBundleClass() {
return bundleClass;
}
/**
* @return 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 && BINDER_URI.equals(uri);
}
public boolean isWidgetElement(XMLElement elem) {
String uri = elem.getNamespaceUri();
return uri != null && uri.startsWith(PACKAGE_URI_SCHEME);
}
/**
* Parses the object associated with the specified element, and returns the
* name of the field (possibly private) 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 name of the field containing the parsed widget
*/
public String parseElementToField(XMLElement elem)
throws UnableToCompleteException {
if (elementParsers.isEmpty()) {
registerParsers();
}
// Get the class associated with this element.
JClassType type = findFieldType(elem);
// Declare its field.
String fieldName = declareField(type.getQualifiedSourceName(), elem);
FieldWriter field = fieldManager.lookup(fieldName);
// 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.
fieldManager.push(field);
// Give all the parsers a chance to generate their code.
for (ElementParser parser : getParsersForClass(type)) {
parser.parse(elem, fieldName, type, this);
}
fieldManager.pop();
return fieldName;
}
/**
* 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 contructor
* invocaction, instead of the default GWT.create call.
*/
public void setFieldInitializerAsConstructor(String fieldName,
JClassType type, String... args) {
setFieldInitializer(fieldName, String.format("new %s(%s)",
type.getQualifiedSourceName(), asCommaSeparatedList(args)));
}
/**
* 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 stiched 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
*/
public String tokenForExpression(String expression) {
return tokenator.nextToken(("\" + " + expression + " + \""));
}
/**
* 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);
}
/**
* 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).
*/
void parseDocument(PrintWriter printWriter) throws UnableToCompleteException {
Document doc = null;
try {
doc = parseXmlResource(templatePath);
} catch (SAXParseException e) {
die("Error parsing XML (line " + e.getLineNumber() + "): "
+ e.getMessage(), e);
}
JClassType uiBinderClass = getOracle().findType(UiBinder.class.getName());
if (!baseClass.isAssignableTo(uiBinderClass)) {
die(baseClass.getName() + " must implement UiBinder");
}
Element documentElement = doc.getDocumentElement();
gwtPrefix = documentElement.lookupPrefix(BINDER_URI);
XMLElement elem = new XMLElement(documentElement, this);
this.rendered = tokenator.detokenate(parseDocumentElement(elem));
printWriter.print(rendered);
}
private void addAttributeParser(String signature, String className) {
attributeParsers.put(signature, className);
}
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.parsers." + className + "Parser";
addElementParser(gwtClass, parser);
}
/**
* Creates a parser for the given bundle class. This method will die soon.
*/
private BundleAttributeParser createBundleParser(JClassType bundleClass,
XMLAttribute attribute) throws UnableToCompleteException {
final String templateResourceName = attribute.getName().split(":")[0];
warn("The %1$s mechanism is deprecated. Instead, declare the following "
+ "%2$s:with element as a child of your %2$s:UiBinder element: "
+ "<%2$s:with field='%3$s' type='%4$s.%5$s' />", BUNDLE_URI_SCHEME,
gwtPrefix, templateResourceName, bundleClass.getPackage().getName(),
bundleClass.getName());
// Try to find any bundle instance created with UiField.
OwnerField field = getOwnerClass().getUiFieldForType(bundleClass);
if (field != null) {
if (!templateResourceName.equals(field.getName())) {
die("Template %s has no \"xmlns:%s='urn:with:%s'\" for %s.%s#%s",
templatePath, field.getName(),
bundleClass.getQualifiedSourceName(),
uiOwnerType.getPackage().getName(), uiOwnerType.getName(),
field.getName());
}
if (field.isProvided()) {
return new BundleAttributeParser(bundleClass, "owner."
+ field.getName(), false);
}
}
// Try to find any bundle instance created with @UiFactory.
JMethod method = getOwnerClass().getUiFactoryMethod(bundleClass);
if (method != null) {
return new BundleAttributeParser(bundleClass, "owner." + method.getName()
+ "()", false);
}
return new BundleAttributeParser(bundleClass, "my"
+ bundleClass.getName().replace('.', '_') + "Instance", true);
}
/**
* Outputs a bundle resource for a given bundle attribute parser.
*/
private String declareStaticField(BundleAttributeParser parser) {
if (!parser.isBundleStatic()) {
return null;
}
String fullBundleClassName = parser.fullBundleClassName();
StringBuilder b = new StringBuilder();
b.append("static ").append(fullBundleClassName).append(" ").append(
parser.bundleInstance()).append(" = GWT.create(").append(
fullBundleClassName).append(".class);");
return b.toString();
}
/**
* Given a DOM tag name, return the corresponding
* {@link com.google.gwt.dom.client.Element} subclass.
*/
private JClassType findGwtDomElementTypeForTag(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;
}
/**
* 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.consumeAttribute("id");
warn("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("Cannot declare both id and field on the same element: " + elem);
}
fieldName = elem.consumeAttribute(getUiFieldAttributeName());
}
return fieldName;
}
/**
* Given a parameter array, return a key for the attributeParsers table.
*/
private String getParametersKey(JParameter[] params) {
String paramTypeNames = "";
for (int i = 0; i < params.length; ++i) {
paramTypeNames += params[i].getType().getParameterizedQualifiedSourceName();
if (i != params.length - 1) {
paramTypeNames += ",";
}
}
return paramTypeNames;
}
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());
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());
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 (!templateClass.isAssignableTo(fieldType)) {
die("Template field and owner field types don't match: %s != %s",
templateClass.getQualifiedSourceName(),
fieldType.getQualifiedSourceName());
}
if (!ownerField.isProvided()) {
niceWriter.write("owner.%1$s = %2$s;", ownerField.getName(),
templateField);
}
}
/**
* 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(bundleClass.getPackageName(),
bundleClass.getClassName(), bundleClass.getFieldName());
// Allow GWT.create() to init the field, the default behavior
String rootField = new UiBinderParser(this, messages, fieldManager, oracle,
bundleClass).parse(elem);
StringWriter stringWriter = new StringWriter();
IndentedWriter niceWriter = new IndentedWriter(
new PrintWriter(stringWriter));
writeBinder(niceWriter, rootField);
return stringWriter.toString();
}
/**
* Parses a package uri (i.e. package://com.google...).
*
* @throws UnableToCompleteException on bad package name
*/
private JPackage parseNamespacePackage(String ns)
throws UnableToCompleteException {
if (ns.startsWith(PACKAGE_URI_SCHEME)) {
String pkgName = ns.substring(PACKAGE_URI_SCHEME.length());
JPackage pkg = oracle.findPackage(pkgName);
if (pkg == null) {
die("Package not found: " + pkgName);
}
return pkg;
}
return null;
}
private Document parseXmlResource(final String resourcePath)
throws SAXParseException, UnableToCompleteException {
// Get the document builder. We need namespaces, and automatic expanding
// of entity references (the latter of which makes life somewhat easier
// for XMLElement).
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setExpandEntityReferences(true);
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
try {
ClassLoader classLoader = UiBinderGenerator.class.getClassLoader();
URL url = classLoader.getResource(resourcePath);
if (null == url) {
die("Unable to find resource: " + resourcePath);
}
InputStream stream = url.openStream();
InputSource input = new InputSource(stream);
input.setSystemId(url.toExternalForm());
builder.setEntityResolver(new GwtResourceEntityResolver());
return builder.parse(input);
} catch (SAXParseException e) {
// Let SAXParseExceptions through.
throw e;
} catch (SAXException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void registerParsers() {
// TODO(rjrjr): Allow third-party parsers to register themselves
// automagically, according to http://b/issue?id=1867118
addElementParser("com.google.gwt.dom.client.Element",
"com.google.gwt.uibinder.parsers.DomElementParser");
// Register widget parsers.
addWidgetParser("UIObject");
addWidgetParser("HasText");
addWidgetParser("HasHTML");
addWidgetParser("HasWidgets");
addWidgetParser("HTMLPanel");
addWidgetParser("DockPanel");
addWidgetParser("StackPanel");
addWidgetParser("DisclosurePanel");
addWidgetParser("TabPanel");
addWidgetParser("MenuItem");
addWidgetParser("MenuBar");
addWidgetParser("RadioButton");
addWidgetParser("CellPanel");
addWidgetParser("CustomButton");
addAttributeParser("boolean",
"com.google.gwt.uibinder.parsers.BooleanAttributeParser");
addAttributeParser("java.lang.String",
"com.google.gwt.uibinder.parsers.StringAttributeParser");
addAttributeParser("int", "com.google.gwt.uibinder.parsers.IntParser");
addAttributeParser("int,int",
"com.google.gwt.uibinder.parsers.IntPairParser");
addAttributeParser("com.google.gwt.user.client.ui.HasHorizontalAlignment."
+ "HorizontalAlignmentConstant",
"com.google.gwt.uibinder.parsers.HorizontalAlignmentConstantParser");
}
/**
* 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, String rootField)
throws UnableToCompleteException {
writePackage(w);
writeImports(w);
w.newline();
writeClassOpen(w);
writeStatics(w);
w.newline();
// createAndBindUi method
w.write("public %s createAndBindUi(final %s owner) {",
uiRootType.getName(), uiOwnerType.getName());
w.indent();
w.newline();
writeGwtFields(w);
w.newline();
writeAddedStatements(w);
w.newline();
writeInitStatements(w);
w.newline();
writeHandlers(w);
w.newline();
writeOwnerFieldSetters(w);
writeCssInjectors(w);
w.write("return %s;", rootField);
w.outdent();
w.write("}");
// Close class
w.outdent();
w.write("}");
}
private void writeClassOpen(IndentedWriter w) {
w.write("public class %s extends AbstractUiBinder<%s, %s> implements %s {",
implClassName, uiRootType.getName(), uiOwnerType.getName(),
baseClass.getName());
w.indent();
}
private void writeCssInjectors(IndentedWriter w) {
for (ImplicitCssResource css : bundleClass.getCssMethods()) {
w.write("ensureCssInjected(%s.%s());", 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(hermes) This can be null due to http://b/1836504. If that
// is fixed properly, a null fieldWriter will be an error
// (would that be a user error or a runtime error? Not sure)
if (fieldWriter != null) {
fieldManager.lookup(fieldName).setInitializerMaybe(
String.format("owner.%1$s", fieldName));
}
}
}
// Write gwt field declarations.
fieldManager.writeGwtFieldsDeclaration(niceWriter, uiOwnerType.getName());
}
private void writeHandlers(IndentedWriter w) throws UnableToCompleteException {
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.uibinder.client.AbstractUiBinder;");
w.write("import com.google.gwt.uibinder.client.UiBinderUtil;");
w.write("import %s.%s;", uiRootType.getPackage().getName(),
uiRootType.getName());
}
/**
* 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 {
for (OwnerField ownerField : getOwnerClass().getUiFields()) {
String fieldName = ownerField.getName();
FieldWriter fieldWriter = fieldManager.lookup(fieldName);
BundleAttributeParser bundleParser = bundleParsers.get(ownerField.getType().getRawType().getQualifiedSourceName());
if (bundleParser != null) {
// ownerField is a bundle resource.
maybeWriteFieldSetter(niceWriter, ownerField,
bundleParser.bundleClass(), bundleParser.bundleInstance());
} else if (fieldWriter != null) {
// ownerField is a widget.
JClassType type = fieldWriter.getType();
if (type != null) {
maybeWriteFieldSetter(niceWriter, ownerField, fieldWriter.getType(),
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();
}
}
/**
* Generates instances of any bundle classes that have been referenced by a
* namespace entry in the top level element. This must be called *after* all
* parsing is through, as the bundle list is generated lazily as dom elements
* are parsed.
*/
private void writeStaticBundleInstances(IndentedWriter niceWriter) {
// TODO(rjrjr) It seems bad that this method has special
// knowledge of BundleAttributeParser, but that'll die soon so...
for (String key : bundleParsers.keySet()) {
String declaration = declareStaticField(bundleParsers.get(key));
if (declaration != null) {
niceWriter.write(declaration);
}
}
}
private void writeStaticMessagesInstance(IndentedWriter niceWriter) {
if (messages.hasMessages()) {
niceWriter.write(messages.getDeclaration());
}
}
private void writeStatics(IndentedWriter w) {
writeStaticMessagesInstance(w);
writeStaticBundleInstances(w);
}
}