Implements UiBinder rendering for Cells. Rendering values passed to the render() method. Retrieving ui:fields using getters Review at http://gwt-code-reviews.appspot.com/1466809 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10434 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt23_24userApi.conf b/tools/api-checker/config/gwt23_24userApi.conf index 14359f9..2801c61 100644 --- a/tools/api-checker/config/gwt23_24userApi.conf +++ b/tools/api-checker/config/gwt23_24userApi.conf
@@ -106,6 +106,7 @@ :user/src/com/google/gwt/user/client/rpc/core/**\ :user/src/com/google/gwt/user/client/rpc/impl/**\ :user/src/com/google/gwt/uibinder/attributeparsers/**\ +:user/src/com/google/gwt/uibinder/client/UiRendererUtils.java\ :user/src/com/google/gwt/uibinder/elementparsers/**\ :user/src/com/google/gwt/uibinder/testing/**\ :user/src/com/google/gwt/util/**\
diff --git a/user/src/com/google/gwt/text/shared/UiRenderer.java b/user/src/com/google/gwt/text/shared/UiRenderer.java deleted file mode 100644 index c2064b8..0000000 --- a/user/src/com/google/gwt/text/shared/UiRenderer.java +++ /dev/null
@@ -1,25 +0,0 @@ -/* - * Copyright 2011 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.text.shared; - -/** - * Marker interface for SafeHtmlRenderer implementations to be code generated - * from ui.xml files. - * - * @param <T> the type to render - */ -public interface UiRenderer<T> extends SafeHtmlRenderer<T> { -}
diff --git a/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml b/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml index 3e2b6db..749c0c9 100644 --- a/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml +++ b/user/src/com/google/gwt/uibinder/UiBinder.gwt.xml
@@ -34,7 +34,7 @@ <set-configuration-property name="UiBinder.useLazyWidgetBuilders" value="false"/> <generate-with class="com.google.gwt.uibinder.rebind.UiBinderGenerator"> - <when-type-assignable class="com.google.gwt.text.shared.UiRenderer"/> + <when-type-assignable class="com.google.gwt.uibinder.client.UiRenderer"/> </generate-with> <generate-with class="com.google.gwt.uibinder.rebind.UiBinderGenerator">
diff --git a/user/src/com/google/gwt/uibinder/client/AbstractUiRenderer.java b/user/src/com/google/gwt/uibinder/client/AbstractUiRenderer.java new file mode 100644 index 0000000..4dd7262 --- /dev/null +++ b/user/src/com/google/gwt/uibinder/client/AbstractUiRenderer.java
@@ -0,0 +1,57 @@ +/* + * Copyright 2011 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.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; + +/** + * Abstract implementation of a safe HTML binder to make implementation of generated rendering + * simpler. + * + * @param <T> the type to render + */ +public abstract class AbstractUiRenderer<T> implements UiRenderer<T> { + + /** + * Marker attribute for DOM structures previously generated by UiRenderer. + */ + public static final String RENDERED_ATTRIBUTE = "gwtuirendered"; + + /** + * Holds the part of the id attribute common to all elements being rendered. + */ + protected String uiId; + + @Override + public boolean isParentOrRenderer(Element parent) { + if (parent == null) { + return false; + } + + Element root; + try { + root = UiRendererUtilsImpl.findRootElement(parent, RENDERED_ATTRIBUTE); + return UiRendererUtilsImpl.isAttachedToDom(root) + && UiRendererUtilsImpl.isRenderedElementSingleChild(root); + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public void onBrowserEvent(T cell, Element parent, NativeEvent event) { + // Trivial implementation when no @UiHandlers + } +}
diff --git a/user/src/com/google/gwt/uibinder/client/UiRenderer.java b/user/src/com/google/gwt/uibinder/client/UiRenderer.java new file mode 100644 index 0000000..d8bfc0a --- /dev/null +++ b/user/src/com/google/gwt/uibinder/client/UiRenderer.java
@@ -0,0 +1,46 @@ +/* + * Copyright 2011 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.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; + +/** + * Marker interface for classes whose implementation is to be provided via UiBinder code + * generation for SafeHtml rendering. + * + * @param <T> the type to render + */ +public interface UiRenderer<T> { + + /** + * Checks whether {@code parent} is a valid element to use as an argument for field getters. + * + * @return {@code true} if parent contains or directly points to a previously rendered element. + * In DevMode it also checks whether the parent is attached to the DOM + */ + boolean isParentOrRenderer(Element parent); + + /** + * Receives an event and dispatches it to the appropriate handler method annotated + * with {@code @UiHandler}. + * + * @param receiver of events + * @param parent element containing a previously rendered element + * @param event received by the browser + */ + void onBrowserEvent(T receiver, Element parent, NativeEvent event); +}
diff --git a/user/src/com/google/gwt/uibinder/client/UiRendererUtilsImpl.java b/user/src/com/google/gwt/uibinder/client/UiRendererUtilsImpl.java new file mode 100644 index 0000000..b59a48c --- /dev/null +++ b/user/src/com/google/gwt/uibinder/client/UiRendererUtilsImpl.java
@@ -0,0 +1,167 @@ +/* + * Copyright 2011 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.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; + +/** + * UiRenderer utilities. + */ +public class UiRendererUtilsImpl { + + /** + * Build id strings used to identify DOM elements related to ui:fields. + * + * @param fieldName name of the field that identifies the element + * @param uiId common part of the identifier for all elements in the rendered DOM structure + */ + public static String buildInnerId(String fieldName, String uiId) { + return uiId + ":" + fieldName; + } + + /** + * Retrieves a specific element within a previously rendered element. + * + * @param parent parent element containing the element of interest + * @param fieldName name of the field to retrieve + * @param attribute that identifies the root element as such + * @return the element identified by {@code fieldName} + * + * @throws IllegalArgumentException if the {@code parent} does not point to or contains + * a previously rendered element. In DevMode also when the root element is not + * attached to the DOM + * @throws IllegalStateException parent does not contain an element matching + * {@code filedName} + * + * @throws RuntimeException if the root element is not attached to the DOM and not running in + * DevMode + * + * @throws NullPointerException if {@code parent} == null + */ + public static Element findInnerField(Element parent, String fieldName, String attribute) { + Element root = findRootElement(parent, attribute); + + if (parent != root && !isRenderedElementSingleChild(root)) { + throw new IllegalArgumentException( + "Parent Element of previously rendered element contains more than one child" + + " while getting \"" + fieldName + "\""); + } + + String uiId = root.getAttribute(attribute); + String renderedId = buildInnerId(fieldName, uiId); + + Element elementById = Document.get().getElementById(renderedId); + if (elementById == null) { + if (!isAttachedToDom(root)) { + throw new RuntimeException("UiRendered element is not attached to DOM while getting \"" + + fieldName + "\""); + } else if (!GWT.isProdMode()) { + throw new IllegalStateException("\"" + fieldName + + "\" not found within rendered element"); + } else { + // In prod mode we do not distinguish between being unattached or not finding the element + throw new IllegalArgumentException("UiRendered element is not attached to DOM, or \"" + + fieldName + "\" not found within rendered element"); + } + } + return elementById; + } + + /** + * Retrieves the root of a previously rendered element contained within the {@code parent}. + * The {@code parent} must either contain the previously rendered DOM structure as its only child, + * or point directly to the rendered element root. + * + * @param parent element containing, or pointing to, a previously rendered DOM structure + * @param attribute attribute name that identifies the root of the DOM structure + * @return the root element of the previously rendered DOM structure + * + * @throws NullPointerException if {@code parent} == null + * @throws IllegalArgumentException if {@code parent} does not contain a previously rendered + * element + */ + public static Element findRootElement(Element parent, String attribute) { + if (parent == null) { + throw new NullPointerException("parent argument is null"); + } + + Element rendered; + if (parent.hasAttribute(attribute)) { + // The parent is the root + return parent; + } else if ((rendered = parent.getFirstChildElement()) != null + && rendered.hasAttribute(attribute)) { + // The first child is the root + return rendered; + } else { + throw new IllegalArgumentException( + "Parent element does not contain a previously rendered element"); + } + } + + /** + * Walks up the parents of the {@code rendered} element to ascertain that it is attached to the + * document. + */ + public static boolean isAttachedToDom(Element rendered) { + if (GWT.isProdMode()) { + return true; + } + + Element body = Document.get().getBody(); + + while (rendered != null && rendered.hasParentElement() && !body.equals(rendered)) { + rendered = rendered.getParentElement(); + } + return body.equals(rendered); + }; + + /** + * Checks that the parent of {@code rendered} has a single child. + */ + public static boolean isRenderedElementSingleChild(Element rendered) { + return GWT.isProdMode() || rendered.getParentElement().getChildCount() == 1; + } + + /** + * Inserts an attribute into the first tag found in a {@code safeHtml} template. + * This method assumes that the {@code safeHtml} template begins with an open HTML tag. + * {@code SafeHtml} templates produced by UiBinder always meet these conditions. + * <p> + * This method does not attempt to ensure {@code atributeName} and {@code attributeValue} + * contain safe values. + * + * @returns the {@code safeHtml} template with "{@code attributeName}={@code attributeValue}" + * inserted as an attribute of the first tag found + */ + public static SafeHtml stampUiRendererAttribute(SafeHtml safeHtml, String attributeName, + String attributeValue) { + String html = safeHtml.asString(); + int endOfFirstTag = html.indexOf(">"); + + assert endOfFirstTag > 1 : "Safe html template does not start with an HTML open tag"; + + if (html.charAt(endOfFirstTag - 1) == '/') { + endOfFirstTag--; + } + + html = html.substring(0, endOfFirstTag) + " " + attributeName + "=\"" + attributeValue + "\"" + + html.substring(endOfFirstTag); + return SafeHtmlUtils.fromTrustedString(html); + } +}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java index c10083c..9cb1032 100644 --- a/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java +++ b/user/src/com/google/gwt/uibinder/elementparsers/WidgetInterpreter.java
@@ -82,7 +82,7 @@ // that idHolder is a local variable reference, not a string id. We // have to generate the ids at runtime, not compile time, or else // we'll reuse ids for any template rendered more than once. - String idHolder = uiWriter.declareDomIdHolder(); + String idHolder = uiWriter.declareDomIdHolder(null); uiWriter.ensureCurrentFieldAttached(); FieldWriter childFieldWriter = uiWriter.parseElementToFieldWriter(elem);
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java b/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java index 5af8f3a..f335218 100644 --- a/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java +++ b/user/src/com/google/gwt/uibinder/elementparsers/WidgetPlaceholderInterpreter.java
@@ -97,7 +97,7 @@ name = "widget" + (++serial); } - String idHolder = uiWriter.declareDomIdHolder(); + String idHolder = uiWriter.declareDomIdHolder(null); idToWidgetElement.put(idHolder, elem); if (oracle.findType(HasHTML.class.getName()).isAssignableFrom(type)) {
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java index 0960b06..610c07e 100644 --- a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java +++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
@@ -19,6 +19,7 @@ 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; @@ -41,7 +42,7 @@ */ public class UiBinderParser { - private enum Resource { + enum Resource { DATA { @Override void create(UiBinderParser parser, XMLElement elem) @@ -153,7 +154,10 @@ private JClassType consumeTypeAttribute(XMLElement elem) throws UnableToCompleteException { - String resourceTypeName = elem.consumeRequiredRawAttribute(TYPE_ATTRIBUTE); + if (!elem.hasAttribute(TYPE_ATTRIBUTE)) { + return null; + } + String resourceTypeName = elem.consumeRawAttribute(TYPE_ATTRIBUTE); JClassType resourceType = oracle.findType(resourceTypeName); if (resourceType == null) { @@ -256,56 +260,122 @@ */ 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 \"field\" and \"type\""); + writer.die(elem, "Should only find attributes \"%s\" and \"%s\".", FIELD_ATTRIBUTE, + TYPE_ATTRIBUTE); } - FieldWriter fieldWriter = fieldManager.registerField( - FieldWriterType.IMPORTED, resourceType, resourceName); - OwnerField ownerField = writer.getOwnerClass().getUiField(resourceName); + /* Is it a parameter passed to a render method? */ - /* Perhaps it is provided via @UiField */ - - if (ownerField != null) { - if (!resourceType.getErasedType().equals(ownerField.getType().getRawType() - .getErasedType())) { - writer.die(elem, "Type must match %s", ownerField); - } - if (ownerField.isProvided()) { - 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.setInitializer(initializer); + if (writer.isRenderer()) { + JClassType matchingResourceType = findRenderParameterType(resourceName); + if (matchingResourceType != null) { + createResourceUiRenderer(elem, resourceName, resourceType, matchingResourceType); return; } } - /* Nope. Maybe a @UiFactory will make it */ + /* Perhaps it is provided via @UiField */ - JMethod factoryMethod = writer.getOwnerClass().getUiFactoryMethod( - resourceType); - if (factoryMethod != null) { - 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()); + 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); } - fieldWriter.setInitializer(initializer); + + if (ownerField.isProvided()) { + createResourceUiField(resourceName, resourceType, 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); + } + } + + 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, JClassType resourceType, + 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("this." + resourceName); } private void createSingleImport(XMLElement elem, JClassType enclosingType, @@ -374,6 +444,27 @@ return publicType; } + private JClassType findRenderParameterType(String resourceName) { + JMethod renderMethod = null; + for (JMethod method : writer.getBaseClass().getMethods()) { + if (method.getName().equals("render")) { + renderMethod = method; + break; + } + } + 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>() {
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java index 0649c5d..255fcf0 100644 --- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java +++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -17,19 +17,24 @@ 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.JParameterizedType; +import com.google.gwt.core.ext.typeinfo.JPrimitiveType; import com.google.gwt.core.ext.typeinfo.JTypeParameter; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.dom.client.TagName; import com.google.gwt.resources.client.ClientBundle; -import com.google.gwt.text.shared.UiRenderer; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.uibinder.attributeparsers.AttributeParser; import com.google.gwt.uibinder.attributeparsers.AttributeParsers; import com.google.gwt.uibinder.attributeparsers.BundleAttributeParser; import com.google.gwt.uibinder.attributeparsers.BundleAttributeParsers; +import com.google.gwt.uibinder.client.AbstractUiRenderer; import com.google.gwt.uibinder.client.LazyDomElement; import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiRenderer; import com.google.gwt.uibinder.elementparsers.AttributeMessageParser; import com.google.gwt.uibinder.elementparsers.BeanParser; import com.google.gwt.uibinder.elementparsers.ElementParser; @@ -48,9 +53,11 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import java.beans.Introspector; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -169,6 +176,67 @@ return propName.substring(0, 1).toUpperCase() + propName.substring(1); } + /** + * Scan the base class for the getter methods. Assumes getters begin with + * "get". See {@link #validateRendererGetters(JClassType)} for a method that + * guarantees this method will succeed. + */ + private static List<JMethod> findGetterNames(JClassType owner) { + List<JMethod> ret = new ArrayList<JMethod>(); + for (JMethod jMethod : owner.getMethods()) { + String getterName = jMethod.getName(); + if (getterName.startsWith("get")) { + ret.add(jMethod); + } + } + return ret; + } + + /** + * Scans a class for a method named "render". Returns its parameters except for the + * first one. See {@link #validateRenderParameters(JClassType)} for a method that + * guarantees this method will succeed. + */ + private static JParameter[] findRenderParameters(JClassType owner) { + JMethod[] methods = owner.getMethods(); + JMethod renderMethod = null; + + for (JMethod jMethod : methods) { + if (jMethod.getName().equals("render")) { + renderMethod = jMethod; + } + } + + JParameter[] parameters = renderMethod.getParameters(); + return Arrays.copyOfRange(parameters, 1, parameters.length); + } + + /** + * Determine the field name a getter is trying to retrieve. Assumes getters begin with + * "get". + */ + private static String getterToFieldName(String name) { + String fieldName = name.substring(3); + return Introspector.decapitalize(fieldName); + } + + private static String renderMethodParameters(JParameter[] renderParameters) { + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < renderParameters.length; i++) { + JParameter parameter = renderParameters[i]; + builder.append("final "); + builder.append(parameter.getType().getQualifiedSourceName()); + builder.append(" "); + builder.append(parameter.getName()); + if (i < renderParameters.length - 1) { + builder.append(", "); + } + } + + return builder.toString(); + } + private final MortalLogger logger; /** @@ -412,7 +480,7 @@ public String declareDomField(String fieldName, String ancestorField) throws UnableToCompleteException { ensureAttached(); - String name = declareDomIdHolder(); + String name = declareDomIdHolder(fieldName); if (useLazyWidgetBuilders) { // Create and initialize the dom field with LazyDomElement. @@ -450,15 +518,23 @@ /** * Declare a variable that will be filled at runtime with a unique id, safe - * for use as a dom element's id attribute. + * for use as a dom element's id attribute. For {@code UiRenderer} based code, + * elements corresponding to a ui:field, need and id initialized to a + * value that depends on the {@code fieldName}. For all other cases let {@code fieldName} + * be {@code null}. * + * @param fieldName name of the field corresponding to this variable. * @return that variable's name. */ - public String declareDomIdHolder() throws UnableToCompleteException { + public String declareDomIdHolder(String fieldName) throws UnableToCompleteException { String domHolderName = "domId" + domId++; FieldWriter domField = fieldManager.registerField(FieldWriterType.DOM_ID_HOLDER, oracle.findType(String.class.getName()), domHolderName); - domField.setInitializer("com.google.gwt.dom.client.Document.get().createUniqueId()"); + if (isRenderer && fieldName != null) { + domField.setInitializer("UiRendererUtilsImpl.buildInnerId(\"" + fieldName + "\", uiId)"); + } else { + domField.setInitializer("com.google.gwt.dom.client.Document.get().createUniqueId()"); + } return domHolderName; } @@ -689,6 +765,13 @@ } /** + * The type we have been asked to generated, e.g. MyUiBinder + */ + public JClassType getBaseClass() { + return baseClass; + } + + /** * Finds an attribute {@link BundleAttributeParser} for the given xml * attribute, if any, based on its namespace uri. * @@ -814,6 +897,10 @@ return findFieldType(elem).isAssignableTo(isRenderableClassType); } + public boolean isRenderer() { + return isRenderer; + } + public boolean isWidgetElement(XMLElement elem) throws UnableToCompleteException { return isElementAssignableTo(elem, IsWidget.class); } @@ -1326,6 +1413,74 @@ } /** + * Scan the base class for the getter methods. Assumes getters begin with + * "get" and validates that each corresponds to a field declared with {@code ui:field}, + * it has a single parameter, the parameter type is assignable to {@code Element} and + * its return type is assignable to {@code Element}. + */ + private void validateRendererGetters(JClassType owner) throws UnableToCompleteException { + for (JMethod jMethod : owner.getMethods()) { + String getterName = jMethod.getName(); + if (getterName.startsWith("get")) { + if (jMethod.getParameterTypes().length != 1) { + die("Getter %s must have exactly one parameter", + getterName); + } + String elementClassName = com.google.gwt.dom.client.Element.class.getCanonicalName(); + JClassType elementType = oracle.findType(elementClassName); + JClassType getterParamType = jMethod.getParameterTypes()[0].getErasedType() + .isClassOrInterface(); + + if (!elementType.isAssignableFrom(getterParamType)) { + die("Getter %s must have exactly one parameter of type assignable to %s", + getterName, + elementClassName); + } + String fieldName = getterToFieldName(getterName); + FieldWriter field = fieldManager.lookup(fieldName); + if (field == null || !FieldWriterType.DEFAULT.equals(field.getFieldType())) { + die("%s does not match a \"ui:field='%s'\" declaration", getterName, fieldName); + } + } else if (!getterName.equals("render")) { + die("Unexpected method \"%s\" found", getterName); + } + } + } + + /** + * Scans a class to validate that it contains a single method called render, + * which has a {@code void} return type, and its first parameter is of type + * {@code SafeHtmlBuilder}. + */ + private void validateRenderParameters(JClassType owner) throws UnableToCompleteException { + JMethod[] methods = owner.getMethods(); + JMethod renderMethod = null; + + for (JMethod jMethod : methods) { + if (jMethod.getName().equals("render")) { + if (renderMethod == null) { + renderMethod = jMethod; + } else { + die("%s declares more than one method named render", + baseClass.getQualifiedSourceName()); + } + } + } + + if (renderMethod == null + || renderMethod.getParameterTypes().length < 1 + || !renderMethod.getParameterTypes()[0].getErasedType().getQualifiedSourceName().equals( + SafeHtmlBuilder.class.getCanonicalName())) { + die("%s does not declare a render(SafeHtmlBuilder ...) method", + baseClass.getQualifiedSourceName()); + } + if (!JPrimitiveType.VOID.equals(renderMethod.getReturnType())) { + die("%s#render(SafeHtmlBuilder ...) does not return void", + baseClass.getQualifiedSourceName()); + } + } + + /** * Write statements that parsers created via calls to {@link #addStatement}. * Such statements will assume that {@link #writeGwtFields} has already been * called. @@ -1462,7 +1617,8 @@ uiOwnerType.getParameterizedQualifiedSourceName(), baseClass.getParameterizedQualifiedSourceName()); } else { - w.write("public class %s extends AbstractSafeHtmlRenderer<%s> implements %s {", implClassName, + w.write("public class %s extends %s<%s> implements %s {", implClassName, + AbstractUiRenderer.class.getName(), uiOwnerType.getParameterizedQualifiedSourceName(), baseClass.getParameterizedQualifiedSourceName()); } @@ -1537,6 +1693,7 @@ uiRootType.getName()); } else { w.write("import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;"); + w.write("import com.google.gwt.uibinder.client.UiRendererUtilsImpl;"); } } @@ -1600,11 +1757,13 @@ } /** - * Writes the SafeHtmlRenderer's source for the renderable + * Writes the UiRenderer's source for the renderable * strategy. */ - private void writeRenderer( - IndentedWriter w, String rootField) throws UnableToCompleteException { + private void writeRenderer(IndentedWriter w, String rootField) throws UnableToCompleteException { + validateRendererGetters(baseClass); + validateRenderParameters(baseClass); + writePackage(w); writeImports(w); @@ -1619,56 +1778,89 @@ w.newline(); - w.write("public SafeHtml render(final %s owner) {", - uiOwnerType.getParameterizedQualifiedSourceName()); + JParameter[] renderParameters = findRenderParameters(baseClass); + + writeRenderParameterDefinitions(w, renderParameters); + + String renderParameterDeclarations = renderMethodParameters(renderParameters); + w.write("public void render(final %s sb%s%s) {", SafeHtmlBuilder.class.getName(), + renderParameterDeclarations.length() != 0 ? ", " : "", + renderParameterDeclarations); w.indent(); w.newline(); - w.write("return new Widgets(owner).getSafeHtml();"); - w.outdent(); + writeRenderParameterInitializers(w, renderParameters); - w.write("}"); + w.write("uiId = com.google.gwt.dom.client.Document.get().createUniqueId();"); w.newline(); - // Writes the inner class Widgets. - w.newline(); - w.write("/**"); - w.write(" * Encapsulates the access to all inner widgets"); - w.write(" */"); - w.write("class Widgets {"); - w.indent(); - - String ownerClassType = uiOwnerType.getParameterizedQualifiedSourceName(); - w.write("private final %s owner;", ownerClassType); - w.newline(); - - w.write("public Widgets(final %s owner) {", ownerClassType); - w.indent(); - w.write("this.owner = owner;"); fieldManager.initializeWidgetsInnerClass(w, getOwnerClass()); + w.newline(); + + // TODO(rchandia) Find a better way to get the root field name + String rootFieldName = rootField.substring(4, rootField.length() - 2); + String safeHtml = fieldManager.lookup(rootFieldName).getSafeHtml(); + + // TODO(rchandia) it should be possible to add the attribute when parsing the UiBinder file + w.write("sb.append(UiRendererUtilsImpl.stampUiRendererAttribute(%s, RENDERED_ATTRIBUTE, uiId));", + safeHtml); w.outdent(); + w.write("}"); w.newline(); - w.write("public SafeHtml getSafeHtml() {"); - w.indent(); - // TODO Find a better way to get the root field name - String safeHtml = fieldManager.lookup(rootField.substring(4, rootField.length() - 2)).getSafeHtml(); - w.write("return %s;", safeHtml); - w.outdent(); - w.write("}"); + fieldManager.writeFieldDefinitions(w, getOracle(), getOwnerClass(), getDesignTime()); - fieldManager.writeFieldDefinitions( - w, getOracle(), getOwnerClass(), getDesignTime()); - - w.outdent(); - w.write("}"); + writeRendererGetters(w, baseClass, rootFieldName); // Close class w.outdent(); w.write("}"); } + private void writeRendererGetters(IndentedWriter w, JClassType owner, String rootFieldName) + throws UnableToCompleteException { + List<JMethod> getters = findGetterNames(owner); + + // For every requested getter + for (JMethod getter : getters) { + // public ElementSubclass getFoo(Element parent) { + w.write("%s {", getter.getReadableDeclaration(false, false, false, false, true)); + w.indent(); + String elementParameter = getter.getParameters()[0].getName(); + String getterFieldName = getterToFieldName(getter.getName()); + // The non-root elements are found by id + if (!getterFieldName.equals(rootFieldName)) { + // return (ElementSubclass) findUiField(parent); + w.write("return (%s) UiRendererUtilsImpl.findInnerField(%s, \"%s\", RENDERED_ATTRIBUTE);", + getter.getReturnType().getErasedType().getQualifiedSourceName(), elementParameter, + getterFieldName); + } else { + // return (ElementSubclass) findPreviouslyRendered(parent); + w.write("return (%s) UiRendererUtilsImpl.findRootElement(%s, RENDERED_ATTRIBUTE);", + getter.getReturnType().getErasedType().getQualifiedSourceName(), elementParameter); + } + w.outdent(); + w.write("}"); + } + } + + private void writeRenderParameterDefinitions(IndentedWriter w, JParameter[] renderParameters) { + for (int i = 0; i < renderParameters.length; i++) { + JParameter parameter = renderParameters[i]; + w.write("private %s %s;", parameter.getType().getQualifiedSourceName(), parameter.getName()); + w.newline(); + } + } + + private void writeRenderParameterInitializers(IndentedWriter w, JParameter[] renderParameters) { + for (int i = 0; i < renderParameters.length; i++) { + JParameter parameter = renderParameters[i]; + w.write("this.%s = %s;", parameter.getName(), parameter.getName()); + w.newline(); + } + } + /** * Write statements created by {@link HtmlTemplates#addSafeHtmlTemplate}. This * code must be placed after all instantiation code.
diff --git a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java index d6e2dc0..5bf6ee3 100644 --- a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java +++ b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
@@ -53,6 +53,8 @@ import com.google.gwt.uibinder.rebind.HandlerEvaluatorTest; import com.google.gwt.uibinder.rebind.TokenatorTest; import com.google.gwt.uibinder.rebind.TypeOracleUtilsTest; +import com.google.gwt.uibinder.rebind.UiBinderParserUiWithTest; +import com.google.gwt.uibinder.rebind.UiRendererValidationTest; import com.google.gwt.uibinder.rebind.XMLElementTest; import com.google.gwt.uibinder.rebind.model.OwnerClassTest; import com.google.gwt.uibinder.rebind.model.OwnerFieldClassTest; @@ -78,6 +80,8 @@ suite.addTestSuite(XMLElementTest.class); suite.addTestSuite(DesignTimeUtilsTest.class); suite.addTestSuite(TypeOracleUtilsTest.class); + suite.addTestSuite(UiBinderParserUiWithTest.class); + suite.addTestSuite(UiRendererValidationTest.class); // model suite.addTestSuite(OwnerClassTest.class);
diff --git a/user/test/com/google/gwt/uibinder/rebind/AbstractUiBinderWriterTest.java b/user/test/com/google/gwt/uibinder/rebind/AbstractUiBinderWriterTest.java new file mode 100644 index 0000000..52eb316 --- /dev/null +++ b/user/test/com/google/gwt/uibinder/rebind/AbstractUiBinderWriterTest.java
@@ -0,0 +1,188 @@ +/* + * Copyright 2011 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.codegen.server.AbortablePrintWriter; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.google.gwt.dev.javac.CompilationState; +import com.google.gwt.dev.javac.CompilationStateBuilder; +import com.google.gwt.dev.javac.testing.impl.MockJavaResource; +import com.google.gwt.dev.javac.testing.impl.MockResourceOracle; +import com.google.gwt.dev.resource.Resource; +import com.google.gwt.dev.util.collect.HashSet; +import com.google.gwt.dev.util.log.PrintWriterTreeLogger; +import com.google.gwt.uibinder.attributeparsers.AttributeParsers; +import com.google.gwt.uibinder.attributeparsers.BundleAttributeParsers; +import com.google.gwt.uibinder.rebind.messages.MessagesWriter; +import com.google.gwt.uibinder.rebind.model.OwnerClass; +import com.google.gwt.uibinder.test.UiJavaResources; + +import junit.framework.TestCase; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXParseException; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Set; + +/** + * Base class for tests that exercise {@link UiBinderWriter}. + */ +public abstract class AbstractUiBinderWriterTest extends TestCase { + + public static final MockJavaResource CLIENT_BUNDLE = new MockJavaResource( + "com.google.gwt.resources.client.ClientBundle") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.resources.client;\n"); + code.append("public interface ClientBundle {"); + code.append("}"); + return code; + } + }; + public static final MockJavaResource DIV_ELEMENT = new MockJavaResource( + "com.google.gwt.dom.client.DivElement") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.dom.client;\n"); + code.append("public class DivElement extends Element {"); + code.append("}"); + return code; + } + }; + public static final MockJavaResource FOO = new MockJavaResource("foo.Foo") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package foo;"); + code.append("public class Foo {"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource RENDERABLE_PANEL = new MockJavaResource( + "com.google.gwt.user.client.ui.RenderablePanel") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.user.client.ui;\n"); + code.append("public class RenderablePanel {"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource SPAN_ELEMENT = new MockJavaResource( + "com.google.gwt.dom.client.SpanElement") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.dom.client;\n"); + code.append("public class SpanElement extends Element {"); + code.append("}"); + return code; + } + }; + + protected static final String BINDER_URI = "urn:ui:com.google.gwt.uibinder"; + + protected static final W3cDomHelper docHelper = new W3cDomHelper(TreeLogger.NULL, + new MockResourceOracle()); + + protected static final String RENDERER_BASE_CLASS_NAME = "renderer.OwnerClass.Renderer"; + + protected static final String RENDERER_OWNER_CLASS_NAME = "renderer.OwnerClass"; + + private static TreeLogger createCompileLogger() { + PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(System.err, true)); + logger.setMaxDetail(TreeLogger.ERROR); + return logger; + } + + protected Document doc; + protected PrintWriter printWriter; + protected UiBinderWriter writer; + protected UiBinderParser parser; + protected BundleAttributeParsers attributeParsers; + protected XMLElementProvider elemProvider; + protected XMLElement elm; + protected FieldManager fieldManager; + protected Element item; + protected MockMortalLogger logger; + protected Set<Resource> resources = new HashSet<Resource>(); + protected TypeOracle types; + + public AbstractUiBinderWriterTest() { + super(); + } + + /** + * @param name + */ + public AbstractUiBinderWriterTest(String name) { + super(name); + } + + @SuppressWarnings("deprecation") + @Override + public void setUp() throws Exception { + super.setUp(); + resources.addAll(UiJavaResources.getUiResources()); + printWriter = new AbortablePrintWriter(new PrintWriter(new StringWriter())); + } + + protected void init(String domString, MockJavaResource rendererClass) throws SAXParseException, + UnableToCompleteException { + resources.add(RENDERABLE_PANEL); + resources.add(CLIENT_BUNDLE); + resources.add(DIV_ELEMENT); + resources.add(SPAN_ELEMENT); + resources.add(FOO); + resources.add(rendererClass); + CompilationState state = CompilationStateBuilder.buildFrom(createCompileLogger(), resources); + types = state.getTypeOracle(); + logger = new MockMortalLogger(); + JClassType ownerType = types.findType(RENDERER_OWNER_CLASS_NAME); + UiBinderContext uiBinderCtx = new UiBinderContext(); + attributeParsers = + new BundleAttributeParsers(types, logger, new OwnerClass(ownerType, logger, uiBinderCtx), + "templatePath", ownerType); + fieldManager = new FieldManager(types, logger, true); + String baseClass = RENDERER_BASE_CLASS_NAME; + DesignTimeUtils designTime = DesignTimeUtilsStub.EMPTY; + elemProvider = + new XMLElementProviderImpl(new AttributeParsers(types, fieldManager, logger), + attributeParsers, types, logger, designTime); + doc = docHelper.documentFor(domString, rendererClass.getPath()); + item = (Element) doc.getDocumentElement().getChildNodes().item(0); + elm = elemProvider.get(item); + JClassType aClass = types.findType(baseClass); + MessagesWriter messages = + new MessagesWriter(types, BINDER_URI, logger, rendererClass.getPath(), "rendererPackage", + "rendererClassName"); + writer = + new UiBinderWriter(aClass, "foo", "", types, logger, fieldManager, messages, + DesignTimeUtilsStub.EMPTY, uiBinderCtx, true, true, BINDER_URI); + parser = new UiBinderParser(writer, messages, fieldManager, types, null, BINDER_URI); + designTime.rememberPathForElements(doc); + } +} \ No newline at end of file
diff --git a/user/test/com/google/gwt/uibinder/rebind/UiBinderParserUiWithTest.java b/user/test/com/google/gwt/uibinder/rebind/UiBinderParserUiWithTest.java new file mode 100644 index 0000000..16a5e5d --- /dev/null +++ b/user/test/com/google/gwt/uibinder/rebind/UiBinderParserUiWithTest.java
@@ -0,0 +1,287 @@ +/* + * Copyright 2011 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.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.google.gwt.dev.javac.CompilationState; +import com.google.gwt.dev.javac.CompilationStateBuilder; +import com.google.gwt.dev.javac.testing.impl.MockJavaResource; +import com.google.gwt.dev.javac.testing.impl.MockResourceOracle; +import com.google.gwt.dev.resource.Resource; +import com.google.gwt.dev.util.collect.HashSet; +import com.google.gwt.dev.util.log.PrintWriterTreeLogger; +import com.google.gwt.uibinder.attributeparsers.AttributeParsers; +import com.google.gwt.uibinder.attributeparsers.BundleAttributeParsers; +import com.google.gwt.uibinder.test.UiJavaResources; + +import junit.framework.TestCase; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXParseException; + +import java.io.PrintWriter; +import java.util.Set; + +/** + * Tests UiBinderParser behavior for <ui:with> tag. + */ +public class UiBinderParserUiWithTest extends TestCase { + + public static final MockJavaResource BAR = new MockJavaResource("bar.Bar") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package bar;"); + code.append("public class Bar {"); + code.append(" public Bar(int a) { }"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource BINDER = new MockJavaResource("binder.OwnerClass") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package binder;\n"); + code.append("import com.google.gwt.uibinder.client.UiBinder;\n"); + code.append("import com.google.gwt.uibinder.client.UiField;\n"); + code.append("import com.google.gwt.uibinder.client.UiFactory;\n"); + code.append("import bar.Bar;\n"); + code.append("import foo.Foo;\n"); + code.append("public class OwnerClass {"); + code.append(" public interface Binder"); + code.append(" extends UiBinder<java.lang.String, OwnerClass> {"); + code.append(" }"); + code.append(" @UiField foo.Foo fieldName;"); + code.append(" @UiFactory bar.Bar aFactory() { return new Bar(1); }"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource FOO = new MockJavaResource("foo.Foo") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package foo;"); + code.append("public class Foo {"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource FOOISH = new MockJavaResource("foo.Fooish") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package foo;"); + code.append("public class Fooish extends Foo {"); + code.append("}"); + return code; + } + }; + + public static final MockJavaResource RENDERER = new MockJavaResource("renderer.OwnerClass") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package renderer;\n"); + code.append("import com.google.gwt.safehtml.shared.SafeHtmlBuilder;\n"); + code.append("import com.google.gwt.uibinder.client.UiRenderer;\n"); + code.append("import foo.Foo;\n"); + code.append("public class OwnerClass {"); + code.append(" public interface Renderer"); + code.append(" extends UiRenderer<OwnerClass> {"); + code.append(" public void render(SafeHtmlBuilder sb, foo.Fooish fieldName);"); + code.append(" }"); + code.append("}"); + return code; + } + }; + + private static final W3cDomHelper docHelper = new W3cDomHelper(TreeLogger.NULL, + new MockResourceOracle()); + + private static TreeLogger createCompileLogger() { + PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(System.err, true)); + logger.setMaxDetail(TreeLogger.ERROR); + return logger; + } + + UiBinderParser parser; + private BundleAttributeParsers attributeParsers; + private Document doc; + private XMLElementProvider elemProvider; + + private XMLElement elm; + + private FieldManager fieldManager; + + private Element item; + + private MockMortalLogger logger; + + private Set<Resource> resources = new HashSet<Resource>(); + + private TypeOracle types; + + private UiBinderWriter writer; + + @SuppressWarnings("deprecation") + @Override + public void setUp() throws Exception { + super.setUp(); + resources.addAll(UiJavaResources.getUiResources()); + resources.add(FOO); + resources.add(FOOISH); + resources.add(BAR); + resources.add(RENDERER); + resources.add(BINDER); + CompilationState state = CompilationStateBuilder.buildFrom(createCompileLogger(), resources); + types = state.getTypeOracle(); + logger = new MockMortalLogger(); + attributeParsers = new BundleAttributeParsers(types, logger, null, "templatePath", null); + fieldManager = new FieldManager(types, logger, true); + } + + public void testWithErrorMisTyped() throws SAXParseException { + try { + init("<doc><with field=\"fieldName\" type=\"bar.Bar\" bloop=\"\"/></doc>", + "renderer.OwnerClass.Renderer"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithErrorNoField() throws SAXParseException { + try { + init("<doc><with type=\"bar.Bar\"/></doc>", "renderer.OwnerClass.Renderer"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithErrorUiBinderBadUiFieldType() throws SAXParseException { + try { + init("<doc><with field=\"someName\" type=\"foo.Unknown\"/></doc>", + "binder.OwnerClass.Binder"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithErrorUiBinderMisTypedUiField() throws SAXParseException { + try { + init("<doc><with field=\"fieldName\" type=\"bar.Bar\"/></doc>", "binder.OwnerClass.Binder"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithErrorUiRendererMisTypedImport() throws SAXParseException { + try { + init("<doc><with field=\"fieldName\" type=\"bar.Bar\"/></doc>", + "renderer.OwnerClass.Renderer"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithErrorUiRendererUnTypedNonExistingFieldName() throws SAXParseException { + try { + init("<doc><with field=\"nonExisting\"/></doc>", "renderer.OwnerClass.Renderer"); + fail("Expecting UnabletoCompleteException"); + } catch (UnableToCompleteException e) { + assertNotNull(logger.died); + } + } + + public void testWithGwtCreated() throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"notAField\" type=\"foo.Foo\"/></doc>", "renderer.OwnerClass.Renderer"); + + assertNotNull(fieldManager.lookup("notAField")); + assertEquals("foo.Foo", fieldManager.lookup("notAField").getAssignableType() + .getQualifiedSourceName()); + } + + public void testWithUiBinderTypedUiField() throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"fieldName\" type=\"foo.Foo\"/></doc>", "binder.OwnerClass.Binder"); + + assertNotNull(fieldManager.lookup("fieldName")); + assertEquals("foo.Foo", fieldManager.lookup("fieldName").getAssignableType() + .getQualifiedSourceName()); + } + + public void testWithUiBinderUiFactory() throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"factoryProvided\" type=\"bar.Bar\"/></doc>", + "binder.OwnerClass.Binder"); + + assertNotNull(fieldManager.lookup("factoryProvided")); + assertEquals("bar.Bar", fieldManager.lookup("factoryProvided").getAssignableType() + .getQualifiedSourceName()); + } + + public void testWithUiBinderUntypedUiField() throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"fieldName\"/></doc>", "binder.OwnerClass.Binder"); + + assertNotNull(fieldManager.lookup("fieldName")); + assertEquals("foo.Foo", fieldManager.lookup("fieldName").getAssignableType() + .getQualifiedSourceName()); + } + + public void testWithUiRendererTypedImport() throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"fieldName\" type=\"foo.Foo\"/></doc>", "renderer.OwnerClass.Renderer"); + + assertNotNull(fieldManager.lookup("fieldName")); + assertEquals("foo.Fooish", fieldManager.lookup("fieldName").getAssignableType() + .getQualifiedSourceName()); + } + + public void testWithUiRendererUntypedImport() + throws UnableToCompleteException, SAXParseException { + init("<doc><with field=\"fieldName\"/></doc>", "renderer.OwnerClass.Renderer"); + + assertNotNull(fieldManager.lookup("fieldName")); + assertEquals("foo.Fooish", fieldManager.lookup("fieldName").getAssignableType() + .getQualifiedSourceName()); + } + + private void init(String domString, String baseClass) throws SAXParseException, + UnableToCompleteException { + DesignTimeUtils designTime = DesignTimeUtilsStub.EMPTY; + elemProvider = + new XMLElementProviderImpl(new AttributeParsers(types, null, logger), attributeParsers, + types, logger, designTime); + doc = docHelper.documentFor(domString, null); + item = (Element) doc.getDocumentElement().getElementsByTagName("with").item(0); + elm = elemProvider.get(item); + JClassType aClass = types.findType(baseClass); + writer = + new UiBinderWriter(aClass, "foo", "", types, logger, fieldManager, null, + DesignTimeUtilsStub.EMPTY, new UiBinderContext(), true, true, ""); + parser = new UiBinderParser(writer, null, fieldManager, types, null, ""); + designTime.rememberPathForElements(doc); + UiBinderParser.Resource.WITH.create(parser, elm); + } +}
diff --git a/user/test/com/google/gwt/uibinder/rebind/UiRendererValidationTest.java b/user/test/com/google/gwt/uibinder/rebind/UiRendererValidationTest.java new file mode 100644 index 0000000..22da576 --- /dev/null +++ b/user/test/com/google/gwt/uibinder/rebind/UiRendererValidationTest.java
@@ -0,0 +1,187 @@ +/* + * Copyright 2011 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.dev.javac.testing.impl.MockJavaResource; + + +import org.xml.sax.SAXParseException; + + +/** + * Tests UiBinderWriter validation of UiRender getters and render method. + */ +public class UiRendererValidationTest extends AbstractUiBinderWriterTest { + + private static String UI_XML = "<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'>" + + "<ui:with field='withField' />" + + " <div ui:field='root'>" + + " <span ui:field='someField'><ui:text from='{withField.toString}'/></span>" + + " </div>" + + "</ui:UiBinder>"; + + private static String UI_XML_TYPED_WITH = "<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'>" + + "<ui:with field='withField' type='foo.Foo' />" + + " <div ui:field='root'>" + + " <span ui:field='someField'><ui:text from='{withField.toString}'/></span>" + + " </div>" + + "</ui:UiBinder>"; + + private StringBuffer declaredMethods = new StringBuffer(); + + public void testAGoodUiRendererInterface() throws UnableToCompleteException, SAXParseException { + declaredMethods.append(" public void render(SafeHtmlBuilder sb, foo.Foo withField);"); + declaredMethods.append(" public DivElement getRoot(Element foo);"); + declaredMethods.append(" public SpanElement getSomeField(Element bar);"); + init(UI_XML, generateRendererResource(declaredMethods)); + writer.parseDocument(doc, printWriter); + } + + public void testGetterBadParameterType() throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public DivElement getRoot(String bar);"); + init(UI_XML, generateRendererResource(declaredMethods)); + + assertParseFailure("Expected failure due to getter parameter not of type Element.", + "Getter getRoot must have exactly one parameter of type assignable to " + + "com.google.gwt.dom.client.Element"); + } + + public void testGetterParameterAssignableToElement() + throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public DivElement getRoot(DivElement bar);"); + init(UI_XML, generateRendererResource(declaredMethods)); + + writer.parseDocument(doc, printWriter); + } + + public void testGetterNoParameters() throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public DivElement getRoot();"); + init(UI_XML, generateRendererResource(declaredMethods)); + + assertParseFailure("Expected failure due to getter with no parameters.", + "Getter getRoot must have exactly one parameter"); + } + + public void testGetterTooManyParameters() throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public DivElement getRoot(Element parent, Integer i);"); + init(UI_XML, generateRendererResource(declaredMethods)); + + assertParseFailure("Expected failure due to bad getter signature.", + "Getter getRoot must have exactly one parameter"); + } + + public void testGetterUnknownField() throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public DivElement getQuux(Element parent);"); + init(UI_XML, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to getter for an unexpected field name.", + "getQuux does not match a \"ui:field='quux'\" declaration"); + } + + public void testRenderBadReturnType() throws SAXParseException, UnableToCompleteException { + declaredMethods.append(" public String render(SafeHtmlBuilder sb, foo.Foo withField);"); + init(UI_XML, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to bad render() return type.", + "renderer.OwnerClass.Renderer#render(SafeHtmlBuilder ...) does not return void"); + } + + public void testRenderExtraParametersOk() throws SAXParseException, UnableToCompleteException { + declaredMethods.append(" public void render(" + + "SafeHtmlBuilder sb, foo.Foo withField, foo.Foo withUnknownField);"); + init(UI_XML, generateRendererResource(declaredMethods)); + writer.parseDocument(doc, printWriter); + } + + public void testRenderMethodRepeated() throws SAXParseException, UnableToCompleteException { + declaredMethods.append(" public void render(SafeHtmlBuilder sb, foo.Foo withField);"); + declaredMethods.append(" public void render(SafeHtmlBuilder sb);"); + init(UI_XML, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to more than one render method.", + "renderer.OwnerClass.Renderer declares more than one method named render"); + } + + public void testRenderMissing() throws SAXParseException, UnableToCompleteException { + // No render method here + init(UI_XML_TYPED_WITH, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to missing render method.", + "renderer.OwnerClass.Renderer does not declare a render(SafeHtmlBuilder ...) method"); + } + + public void testRenderNoParameters() throws SAXParseException, UnableToCompleteException { + declaredMethods.append(" public void render();"); + init(UI_XML_TYPED_WITH, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to render with no parameters.", + "renderer.OwnerClass.Renderer does not declare a render(SafeHtmlBuilder ...) method"); + } + + public void testRenderNoParametersRepeated() throws SAXParseException, UnableToCompleteException { + declaredMethods.append(" public void render(SafeHtmlBuilder sb, foo.Foo withField);"); + declaredMethods.append(" public void render();"); + init(UI_XML_TYPED_WITH, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to render method repeated.", + "renderer.OwnerClass.Renderer declares more than one method named render"); + } + + public void testUnknownMethodDeclaration() throws SAXParseException, UnableToCompleteException { + initGetterTest(); + declaredMethods.append(" public void quux();"); + init(UI_XML_TYPED_WITH, generateRendererResource(declaredMethods)); + assertParseFailure("Expected failure due to unexpected method.", + "Unexpected method \"quux\" found"); + } + + private void assertParseFailure(String message, String expectedMessage) { + try { + writer.parseDocument(doc, printWriter); + fail(message); + } catch (UnableToCompleteException e) { + if (expectedMessage != null) { + assertEquals(expectedMessage, logger.died); + } + } + } + + private MockJavaResource generateRendererResource(final StringBuffer declarations) { + return new MockJavaResource("renderer.OwnerClass") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package renderer;\n"); + code.append("import com.google.gwt.safehtml.shared.SafeHtmlBuilder;\n"); + code.append("import com.google.gwt.uibinder.client.UiRenderer;\n"); + code.append("import com.google.gwt.dom.client.DivElement;\n"); + code.append("import com.google.gwt.dom.client.Element;\n"); + code.append("import com.google.gwt.dom.client.SpanElement;\n"); + code.append("import foo.Foo;\n"); + code.append("public class OwnerClass {"); + code.append(" public interface Renderer"); + code.append(" extends UiRenderer<OwnerClass> {"); + code.append(declarations); + code.append(" }"); + code.append("}"); + return code; + } + }; + } + + private void initGetterTest() { + // Required, not part of test + declaredMethods.append(" public void render(SafeHtmlBuilder sb, foo.Foo withField);"); + } +}
diff --git a/user/test/com/google/gwt/uibinder/test/UiJavaResources.java b/user/test/com/google/gwt/uibinder/test/UiJavaResources.java index 65ac2c8..e975dff 100644 --- a/user/test/com/google/gwt/uibinder/test/UiJavaResources.java +++ b/user/test/com/google/gwt/uibinder/test/UiJavaResources.java
@@ -492,6 +492,17 @@ return code; } }; + public static final MockJavaResource SAFEHTML_BUILDER = new MockJavaResource( + "com.google.gwt.safehtml.shared.SafeHtmlBuilder") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.safehtml.shared;\n"); + code.append("public class SafeHtmlBuilder {"); + code.append("}"); + return code; + } + }; public static final MockJavaResource SPLIT_LAYOUT_PANEL = new MockJavaResource( "com.google.gwt.user.client.ui.SplitLayoutPanel") { @Override @@ -640,6 +651,26 @@ return code; } }; + public static final MockJavaResource UI_FIELD = new MockJavaResource( + "com.google.gwt.uibinder.client.UiField") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.uibinder.client;"); + code.append("import java.lang.annotation.Documented;"); + code.append("import java.lang.annotation.ElementType;"); + code.append("import java.lang.annotation.Retention;"); + code.append("import java.lang.annotation.RetentionPolicy;"); + code.append("import java.lang.annotation.Target;"); + code.append("@Documented"); + code.append("@Retention(RetentionPolicy.RUNTIME)"); + code.append("@Target(ElementType.FIELD)"); + code.append("public @interface UiField {"); + code.append(" boolean provided() default false;"); + code.append("}"); + return code; + } + }; public static final MockJavaResource UI_OBJECT = new MockJavaResource( "com.google.gwt.user.client.ui.UIObject") { @Override @@ -651,6 +682,17 @@ return code; } }; + public static final MockJavaResource UI_RENDERER = new MockJavaResource( + "com.google.gwt.uibinder.client.UiRenderer") { + @Override + public CharSequence getContent() { + StringBuffer code = new StringBuffer(); + code.append("package com.google.gwt.uibinder.client;\n"); + code.append("public interface UiRenderer<T> {"); + code.append("}"); + return code; + } + }; public static final MockJavaResource VALUE_LABEL = new MockJavaResource( "com.google.gwt.user.client.ui.ValueLabel") { @Override @@ -720,6 +762,7 @@ rtn.add(NUMBER_FORMAT); rtn.add(RENDERER); rtn.add(SAFEHTML); + rtn.add(SAFEHTML_BUILDER); rtn.add(SPLIT_LAYOUT_PANEL); rtn.add(STACK_LAYOUT_PANEL); rtn.add(STACK_PANEL); @@ -734,6 +777,8 @@ rtn.add(UI_OBJECT); rtn.add(UI_BINDER); rtn.add(UI_FACTORY); + rtn.add(UI_FIELD); + rtn.add(UI_RENDERER); rtn.add(VALUE_LABEL); rtn.add(WIDGET); return rtn;
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java b/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java index 15eabe0..8e4e6b0 100644 --- a/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java +++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererTest.java
@@ -15,16 +15,25 @@ */ package com.google.gwt.uibinder.test.client; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.LabelElement; import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.SpanElement; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.uibinder.test.client.UiRendererUi.HtmlRenderer; /** * Functional test of UiBinder. */ public class UiRendererTest extends GWTTestCase { + + private static final String RENDERED_VALUE = "bar"; + + private DivElement docDiv; + private SafeHtml renderedHtml; + private HtmlRenderer renderer; private UiRendererUi safeHtmlUi; @Override @@ -37,15 +46,172 @@ super.gwtSetUp(); UiRendererTestApp app = UiRendererTestApp.getInstance(); safeHtmlUi = app.getSafeHtmlUi(); + renderedHtml = safeHtmlUi.render(RENDERED_VALUE); + renderer = safeHtmlUi.getRenderer(); + + docDiv = Document.get().createDivElement(); + docDiv.setInnerHTML(renderedHtml.asString()); + Document.get().getBody().appendChild(docDiv); + } + + public void testFieldGetters() { + assertTrue(renderer.isParentOrRenderer(docDiv)); + // Get root from parent + DivElement root = renderer.getRoot(docDiv); + assertTrue(renderer.isParentOrRenderer(root)); + assertNotNull(root); + + // For example, the rendered value should be inside + assertSpanContainsRenderedValue(root); + + // Get nameSpan + SpanElement nameSpan = renderer.getNameSpan(docDiv); + assertSpanContainsRenderedValueText(nameSpan.getFirstChild()); + + // Getters also work from the root element + DivElement root2 = renderer.getRoot(root); + assertTrue(renderer.isParentOrRenderer(root2)); + assertNotNull(root2); + assertSpanContainsRenderedValue(root2); + nameSpan = renderer.getNameSpan(root); + assertSpanContainsRenderedValueText(nameSpan.getFirstChild()); + } + + public void testFieldGettersDetachedRoot() { + // Detach root + DivElement root = renderer.getRoot(docDiv); + root.removeFromParent(); + + // Getting the root element is still fine + DivElement rootAgain = renderer.getRoot(root); + assertEquals(root, rootAgain); + + if (GWT.isProdMode()) { + // In prod mode we avoid checking whether the parent is attached + try { + renderer.getNameSpan(root); + fail("Expected a IllegalArgumentException because root is not attached to the DOM"); + } catch (IllegalArgumentException e) { + // Expected + } + } else { + // In dev Mode we explicitly check to see if parent is attached + try { + renderer.getNameSpan(root); + fail("Expected a RuntimeException because root is not attached to the DOM"); + } catch (RuntimeException e) { + // Expected + } + } + } + + public void testFieldGettersNoPreviouslyRenderedElement() { + assertFalse(renderer.isParentOrRenderer(null)); + + try { + renderer.getRoot(null); + fail("Expected NPE"); + } catch (NullPointerException e) { + // Expected + } + + try { + renderer.getNameSpan(null); + fail("Expected NPE"); + } catch (NullPointerException e) { + // Expected + } + + DivElement root = renderer.getRoot(docDiv); + SpanElement nameSpan = renderer.getNameSpan(docDiv); + + // remove nameSpan + nameSpan.removeFromParent(); + try { + renderer.getNameSpan(docDiv); + fail("Expected IllegalStateException because nameSpan was removed"); + } catch (IllegalStateException e) { + // In dev mode this is different from not being attached + assertFalse(GWT.isProdMode()); + } catch (IllegalArgumentException e) { + // Otherwise the same error as being not attached + assertTrue(GWT.isProdMode()); + } + + // Add a a sibling to the root element and remove the root from the parent altogether + SpanElement spanElement = Document.get().createSpanElement(); + docDiv.appendChild(spanElement); + root.removeFromParent(); + + assertFalse(renderer.isParentOrRenderer(docDiv)); + if (GWT.isProdMode()) { + // In prod mode no attempt is made to check whether root is still attached + assertTrue(renderer.isParentOrRenderer(root)); + } else { + assertFalse(renderer.isParentOrRenderer(root)); + } + try { + renderer.getRoot(docDiv); + fail("Expected an IllegalArgumentException to fail because parent does not contain the root"); + } catch (IllegalArgumentException e) { + // Expected + } + + try { + renderer.getNameSpan(docDiv); + fail("Expected an IllegalArgumentException to fail because parent does not contain the root"); + } catch (IllegalArgumentException e) { + // Expected + } + + // Finally remove the spanElement too + spanElement.removeFromParent(); + + assertFalse(renderer.isParentOrRenderer(docDiv)); + try { + renderer.getRoot(docDiv); + fail("Expected an IllegalArgumentException to fail because parent does not contain the root"); + } catch (IllegalArgumentException e) { + // Expected + } + + try { + renderer.getNameSpan(docDiv); + fail("Expected an IllegalArgumentException to fail because parent does not contain the root"); + } catch (IllegalArgumentException e) { + // Expected + } + } + + public void testFieldGettersNotOnlyChild() { + DivElement root = renderer.getRoot(docDiv); + + // Add a a sibling to the root element + docDiv.appendChild(Document.get().createSpanElement()); + + // Getting the root element is still fine + DivElement rootAgain = renderer.getRoot(docDiv); + assertEquals(root, rootAgain); + + if (GWT.isProdMode()) { + // In prod mode we avoid checking for being the only child + assertTrue(renderer.isParentOrRenderer(docDiv)); + SpanElement nameSpan = renderer.getNameSpan(docDiv); + assertSpanContainsRenderedValueText(nameSpan.getFirstChild()); + } else { + // in dev mode an explicit check is made + assertFalse(renderer.isParentOrRenderer(docDiv)); + try { + renderer.getNameSpan(docDiv); + fail("Expected an IllegalArgumentException to fail because root is not the only child"); + } catch (IllegalArgumentException e) { + // Expected + } + } } public void testSafeHtmlRendererText() { - SafeHtml render = safeHtmlUi.render(); - - LabelElement renderedHtml = Document.get().createLabelElement(); - renderedHtml.setInnerHTML(render.asString()); - - Node innerDiv = renderedHtml.getFirstChild(); + Node innerDiv = docDiv.getFirstChild(); // Was the first span rendered as a "HTML-safe" text string? Node spanWithConstantTextNode = innerDiv.getChild(0); @@ -58,15 +224,38 @@ assertEquals(Node.TEXT_NODE, firstRawTextNode.getNodeType()); assertEquals(" Hello, ", firstRawTextNode.getNodeValue()); - // Fields not present in owning class produce no content - Node firstFieldNode = innerDiv.getChild(2); - assertEquals(Node.ELEMENT_NODE, firstFieldNode.getNodeType()); - assertEquals("span", firstFieldNode.getNodeName().toLowerCase()); - assertFalse(firstFieldNode.hasChildNodes()); + // The value passed to render() was rendered correctly + assertSpanContainsRenderedValue(innerDiv); // ui:msg tags get rendered but the "<ui:msg>" tag is not Node secondRawTextNode = innerDiv.getChild(3); assertEquals(Node.TEXT_NODE, secondRawTextNode.getNodeType()); assertEquals(". How goes it? ", secondRawTextNode.getNodeValue()); + + // Fields not present in owning class produce no content + Node spanNode = innerDiv.getChild(4); + assertEquals(Node.ELEMENT_NODE, spanNode.getNodeType()); + assertEquals("span", spanNode.getNodeName().toLowerCase()); + assertFalse(spanNode.hasChildNodes()); + } + + @Override + protected void gwtTearDown() throws Exception { + docDiv.removeFromParent(); + docDiv = null; + } + + private void assertSpanContainsRenderedValue(Node root) { + Node firstFieldNode = root.getChild(2); + assertEquals(Node.ELEMENT_NODE, firstFieldNode.getNodeType()); + assertEquals("span", firstFieldNode.getNodeName().toLowerCase()); + assertTrue(firstFieldNode.hasChildNodes()); + Node renderedValue = firstFieldNode.getFirstChild(); + assertSpanContainsRenderedValueText(renderedValue); + } + + private void assertSpanContainsRenderedValueText(Node renderedValue) { + assertEquals(Node.TEXT_NODE, renderedValue.getNodeType()); + assertEquals(RENDERED_VALUE, renderedValue.getNodeValue()); } }
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java index 0b26d06..5127079 100644 --- a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java +++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.java
@@ -1,31 +1,37 @@ /* * Copyright 2011 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 - * + * + * 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 + * + * 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.test.client; import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableColElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.text.shared.UiRenderer; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.uibinder.client.UiRenderer; /** - * Sample use of a {@code SafeHtmlRenderer} with no dependency on - * com.google.gwt.user. + * Sample use of a {@code UiRenderer} with no dependency on com.google.gwt.user. */ public class UiRendererUi { + /** * Resources for this template. */ @@ -39,16 +45,48 @@ */ public interface Style extends CssResource { String bodyColor(); + String bodyFont(); } - interface HtmlRenderer extends UiRenderer<String> { } + static class Foo { + String bar; + + public Foo(String bar) { + this.bar = bar; + } + + String getBar() { + return bar; + } + } + + interface HtmlRenderer extends UiRenderer<UiRendererUi> { + SpanElement getNameSpan(Element owner); + TableColElement getNarrowColumn(Element owner); + DivElement getRoot(Element owner); + TableSectionElement getTbody(Element owner); + TableCellElement getTh1(Element owner); + TableCellElement getTh2(Element owner); + TableCellElement getTh4(Element owner); + Element getTmElement(Element owner); + TableRowElement getTr(Element owner); + + void render(SafeHtmlBuilder sb, Foo aValue); + } + private static final HtmlRenderer renderer = GWT.create(HtmlRenderer.class); + public static HtmlRenderer getRenderer() { + return renderer; + } + public UiRendererUi() { } - public SafeHtml render() { - return renderer.render(null); + public SafeHtml render(String value) { + SafeHtmlBuilder sb = new SafeHtmlBuilder(); + getRenderer().render(sb, new Foo(value)); + return sb.toSafeHtml(); } }
diff --git a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml index 2dea655..e5e228c 100644 --- a/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml +++ b/user/test/com/google/gwt/uibinder/test/client/UiRendererUi.ui.xml
@@ -16,12 +16,14 @@ xmlns:res='urn:with:com.google.gwt.uibinder.test.client.DomBasedUi.Resources' > <ui:with field="constants" type="com.google.gwt.uibinder.test.client.Constants"/> + <ui:with field="aValue"/> <div ui:field='root' res:class="style.bodyColor style.bodyFont" title="The title of this div is localizable"> <ui:attribute name='title'/> <span><ui:text from="{constants.getText}" /></span> - Hello, <span ui:field="nameSpan" />. + Hello, <span ui:field="nameSpan"><ui:text from="{aValue.getBar}"/></span>. <ui:msg>How goes it?</ui:msg> + <span/> <h2 res:class="style.bodyColor style.bodyFont">Placeholders in localizables</h2> <p><ui:msg>When you mark up your text for translation, there will be bits that you don't want the translators to mess with. You can protect