/*
* 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.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.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;
/**
* 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";
// 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;
public UiBinderParser(UiBinderWriter writer, MessagesWriter messagesWriter,
FieldManager fieldManager, TypeOracle oracle,
ImplicitClientBundle bundleClass, String binderUri, UiBinderContext uiBinderContext) {
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;
}
/**
* 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);
ImplicitDataResource dataMethod = bundleClass.createDataResource(name,
source);
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);
fieldWriter.setInitializer(UiBinderWriter.RENDER_PARAM_HOLDER_PREFIX + 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(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));
}
ImplicitCssResource cssMethod = bundleClass.createCssResource(name, source,
publicType, body, importTypes);
FieldWriter field = fieldManager.registerFieldForGeneratedCssResource(cssMethod);
field.setInitializer(String.format("%s.%s()",
fieldManager.convertFieldToGetter(bundleClass.getFieldName()),
cssMethod.getName()));
}
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()).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
}
});
}
}