| /* |
| * 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("<", "<"); |
| text = text.replaceAll(">", ">"); |
| |
| if (!preserveWhitespace) { |
| text = text.replaceAll("\\s+", " "); |
| } |
| |
| return escapeTextForJavaStringLiteral(text); |
| } |
| |
| /** |
| * Escape characters that would mess up interpretation of this string as a |
| * string literal in generated code (that is, protect \n and " ). |
| */ |
| public static String escapeTextForJavaStringLiteral(String text) { |
| text = text.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)); |
| } |
| fieldName = normalizeFieldName(fieldName); |
| 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); |
| } |
| } |
| |
| 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(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"); |
| |
| addWidgetParser("DockLayoutPanel"); |
| addWidgetParser("StackLayoutPanel"); |
| |
| 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); |
| } |
| |
| } |