Java tutorial
/* * 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 fr.onevu.gwt.uibinder.client.impl; 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.dom.client.EventTarget; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.uibinder.client.UiRenderer; import java.util.HashMap; /** * Abstract implementation of a safe HTML binder to make implementation of * generated rendering simpler. */ public abstract class AbstractUiRenderer implements UiRenderer { /** * Helps handle method dispatch to classes that use UiRenderer. * * @param <T> * class that can receive events from a UiRenderer implementation */ protected abstract static class UiRendererDispatcher<T> implements HasHandlers { private T eventTarget; private int methodIndex; private Element root; /** * Maps strings describing event types and field names to methods contained * in type {@code T} (which are indexed by an integer). */ private HashMap<String, Integer> table; /** * Fire an event to the receiver. * * @param target * object that will handle the events * @param event * event to dispatch * @param parentOrRoot * root element of a previously rendered DOM structure (or its * parent) */ protected void fireEvent(T target, NativeEvent event, Element parentOrRoot) { if (target == null) { throw new NullPointerException("Null event handler received"); } if (event == null) { throw new NullPointerException("Null event object received"); } if (parentOrRoot == null) { throw new NullPointerException("Null parent received"); } if (!isParentOrRenderer(parentOrRoot, RENDERED_ATTRIBUTE)) { return; } eventTarget = target; root = findRootElementOrNull(parentOrRoot, RENDERED_ATTRIBUTE); methodIndex = computeDispatchEvent(table, root, event); DomEvent.fireNativeEvent(event, this); } /** * Object that will receive the event. */ protected T getEventTarget() { return eventTarget; } /** * Index of the method that will receive the event. */ protected int getMethodIndex() { return methodIndex; } /** * Root Element of a previously rendered DOM structure. */ protected Element getRoot() { return root; } /** * Initializes the dispatch table if necessary. */ protected void initDispatchTable(String[] keys, Integer[] values) { table = buildDispatchMap(keys, values); } } /** * Marker attribute for DOM structures previously generated by UiRenderer. */ public static final String RENDERED_ATTRIBUTE = "gwtuirendered"; /** * Field name used to identify the root element while dispatching events. */ public static final String ROOT_FAKE_NAME = "^"; public static final String UI_ID_SEPARATOR = ":"; private static final int NO_HANDLER_FOUND = -1; /** * 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 */ protected static String buildInnerId(String fieldName, String uiId) { return uiId + UI_ID_SEPARATOR + 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 */ protected 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 */ protected static Element findRootElement(Element parent, String attribute) { Element root = findRootElementOrNull(parent, attribute); if (root == null) { throw new IllegalArgumentException("Parent element does not contain a previously rendered element"); } return root; } /** * 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 */ protected 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); } /** * Converts an array of keys and values into a map. */ private static HashMap<String, Integer> buildDispatchMap(String[] keys, Integer[] values) { HashMap<String, Integer> result = new HashMap<String, Integer>(keys.length); for (int i = 0; i < keys.length; i++) { result.put(keys[i], values[i]); } return result; } /** * Obtains the index of the method that will receive an event. * * @param table * event types and field names indexed by the method that can handle * an event. * @param root * of a previously rendered DOM structure * @param event * event to handle * @return index of the method that will process the event or * NO_HANDLER_FOUND. */ private static int computeDispatchEvent(HashMap<String, Integer> table, Element root, NativeEvent event) { String uiId = root.getAttribute(RENDERED_ATTRIBUTE); EventTarget eventTarget = event.getEventTarget(); if (!Element.is(eventTarget)) { return NO_HANDLER_FOUND; } Element cursor = Element.as(eventTarget); while (cursor != null && cursor != root && cursor.getNodeType() != Element.DOCUMENT_NODE) { String fieldName = getFieldName(uiId, cursor); if (fieldName == null) { cursor = cursor.getParentElement(); continue; } String key = event.getType() + UI_ID_SEPARATOR + fieldName; if (table.containsKey(key)) { return table.get(key); } cursor = cursor.getParentElement(); } if (cursor == root) { String key = event.getType() + UI_ID_SEPARATOR + ROOT_FAKE_NAME; if (table.containsKey(key)) { return table.get(key); } } return NO_HANDLER_FOUND; } /** * 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 or * <code>null</code> if {@code parent} does not contain a previously * rendered element * * @throws NullPointerException * if {@code parent} == null */ private static Element findRootElementOrNull(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 { return null; } } /** * Obtains the field name of a previously rendered DOM Element. * * @param uiId * identifier of the fields contained in a previously rendered DOM * structure * @param element * which may correspond to {@code ui:field} * @return the field name or {@code null} if the {@code element} does not have * an id attribute as would be produced by * {@link #buildInnerId(String, String)}) with {@code fieldName} and * {@code uiId} */ private static String getFieldName(String uiId, Element element) { String id = element.getId(); if (id == null) { return null; } int split = id.indexOf(UI_ID_SEPARATOR); return split != -1 && uiId.length() == split && id.startsWith(uiId) ? id.substring(split + 1) : null; } /** * In DevMode, walks up the parents of the {@code rendered} element to * ascertain that it is attached to the document. Always returns * <code>true</code> in ProdMode. */ private 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); } /** * Implements {@link UiRenderer#isParentOrRenderer(Element)} . */ private static boolean isParentOrRenderer(Element parent, String attribute) { if (parent == null) { return false; } Element root = findRootElementOrNull(parent, attribute); return root != null && isAttachedToDom(root) && isRenderedElementSingleChild(root); } /** * Checks that the parent of {@code rendered} has a single child. */ private static boolean isRenderedElementSingleChild(Element rendered) { return GWT.isProdMode() || rendered.getParentElement().getChildCount() == 1; } /** * Holds the part of the id attribute common to all elements being rendered. */ protected String uiId; @Override public boolean isParentOrRenderer(Element parent) { return isParentOrRenderer(parent, RENDERED_ATTRIBUTE); } }