| /* |
| * Copyright 2009 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.JType; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.uibinder.attributeparsers.FieldReferenceConverter; |
| 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 java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * This class handles all {@link FieldWriter} instances created for the current |
| * template. |
| */ |
| public class FieldManager { |
| |
| static class FieldAndSource { |
| final FieldWriter field; |
| final XMLElement element; |
| |
| public FieldAndSource(FieldWriter field, XMLElement element) { |
| this.field = field; |
| this.element = element; |
| } |
| } |
| |
| private static final String GETTER_PREFIX = "get_"; |
| |
| private static final String BUILDER_PREFIX = "build_"; |
| |
| private static final String DUPLICATE_FIELD_ERROR = "Duplicate declaration of field %1$s."; |
| |
| private static final Comparator<FieldWriter> BUILD_DEFINITION_SORT = |
| new Comparator<FieldWriter>() { |
| public int compare(FieldWriter field1, FieldWriter field2) { |
| // First get type precedence, if ties the field precedence is used. |
| int precedence = field2.getFieldType().getBuildPrecedence() |
| - field1.getFieldType().getBuildPrecedence(); |
| if (precedence == 0) { |
| precedence = field2.getBuildPrecedence() - field1.getBuildPrecedence(); |
| } |
| return precedence; |
| } |
| }; |
| |
| private static final Pattern JAVA_IDENTIFIER = |
| Pattern.compile("[\\p{L}_$][\\p{L}\\p{N}_$]*"); |
| |
| public static String getFieldBuilder(String fieldName) { |
| return String.format(BUILDER_PREFIX + "%s()", fieldName); |
| } |
| |
| public static String getFieldGetter(String fieldName) { |
| return String.format(GETTER_PREFIX + "%s()", fieldName); |
| } |
| |
| public static String stripFieldGetter(String fieldName) { |
| if (fieldName.startsWith(GETTER_PREFIX)) { |
| return fieldName.substring(GETTER_PREFIX.length()); |
| } |
| return fieldName; |
| } |
| |
| private final TypeOracle typeOracle; |
| |
| private final MortalLogger logger; |
| |
| /** |
| * Map of field name to FieldWriter. Note its a LinkedHashMap--we want to |
| * write these out in the order they're declared. |
| */ |
| private final LinkedHashMap<String, FieldWriter> fieldsMap = |
| new LinkedHashMap<String, FieldWriter>(); |
| |
| /** |
| * A stack of the fields. |
| */ |
| private final LinkedList<FieldAndSource> parsedFieldStack = new LinkedList<FieldAndSource>(); |
| |
| private LinkedHashMap<String, FieldReference> fieldReferences = |
| new LinkedHashMap<String, FieldReference>(); |
| |
| /** |
| * Counts the number of times a getter field is called, this important to |
| * decide which strategy to take when outputing getters and builders. |
| * {@see com.google.gwt.uibinder.rebind.FieldWriter#writeFieldDefinition}. |
| */ |
| private final Map<String, Integer> gettersCounter = new HashMap<String, Integer>(); |
| |
| /** |
| * Whether to use the new strategy of generating UiBinder code. |
| */ |
| private final boolean useLazyWidgetBuilders; |
| |
| public FieldManager(TypeOracle typeOracle, MortalLogger logger, boolean useLazyWidgetBuilders) { |
| this.typeOracle = typeOracle; |
| this.logger = logger; |
| this.useLazyWidgetBuilders = useLazyWidgetBuilders; |
| } |
| |
| /** |
| * Converts the given field to its getter. Example: |
| * <li> myWidgetX = get_myWidgetX() |
| * <li> f_html1 = get_f_html1() |
| */ |
| public String convertFieldToGetter(String fieldName) { |
| // could this conversion can be moved to FieldWriter? |
| if (!useLazyWidgetBuilders) { |
| return fieldName; |
| } |
| |
| incrementFieldCounter(fieldName); |
| return getFieldGetter(fieldName); |
| } |
| |
| /** |
| * Prevent a field from being optimized as only being referenced once (and therefore constant for |
| * all intents). This is necessary for UiRenderer fields passed as parameters to render() calls. |
| * Those fields are modified every time a template is rendered with the parameter values. |
| */ |
| public void disableOptimization(String fieldName) { |
| // TODO(rchandia): This hackish method should go away when the |
| // UiRenderer generator gets separated from the one used for |
| // UiBinder. Fields corresponding to parameters of render() will |
| // not use the initialization generated by the FieldWriter. |
| |
| // Incrementing the counter twice ensures no optimization happens. |
| // See AbstractFieldWriter#writeFieldDefinition() |
| incrementFieldCounter(fieldName); |
| incrementFieldCounter(fieldName); |
| } |
| |
| public FieldReference findFieldReference(String expressionIn) { |
| String expression = expressionIn; |
| if (useLazyWidgetBuilders) { |
| expression = stripFieldGetter(expression); |
| } |
| String converted = FieldReferenceConverter.expressionToPath(expression); |
| return fieldReferences.get(converted); |
| } |
| |
| /** |
| * Initialize with field builders the generated <b>Widgets</b> inner class. |
| * {@see com.google.gwt.uibinder.rebind.FieldWriter#writeFieldBuilder}. |
| */ |
| public void initializeWidgetsInnerClass(IndentedWriter w, |
| OwnerClass ownerClass) throws UnableToCompleteException { |
| |
| FieldWriter[] fields = fieldsMap.values().toArray( |
| new FieldWriter[fieldsMap.size()]); |
| Arrays.sort(fields, BUILD_DEFINITION_SORT); |
| |
| for (FieldWriter field : fields) { |
| int count = getGetterCounter(field.getName()); |
| field.writeFieldBuilder(w, count, ownerClass.getUiField(field.getName())); |
| } |
| } |
| |
| /** |
| * @param fieldName the name of the {@link FieldWriter} to find |
| * @return the {@link FieldWriter} instance indexed by fieldName or |
| * <b>null</b> in case fieldName is not found |
| */ |
| public FieldWriter lookup(String fieldName) { |
| return fieldsMap.get(fieldName); |
| } |
| |
| /** |
| * Remove the field at the top of the {@link #parsedFieldStack}. |
| */ |
| public void pop() { |
| parsedFieldStack.removeFirst(); |
| } |
| |
| /** |
| * @param source the element this field was parsed from |
| * @param fieldWriter the field to push on the top of the |
| * {@link #parsedFieldStack} |
| */ |
| public void push(XMLElement source, FieldWriter fieldWriter) { |
| parsedFieldStack.addFirst(new FieldAndSource(fieldWriter, source)); |
| } |
| |
| /** |
| * Used to declare fields of an existing type. If your field will hold a type |
| * that is being generated, see {@link #registerFieldOfGeneratedType}. |
| * <p> |
| * When making a field we peek at the {@link #parsedFieldStack} to make sure |
| * that the field that holds the widget currently being parsed will depended |
| * upon the field being declared. This ensures, for example, that dom id |
| * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an HTMLPanel |
| * will be declared before it is. |
| * |
| * @param fieldWriterType the field writer type associated |
| * @param fieldType the type of the new field |
| * @param fieldName the name of the new field |
| * @return a new {@link FieldWriter} instance |
| * @throws UnableToCompleteException on duplicate name |
| */ |
| public FieldWriter registerField(FieldWriterType fieldWriterType, |
| JClassType fieldType, String fieldName) throws UnableToCompleteException { |
| FieldWriter field = new FieldWriterOfExistingType(this, |
| fieldWriterType, fieldType, fieldName, logger); |
| return registerField(fieldName, field); |
| } |
| |
| public FieldWriter registerField(JClassType fieldType, String fieldName) |
| throws UnableToCompleteException { |
| return registerField(FieldWriterType.DEFAULT, fieldType, fieldName); |
| } |
| |
| public FieldWriter registerField(String type, String fieldName) |
| throws UnableToCompleteException { |
| return registerField(typeOracle.findType(type), fieldName); |
| } |
| |
| /** |
| * Used to declare fields that will hold generated instances generated |
| * CssResource interfaces. If your field will hold a reference of an existing |
| * type, see {@link #registerField}. For other generated types, use |
| * {@link #registerFieldOfGeneratedType} |
| * {@link #registerFieldForGeneratedCssResource}. |
| * <p> |
| * When making a field we peek at the {@link #parsedFieldStack} to make sure |
| * that the field that holds the widget currently being parsed will depended |
| * upon the field being declared. This ensures, for example, that dom id |
| * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an HTMLPanel |
| * will be declared before it is. |
| * |
| * @throws UnableToCompleteException on duplicate name |
| * @return a new {@link FieldWriter} instance |
| */ |
| public FieldWriter registerFieldForGeneratedCssResource( |
| ImplicitCssResource cssResource) throws UnableToCompleteException { |
| FieldWriter field = new FieldWriterOfGeneratedCssResource(this, |
| typeOracle.findType(String.class.getCanonicalName()), cssResource, logger); |
| return registerField(cssResource.getName(), field); |
| } |
| |
| /** |
| * Register a new field for {@link com.google.gwt.uibinder.client.LazyDomElement} |
| * types. LazyDomElement fields can only be associated with html elements. Example: |
| * |
| * <li>LazyDomElement<DivElement> -> <div></li> |
| * <li>LazyDomElement<Element> -> <div></li> |
| * <li>LazyDomElement<SpanElement> -> <span></li> |
| * |
| * @param templateFieldType the html type to bind, eg, SpanElement, DivElement, etc |
| * @param ownerField the field instance |
| */ |
| public FieldWriter registerFieldForLazyDomElement(JClassType templateFieldType, |
| OwnerField ownerField) throws UnableToCompleteException { |
| if (ownerField == null) { |
| throw new RuntimeException("Cannot register a null owner field for LazyDomElement."); |
| } |
| FieldWriter field = new FieldWriterOfLazyDomElement(this, |
| templateFieldType, ownerField, logger); |
| return registerField(ownerField.getName(), field); |
| } |
| |
| /** |
| * Used to declare fields of a type (other than CssResource) that is to be |
| * generated. If your field will hold a reference of an existing type, see |
| * {@link #registerField}. For generated CssResources, see |
| * {@link #registerFieldForGeneratedCssResource}. |
| * <p> |
| * When making a field we peek at the {@link #parsedFieldStack} to make sure |
| * that the field that holds the widget currently being parsed will depended |
| * upon the field being declared. This ensures, for example, that dom id |
| * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an HTMLPanel |
| * will be declared before it is. |
| * |
| * @param assignableType class or interface extened or implemented by this |
| * type |
| * @param typeName the full qualified name for the class associated with the |
| * field |
| * @param fieldName the name of the field |
| * @throws UnableToCompleteException on duplicate name |
| * @return a new {@link FieldWriter} instance |
| */ |
| public FieldWriter registerFieldOfGeneratedType(JClassType assignableType, |
| String typePackage, String typeName, String fieldName) |
| throws UnableToCompleteException { |
| FieldWriter field = new FieldWriterOfGeneratedType(this, assignableType, |
| typePackage, typeName, fieldName, logger); |
| return registerField(fieldName, field); |
| } |
| |
| /** |
| * Called to register a <code>{field.reference}</code> encountered during |
| * parsing, to be validated against the type oracle once parsing is complete. |
| */ |
| public void registerFieldReference(XMLElement source, String fieldReferenceString, JType... types) { |
| source = source != null ? source : parsedFieldStack.peek().element; |
| |
| FieldReference fieldReference = fieldReferences.get(fieldReferenceString); |
| if (fieldReference == null) { |
| fieldReference = new FieldReference(fieldReferenceString, source, this, typeOracle); |
| fieldReferences.put(fieldReferenceString, fieldReference); |
| } |
| |
| fieldReference.addLeftHandType(source, types); |
| } |
| |
| /** |
| * Gets a FieldWriter given its name or throws a RuntimeException if not found. |
| * @param fieldName the name of the {@link FieldWriter} to find |
| * @return the {@link FieldWriter} instance indexed by fieldName |
| */ |
| public FieldWriter require(String fieldName) { |
| FieldWriter fieldWriter = lookup(fieldName); |
| if (fieldWriter == null) { |
| throw new RuntimeException(String.format("The required field \"%s\" doesn't exist.", |
| fieldName)); |
| } |
| return fieldWriter; |
| } |
| |
| /** |
| * To be called after parsing is complete. Surveys all |
| * <code>{field.reference}</code>s and checks they refer to existing types, |
| * and have appropriate return types. |
| * |
| * @throws UnableToCompleteException if any <code>{field.references}</code> |
| * can't be resolved |
| */ |
| public void validate() throws UnableToCompleteException { |
| boolean failed = false; |
| |
| for (Map.Entry<String, FieldReference> entry : fieldReferences.entrySet()) { |
| FieldReference ref = entry.getValue(); |
| MonitoredLogger monitoredLogger = new MonitoredLogger(logger); |
| ref.validate(monitoredLogger); |
| failed |= monitoredLogger.hasErrors(); |
| } |
| if (failed) { |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| /** |
| * Outputs the getter and builder definitions for all fields. |
| * {@see com.google.gwt.uibinder.rebind.AbstractFieldWriter#writeFieldDefinition}. |
| */ |
| public void writeFieldDefinitions(IndentedWriter writer, TypeOracle typeOracle, |
| OwnerClass ownerClass, DesignTimeUtils designTime) |
| throws UnableToCompleteException { |
| Collection<FieldWriter> fields = fieldsMap.values(); |
| for (FieldWriter field : fields) { |
| int counter = getGetterCounter(field.getName()); |
| field.writeFieldDefinition( |
| writer, |
| typeOracle, |
| ownerClass.getUiField(field.getName()), |
| designTime, |
| counter, |
| useLazyWidgetBuilders); |
| } |
| } |
| |
| /** |
| * Writes all stored gwt fields. |
| * |
| * @param writer the writer to output |
| */ |
| public void writeGwtFieldsDeclaration(IndentedWriter writer) throws UnableToCompleteException { |
| Collection<FieldWriter> fields = fieldsMap.values(); |
| for (FieldWriter field : fields) { |
| field.write(writer); |
| } |
| } |
| |
| private void ensureValidity(String fieldName) throws UnableToCompleteException { |
| if (!JAVA_IDENTIFIER.matcher(fieldName).matches()) { |
| logger.die("Illegal field name \"%s\"", fieldName); |
| } |
| } |
| |
| /** |
| * Gets the number of times a getter for the given field is called. |
| */ |
| private int getGetterCounter(String fieldName) { |
| Integer count = gettersCounter.get(fieldName); |
| return (count == null) ? 0 : count; |
| } |
| |
| /** |
| * Increments the number of times a getter for the given field is called. |
| */ |
| private void incrementFieldCounter(String fieldName) { |
| int count = getGetterCounter(fieldName) + 1; |
| gettersCounter.put(fieldName, count); |
| } |
| |
| private FieldWriter registerField(String fieldName, FieldWriter field) |
| throws UnableToCompleteException { |
| ensureValidity(fieldName); |
| requireUnique(fieldName); |
| fieldsMap.put(fieldName, field); |
| |
| if (parsedFieldStack.size() > 0) { |
| FieldWriter parent = parsedFieldStack.peek().field; |
| field.setBuildPrecedence(parent.getBuildPrecedence() + 1); |
| parent.needs(field); |
| } |
| |
| return field; |
| } |
| |
| private void requireUnique(String fieldName) throws UnableToCompleteException { |
| if (fieldsMap.containsKey(fieldName)) { |
| logger.die(DUPLICATE_FIELD_ERROR, fieldName); |
| } |
| } |
| } |