| /* |
| * 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.JField; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.JParameter; |
| import com.google.gwt.core.ext.typeinfo.JPrimitiveType; |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.dev.resource.ResourceOracle; |
| import com.google.gwt.resources.client.CssResource; |
| import com.google.gwt.resources.client.DataResource; |
| import com.google.gwt.resources.client.ImageResource; |
| import com.google.gwt.resources.client.ImageResource.RepeatStyle; |
| import com.google.gwt.resources.rg.GssResourceGenerator.GssOptions; |
| import com.google.gwt.uibinder.elementparsers.BeanParser; |
| import com.google.gwt.uibinder.elementparsers.SimpleInterpeter; |
| 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.ImplicitDataResource; |
| import com.google.gwt.uibinder.rebind.model.ImplicitImageResource; |
| import com.google.gwt.uibinder.rebind.model.OwnerField; |
| |
| import java.util.LinkedHashSet; |
| import java.util.Locale; |
| |
| /** |
| * Parses the root UiBinder element, and kicks of the parsing of the rest of the |
| * document. |
| */ |
| public class UiBinderParser { |
| |
| enum Resource { |
| DATA { |
| @Override |
| void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException { |
| parser.createData(elem); |
| } |
| }, |
| IMAGE { |
| @Override |
| void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException { |
| parser.createImage(elem); |
| } |
| }, |
| IMPORT { |
| @Override |
| void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException { |
| parser.createImport(elem); |
| } |
| }, |
| STYLE { |
| @Override |
| void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException { |
| parser.createStyle(elem); |
| } |
| }, |
| WITH { |
| @Override |
| void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException { |
| parser.createResource(elem); |
| } |
| }; |
| |
| abstract void create(UiBinderParser parser, XMLElement elem) |
| throws UnableToCompleteException; |
| } |
| |
| private static final String FLIP_RTL_ATTRIBUTE = "flipRtl"; |
| private static final String FIELD_ATTRIBUTE = "field"; |
| private static final String REPEAT_STYLE_ATTRIBUTE = "repeatStyle"; |
| private static final String SOURCE_ATTRIBUTE = "src"; |
| private static final String TYPE_ATTRIBUTE = "type"; |
| private static final String GSS_ATTRIBUTE = "gss"; |
| private static final String DO_NOT_EMBED_ATTRIBUTE = "doNotEmbed"; |
| private static final String MIME_TYPE_ATTRIBUTE = "mimeType"; |
| |
| // TODO(rjrjr) Make all the ElementParsers receive their dependencies via |
| // constructor like this one does, and make this an ElementParser. I want |
| // guice!!! |
| |
| private static final String IMPORT_ATTRIBUTE = "import"; |
| private static final String TAG = "UiBinder"; |
| private final UiBinderWriter writer; |
| private final TypeOracle oracle; |
| private final MessagesWriter messagesWriter; |
| private final FieldManager fieldManager; |
| private final ImplicitClientBundle bundleClass; |
| private final JClassType cssResourceType; |
| private final JClassType imageResourceType; |
| |
| private final JClassType dataResourceType; |
| private final String binderUri; |
| private final UiBinderContext uiBinderContext; |
| private final ResourceOracle resourceOracle; |
| private GssOptions gssOptions; |
| |
| public UiBinderParser(UiBinderWriter writer, MessagesWriter messagesWriter, |
| FieldManager fieldManager, TypeOracle oracle, ImplicitClientBundle bundleClass, |
| String binderUri, UiBinderContext uiBinderContext, ResourceOracle resourceOracle, |
| GssOptions gssOptions) { |
| this.writer = writer; |
| this.oracle = oracle; |
| this.messagesWriter = messagesWriter; |
| this.fieldManager = fieldManager; |
| this.bundleClass = bundleClass; |
| this.uiBinderContext = uiBinderContext; |
| this.cssResourceType = oracle.findType(CssResource.class.getCanonicalName()); |
| this.imageResourceType = oracle.findType(ImageResource.class.getCanonicalName()); |
| this.dataResourceType = oracle.findType(DataResource.class.getCanonicalName()); |
| this.binderUri = binderUri; |
| this.resourceOracle = resourceOracle; |
| this.gssOptions = gssOptions; |
| } |
| |
| /** |
| * Parses the root UiBinder element, and kicks off the parsing of the rest of |
| * the document. |
| */ |
| public FieldWriter parse(XMLElement elem) throws UnableToCompleteException { |
| |
| if (!writer.isBinderElement(elem)) { |
| writer.die(elem, "Bad prefix on <%s:%s>? The root element must be in " |
| + "xml namespace \"%s\" (usually with prefix \"ui:\"), " |
| + "but this has prefix \"%s\"", elem.getPrefix(), |
| elem.getLocalName(), binderUri, elem.getPrefix()); |
| } |
| |
| if (!TAG.equals(elem.getLocalName())) { |
| writer.die(elem, "Root element must be %s:%s", elem.getPrefix(), TAG); |
| } |
| |
| findResources(elem); |
| messagesWriter.findMessagesConfig(elem); |
| XMLElement uiRoot = elem.consumeSingleChildElement(); |
| return writer.parseElementToField(uiRoot); |
| } |
| |
| private JClassType consumeCssResourceType(XMLElement elem) |
| throws UnableToCompleteException { |
| String typeName = elem.consumeRawAttribute(TYPE_ATTRIBUTE, null); |
| if (typeName == null) { |
| return cssResourceType; |
| } |
| |
| return findCssResourceType(elem, typeName); |
| } |
| |
| private JClassType consumeTypeAttribute(XMLElement elem) |
| throws UnableToCompleteException { |
| if (!elem.hasAttribute(TYPE_ATTRIBUTE)) { |
| return null; |
| } |
| String resourceTypeName = elem.consumeRawAttribute(TYPE_ATTRIBUTE); |
| |
| JClassType resourceType = oracle.findType(resourceTypeName); |
| if (resourceType == null) { |
| writer.die(elem, "No such type %s", resourceTypeName); |
| } |
| |
| return resourceType; |
| } |
| |
| /** |
| * Interprets <ui:data> elements. |
| */ |
| private void createData(XMLElement elem) throws UnableToCompleteException { |
| String name = elem.consumeRequiredRawAttribute(FIELD_ATTRIBUTE); |
| String source = elem.consumeRequiredRawAttribute(SOURCE_ATTRIBUTE); |
| // doNotEmbed is optional on DataResource |
| Boolean doNotEmbed = elem.consumeBooleanConstantAttribute(DO_NOT_EMBED_ATTRIBUTE); |
| // mimeType is optional on DataResource |
| String mimeType = elem.consumeRawAttribute(MIME_TYPE_ATTRIBUTE); |
| ImplicitDataResource dataMethod = bundleClass.createDataResource( |
| name, source, mimeType, doNotEmbed); |
| FieldWriter field = fieldManager.registerField(dataResourceType, |
| dataMethod.getName()); |
| field.setInitializer(String.format("%s.%s()", |
| fieldManager.convertFieldToGetter(bundleClass.getFieldName()), |
| dataMethod.getName())); |
| } |
| |
| /** |
| * Interprets <ui:image> elements. |
| */ |
| private void createImage(XMLElement elem) throws UnableToCompleteException { |
| String name = elem.consumeRequiredRawAttribute(FIELD_ATTRIBUTE); |
| // @source is optional on ImageResource |
| String source = elem.consumeRawAttribute(SOURCE_ATTRIBUTE, null); |
| |
| Boolean flipRtl = elem.consumeBooleanConstantAttribute(FLIP_RTL_ATTRIBUTE); |
| |
| RepeatStyle repeatStyle = null; |
| if (elem.hasAttribute(REPEAT_STYLE_ATTRIBUTE)) { |
| String value = elem.consumeRawAttribute(REPEAT_STYLE_ATTRIBUTE); |
| try { |
| repeatStyle = RepeatStyle.valueOf(value); |
| } catch (IllegalArgumentException e) { |
| writer.die(elem, "Bad repeatStyle value %s", value); |
| } |
| } |
| |
| ImplicitImageResource imageMethod = bundleClass.createImageResource(name, |
| source, flipRtl, repeatStyle); |
| |
| FieldWriter field = fieldManager.registerField(imageResourceType, |
| imageMethod.getName()); |
| field.setInitializer(String.format("%s.%s()", |
| fieldManager.convertFieldToGetter(bundleClass.getFieldName()), |
| imageMethod.getName())); |
| } |
| |
| /** |
| * Process <code><ui:import field="com.example.Blah.CONSTANT"></code>. |
| */ |
| private void createImport(XMLElement elem) throws UnableToCompleteException { |
| String rawFieldName = elem.consumeRequiredRawAttribute(FIELD_ATTRIBUTE); |
| if (elem.getAttributeCount() > 0) { |
| writer.die(elem, "Should only find attribute \"%s\"", FIELD_ATTRIBUTE); |
| } |
| |
| int idx = rawFieldName.lastIndexOf('.'); |
| if (idx < 1) { |
| writer.die(elem, "Attribute %s does not look like a static import " |
| + "reference", FIELD_ATTRIBUTE); |
| } |
| String enclosingName = rawFieldName.substring(0, idx); |
| String constantName = rawFieldName.substring(idx + 1); |
| |
| JClassType enclosingType = oracle.findType(enclosingName); |
| if (enclosingType == null) { |
| writer.die(elem, "Unable to locate type %s", enclosingName); |
| } |
| |
| if ("*".equals(constantName)) { |
| for (JField field : enclosingType.getFields()) { |
| if (!field.isStatic()) { |
| continue; |
| } else if (field.isPublic()) { |
| // OK |
| } else if (field.isProtected() || field.isPrivate()) { |
| continue; |
| } else if (!enclosingType.getPackage().equals( |
| writer.getOwnerClass().getOwnerType().getPackage())) { |
| // package-protected in another package |
| continue; |
| } |
| createSingleImport(elem, enclosingType, enclosingName + "." |
| + field.getName(), field.getName()); |
| } |
| } else { |
| createSingleImport(elem, enclosingType, rawFieldName, constantName); |
| } |
| } |
| |
| /** |
| * Interprets <ui:with> elements. |
| */ |
| private void createResource(XMLElement elem) throws UnableToCompleteException { |
| String resourceName = elem.consumeRequiredRawAttribute(FIELD_ATTRIBUTE); |
| |
| JClassType resourceType = consumeTypeAttribute(elem); |
| |
| if (elem.getAttributeCount() > 0) { |
| writer.die(elem, "Should only find attributes \"%s\" and \"%s\".", FIELD_ATTRIBUTE, |
| TYPE_ATTRIBUTE); |
| } |
| |
| /* Is it a parameter passed to a render method? */ |
| |
| if (writer.isRenderer()) { |
| JClassType matchingResourceType = findRenderParameterType(resourceName); |
| if (matchingResourceType != null) { |
| createResourceUiRenderer(elem, resourceName, resourceType, matchingResourceType); |
| return; |
| } |
| } |
| |
| /* Perhaps it is provided via @UiField */ |
| |
| if (writer.getOwnerClass() == null) { |
| writer.die("No owner provided for %s", writer.getBaseClass().getQualifiedSourceName()); |
| } |
| |
| if (writer.getOwnerClass().getUiField(resourceName) != null) { |
| // If the resourceType is present, is it the same as the one in the base class? |
| OwnerField ownerField = writer.getOwnerClass().getUiField(resourceName); |
| |
| // If the resourceType was given, it must match the one declared with @UiField |
| if (resourceType != null && !resourceType.getErasedType() |
| .equals(ownerField.getType().getRawType().getErasedType())) { |
| writer.die(elem, "Type must match %s.", ownerField); |
| } |
| |
| if (ownerField.isProvided()) { |
| createResourceUiField(resourceName, ownerField); |
| return; |
| } else { |
| // Let's keep trying, but we know the type at least. |
| resourceType = ownerField.getType().getRawType().getErasedType(); |
| } |
| } |
| |
| /* Nope. If we know the type, maybe a @UiFactory will make it */ |
| |
| if (resourceType != null && writer.getOwnerClass().getUiFactoryMethod(resourceType) != null) { |
| createResourceUiFactory(elem, resourceName, resourceType); |
| return; |
| } |
| |
| /* |
| * If neither of the above, the FieldWriter's default GWT.create call will |
| * do just fine. |
| */ |
| if (resourceType != null) { |
| fieldManager.registerField(FieldWriterType.IMPORTED, resourceType, resourceName); |
| } else { |
| writer.die(elem, "Could not infer type for field %s.", resourceName); |
| } |
| |
| // process ui:attributes child for property setting |
| boolean attributesChildFound = false; |
| // Use consumeChildElements(Interpreter) so no assertEmpty check is performed |
| for (XMLElement child : elem.consumeChildElements(new SimpleInterpeter<Boolean>(true))) { |
| if (attributesChildFound) { |
| writer.die(child, "<ui:with> can only contain a single <ui:attributes> child Element."); |
| } |
| attributesChildFound = true; |
| |
| if (!elem.getNamespaceUri().equals(child.getNamespaceUri()) || !"attributes".equals(child.getLocalName())) { |
| writer.die(child, "Found unknown child element."); |
| } |
| |
| new BeanParser(uiBinderContext).parse(child, resourceName, resourceType, writer); |
| } |
| } |
| |
| private void createResourceUiFactory(XMLElement elem, String resourceName, JClassType resourceType) |
| throws UnableToCompleteException { |
| FieldWriter fieldWriter; |
| JMethod factoryMethod = writer.getOwnerClass().getUiFactoryMethod(resourceType); |
| JClassType methodReturnType = factoryMethod.getReturnType().getErasedType() |
| .isClassOrInterface(); |
| if (!resourceType.getErasedType().equals(methodReturnType)) { |
| writer.die(elem, "Type must match %s.", methodReturnType); |
| } |
| |
| String initializer; |
| if (writer.getDesignTime().isDesignTime()) { |
| String typeName = factoryMethod.getReturnType().getQualifiedSourceName(); |
| initializer = writer.getDesignTime().getProvidedFactory(typeName, |
| factoryMethod.getName(), ""); |
| } else { |
| initializer = String.format("owner.%s()", factoryMethod.getName()); |
| } |
| fieldWriter = fieldManager.registerField( |
| FieldWriterType.IMPORTED, resourceType, resourceName); |
| fieldWriter.setInitializer(initializer); |
| } |
| |
| private void createResourceUiField(String resourceName, OwnerField ownerField) |
| throws UnableToCompleteException { |
| FieldWriter fieldWriter; |
| String initializer; |
| |
| if (writer.getDesignTime().isDesignTime()) { |
| String typeName = ownerField.getType().getRawType().getQualifiedSourceName(); |
| initializer = writer.getDesignTime().getProvidedField(typeName, ownerField.getName()); |
| } else { |
| initializer = "owner." + ownerField.getName(); |
| } |
| fieldWriter = fieldManager.registerField( |
| FieldWriterType.IMPORTED, |
| ownerField.getType().getRawType().getErasedType(), |
| resourceName); |
| fieldWriter.setInitializer(initializer); |
| } |
| |
| private void createResourceUiRenderer(XMLElement elem, String resourceName, |
| JClassType resourceType, JClassType matchingResourceType) throws UnableToCompleteException { |
| FieldWriter fieldWriter; |
| if (resourceType != null |
| && !resourceType.getErasedType().isAssignableFrom(matchingResourceType.getErasedType())) { |
| writer.die(elem, "Type must match the type of parameter %s in %s#render method.", |
| resourceName, |
| writer.getBaseClass().getQualifiedSourceName()); |
| } |
| |
| fieldWriter = fieldManager.registerField( |
| FieldWriterType.IMPORTED, matchingResourceType.getErasedType(), resourceName); |
| // Sets initialization as a NOOP. These fields are set from |
| // parameters passed to UiRenderer#render(), instead. |
| fieldWriter.setInitializer(resourceName); |
| } |
| |
| private void createSingleImport(XMLElement elem, JClassType enclosingType, |
| String rawFieldName, String constantName) |
| throws UnableToCompleteException { |
| JField field = enclosingType.findField(constantName); |
| if (field == null) { |
| writer.die(elem, "Unable to locate a field named %s in %s", constantName, |
| enclosingType.getQualifiedSourceName()); |
| } else if (!field.isStatic()) { |
| writer.die(elem, "Field %s in type %s is not static", constantName, |
| enclosingType.getQualifiedSourceName()); |
| } |
| |
| JType importType = field.getType(); |
| JClassType fieldType; |
| if (importType instanceof JPrimitiveType) { |
| fieldType = oracle.findType(((JPrimitiveType) importType).getQualifiedBoxedSourceName()); |
| } else { |
| fieldType = (JClassType) importType; |
| } |
| |
| FieldWriter fieldWriter = fieldManager.registerField( |
| FieldWriterType.IMPORTED, fieldType, constantName); |
| fieldWriter.setInitializer(rawFieldName); |
| } |
| |
| private void createStyle(XMLElement elem) throws UnableToCompleteException { |
| String body = elem.consumeUnescapedInnerText(); |
| String[] source = elem.consumeRawArrayAttribute(SOURCE_ATTRIBUTE); |
| |
| if (0 == body.length() && 0 == source.length) { |
| writer.die(elem, "Must have either a src attribute or body text"); |
| } |
| |
| String name = elem.consumeRawAttribute(FIELD_ATTRIBUTE, "style"); |
| JClassType publicType = consumeCssResourceType(elem); |
| |
| String[] importTypeNames = elem.consumeRawArrayAttribute(IMPORT_ATTRIBUTE); |
| LinkedHashSet<JClassType> importTypes = new LinkedHashSet<JClassType>(); |
| for (String type : importTypeNames) { |
| importTypes.add(findCssResourceType(elem, type)); |
| } |
| |
| boolean gss = determineGssForFile(elem.consumeBooleanConstantAttribute(GSS_ATTRIBUTE)); |
| ImplicitCssResource cssMethod = bundleClass.createCssResource(name, source, |
| publicType, body, importTypes, gss, resourceOracle); |
| |
| FieldWriter field = fieldManager.registerFieldForGeneratedCssResource(cssMethod); |
| field.setInitializer(String.format("%s.%s()", |
| fieldManager.convertFieldToGetter(bundleClass.getFieldName()), |
| cssMethod.getName())); |
| } |
| |
| private boolean determineGssForFile(Boolean attributeInUiBinderFile) throws UnableToCompleteException { |
| if (attributeInUiBinderFile == null) { |
| if (!gssOptions.isEnabled() && gssOptions.isGssDefaultInUiBinder()) { |
| writer.die("Invalid combination of configuration properties. " |
| + "CssResource.enableGss is false, but CssResource.uiBinderGssDefault is true"); |
| } |
| return gssOptions.isGssDefaultInUiBinder(); |
| } |
| |
| if (Boolean.TRUE.equals(attributeInUiBinderFile)) { |
| if (!gssOptions.isEnabled()) { |
| writer.die("UiBinder file has attribute gss=\"true\", but GSS is disabled globally"); |
| } |
| return true; |
| } |
| |
| if (gssOptions.isEnabled() && gssOptions.isAutoConversionOff()) { |
| writer.die("UiBinder file has attribute gss=\"false\", " |
| + "but CssResource.conversionMode is \"off\""); |
| } |
| return false; |
| } |
| |
| private JClassType findCssResourceType(XMLElement elem, String typeName) |
| throws UnableToCompleteException { |
| JClassType publicType = oracle.findType(typeName); |
| if (publicType == null) { |
| writer.die(elem, "No such type %s", typeName); |
| } |
| |
| if (!publicType.isAssignableTo(cssResourceType)) { |
| writer.die(elem, "Type %s does not extend %s", |
| publicType.getQualifiedSourceName(), |
| cssResourceType.getQualifiedSourceName()); |
| } |
| return publicType; |
| } |
| |
| private JClassType findRenderParameterType(String resourceName) throws UnableToCompleteException { |
| JMethod renderMethod = null; |
| JClassType baseClass = writer.getBaseClass(); |
| for (JMethod method : baseClass.getInheritableMethods()) { |
| if (method.getName().equals("render")) { |
| if (renderMethod == null) { |
| renderMethod = method; |
| } else { |
| writer.die("%s declares more than one method named render", |
| baseClass.getQualifiedSourceName()); |
| } |
| } |
| } |
| if (renderMethod == null) { |
| return null; |
| } |
| JClassType matchingResourceType = null; |
| for (JParameter jParameter : renderMethod.getParameters()) { |
| if (jParameter.getName().equals(resourceName)) { |
| matchingResourceType = jParameter.getType().isClassOrInterface(); |
| break; |
| } |
| } |
| return matchingResourceType; |
| } |
| |
| private void findResources(XMLElement binderElement) |
| throws UnableToCompleteException { |
| binderElement.consumeChildElements(new XMLElement.Interpreter<Boolean>() { |
| @Override |
| public Boolean interpretElement(XMLElement elem) |
| throws UnableToCompleteException { |
| |
| if (writer.isBinderElement(elem)) { |
| try { |
| Resource.valueOf(elem.getLocalName().toUpperCase(Locale.ROOT)).create( |
| UiBinderParser.this, elem); |
| } catch (IllegalArgumentException e) { |
| writer.die(elem, |
| "Unknown tag %s, or is not appropriate as a top level element", |
| elem.getLocalName()); |
| } |
| return true; |
| } |
| return false; // leave it be |
| } |
| }); |
| } |
| } |