| /* | |
| * 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.JMethod; | |
| import com.google.gwt.core.ext.typeinfo.JParameter; | |
| import com.google.gwt.core.ext.typeinfo.JParameterizedType; | |
| import com.google.gwt.core.ext.typeinfo.JType; | |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; | |
| import com.google.gwt.event.shared.EventHandler; | |
| import com.google.gwt.event.shared.HandlerRegistration; | |
| import com.google.gwt.uibinder.client.UiHandler; | |
| import com.google.gwt.uibinder.rebind.model.OwnerClass; | |
| /** | |
| * This class implements an easy way to bind widget event handlers to methods | |
| * annotated with {@link com.google.gwt.uibinder.client.UiHandler} so that the | |
| * user doesn't need to worry about writing code to implement these bindings. | |
| * | |
| * <p> | |
| * For instance, the class defined below: | |
| * | |
| * <pre> | |
| * public class MyClass { | |
| * @UiField Label label; | |
| * | |
| * @UiBinder({"label", "link"}) | |
| * public void doClick(ClickEvent e) { | |
| * // do something | |
| * } | |
| * } | |
| * </pre> | |
| * | |
| * will generate a piece of code like: | |
| * | |
| * <pre> | |
| * ClickHandler handler0 = new ClickHandler() { | |
| * @Override | |
| * public void onClick(ClickEvent event) { | |
| * owner.doClick(event); | |
| * } | |
| * }); | |
| * label.addClickHandler(handler0); | |
| * link.addClickHandler(handler0); | |
| * </pre> | |
| * | |
| * Notice that the <b>link</b> object doesn't need to be annotated with | |
| * {@link com.google.gwt.uibinder.client.UiField} as long as it exists | |
| * (annotated with ui:field) in the template. | |
| */ | |
| class HandlerEvaluator { | |
| private static final String HANDLER_BASE_NAME = "handlerMethodWithNameVeryUnlikelyToCollideWithUserFieldNames"; | |
| /* | |
| * TODO(rjrjr) The correct fix is to put the handlers in a locally defined | |
| * class, making the generated code look like this | |
| * | |
| * http://docs.google.com/Doc?docid=0AQfnKgX9tAdgZGZ2cTM5YjdfMmQ4OTk0eGhz&hl=en | |
| * | |
| * But that needs to wait for a refactor to get most of this stuff out of here | |
| * and into com.google.gwt.uibinder.rebind.model | |
| */ | |
| private int varCounter = 0; | |
| private final MortalLogger logger; | |
| private final JClassType handlerRegistrationJClass; | |
| private final JClassType eventHandlerJClass; | |
| private final OwnerClass ownerClass; | |
| /** | |
| * The verbose testable constructor. | |
| * | |
| * @param ownerClass a descriptor of the UI owner class | |
| * @param logger the logger for warnings and errors | |
| * @param oracle the type oracle | |
| */ | |
| HandlerEvaluator(OwnerClass ownerClass, MortalLogger logger, TypeOracle oracle) { | |
| this.ownerClass = ownerClass; | |
| this.logger = logger; | |
| handlerRegistrationJClass = oracle.findType(HandlerRegistration.class.getName()); | |
| eventHandlerJClass = oracle.findType(EventHandler.class.getName()); | |
| } | |
| /** | |
| * Runs the evaluator in the given class according to the valid fields | |
| * extracted from the template (via attribute ui:field). | |
| * | |
| * @param writer the writer used to output the results | |
| * @param fieldManager the field manager instance | |
| * @param uiOwner the name of the class evaluated here that owns the template | |
| */ | |
| public void run(IndentedWriter writer, FieldManager fieldManager, | |
| String uiOwner) throws UnableToCompleteException { | |
| // Iterate through all methods defined in the class. | |
| for (JMethod method : ownerClass.getUiHandlers()) { | |
| // Evaluate the method. | |
| String boundMethod = method.getName(); | |
| if (method.isPrivate()) { | |
| logger.die("Method '%s' cannot be private.", boundMethod); | |
| } | |
| // Retrieves both event and handler types. | |
| JParameter[] parameters = method.getParameters(); | |
| if (parameters.length != 1) { | |
| logger.die("Method '%s' must have a single event parameter defined.", | |
| boundMethod); | |
| } | |
| JClassType eventType = parameters[0].getType().isClass(); | |
| JClassType handlerType = getHandlerForEvent(eventType); | |
| if (handlerType == null) { | |
| logger.die("Parameter '%s' is not an event (subclass of GwtEvent).", | |
| eventType.getName()); | |
| } | |
| // Cool to add the handler in the output. | |
| String handlerVarName = HANDLER_BASE_NAME + (++varCounter); | |
| writeHandler(writer, uiOwner, handlerVarName, handlerType, eventType, | |
| boundMethod); | |
| // Adds the handler created above. | |
| UiHandler annotation = method.getAnnotation(UiHandler.class); | |
| for (String objectName : annotation.value()) { | |
| // Is the field object valid? | |
| FieldWriter fieldWriter = fieldManager.lookup(objectName); | |
| if (fieldWriter == null) { | |
| logger.die( | |
| ("Method '%s' can not be bound. You probably missed ui:field='%s' " | |
| + "in the template."), boundMethod, objectName); | |
| } | |
| // Retrieves the "add handler" method in the object. | |
| JMethod addHandlerMethodType = getAddHandlerMethodForObject( | |
| fieldWriter.getInstantiableType(), handlerType); | |
| if (addHandlerMethodType == null) { | |
| logger.die("Field '%s' does not have an 'add%s' method associated.", | |
| objectName, handlerType.getName()); | |
| } | |
| // Cool to tie the handler into the object. | |
| writeAddHandler(writer, handlerVarName, addHandlerMethodType.getName(), | |
| objectName); | |
| } | |
| } | |
| } | |
| /** | |
| * Writes a handler entry using the given writer. | |
| * | |
| * @param writer the writer used to output the results | |
| * @param uiOwner the name of the class evaluated here that owns the template | |
| * @param handlerVarName the name of the handler variable | |
| * @param handlerType the handler we want to create | |
| * @param eventType the event associated with the handler | |
| * @param boundMethod the method bound in the handler | |
| */ | |
| protected void writeHandler(IndentedWriter writer, String uiOwner, | |
| String handlerVarName, JClassType handlerType, JClassType eventType, | |
| String boundMethod) throws UnableToCompleteException { | |
| // Retrieves the single method (usually 'onSomething') related to all | |
| // handlers. Ex: onClick in ClickHandler, onBlur in BlurHandler ... | |
| JMethod[] methods = handlerType.getMethods(); | |
| if (methods.length != 1) { | |
| logger.die("'%s' has more than one method defined.", | |
| handlerType.getName()); | |
| } | |
| // Checks if the method has an Event as parameter. Ex: ClickEvent in | |
| // onClick, BlurEvent in onBlur ... | |
| JParameter[] parameters = methods[0].getParameters(); | |
| if (parameters.length != 1 || parameters[0].getType() != eventType) { | |
| logger.die("Method '%s' needs '%s' as parameter", methods[0].getName(), | |
| eventType.getName()); | |
| } | |
| writer.newline(); | |
| writer.write("final %1$s %2$s = new %1$s() {", | |
| handlerType.getParameterizedQualifiedSourceName(), handlerVarName); | |
| writer.indent(); | |
| writer.write("public void %1$s(%2$s event) {", methods[0].getName(), | |
| eventType.getParameterizedQualifiedSourceName()); | |
| writer.indent(); | |
| writer.write("%1$s.%2$s(event);", uiOwner, boundMethod); | |
| writer.outdent(); | |
| writer.write("}"); | |
| writer.outdent(); | |
| writer.write("};"); | |
| } | |
| /** | |
| * Adds the created handler to the given object (field). | |
| * | |
| * @param writer the writer used to output the results | |
| * @param handlerVarName the name of the handler variable | |
| * @param addHandlerMethodName the "add handler" method name associated with | |
| * the object | |
| * @param objectName the name of the object we want to tie the handler | |
| */ | |
| void writeAddHandler(IndentedWriter writer, String handlerVarName, | |
| String addHandlerMethodName, String objectName) { | |
| writer.write("%1$s.%2$s(%3$s);", objectName, addHandlerMethodName, | |
| handlerVarName); | |
| } | |
| /** | |
| * Checks if a specific handler is valid for a given object and return the | |
| * method that ties them. The object must override a method that returns | |
| * {@link com.google.gwt.event.shared.HandlerRegistration} and receives a | |
| * single input parameter of the same type of handlerType. | |
| * | |
| * <p> | |
| * Output an error in case more than one method match the conditions described | |
| * above. | |
| * </p> | |
| * | |
| * <pre> | |
| * <b>Examples:</b> | |
| * - HandlerRegistration addClickHandler(ClickHandler handler) | |
| * - HandlerRegistration addMouseOverHandler(MouseOverHandler handler) | |
| * - HandlerRegistration addSubmitCompleteHandler( | |
| * FormPanel.SubmitCompleteHandler handler) | |
| * </pre> | |
| * | |
| * @param objectType the object type we want to check | |
| * @param handlerType the handler type we want to check in the object | |
| * | |
| * @return the method that adds handlerType into objectType, or <b>null</b> if | |
| * no method was found | |
| */ | |
| private JMethod getAddHandlerMethodForObject(JClassType objectType, | |
| JClassType handlerType) throws UnableToCompleteException { | |
| JMethod handlerMethod = null; | |
| JMethod alternativeHandlerMethod = null; | |
| for (JMethod method : objectType.getInheritableMethods()) { | |
| // Condition 1: returns HandlerRegistration? | |
| if (method.getReturnType() == handlerRegistrationJClass) { | |
| // Condition 2: single parameter of the same type of handlerType? | |
| JParameter[] parameters = method.getParameters(); | |
| if (parameters.length != 1) { | |
| continue; | |
| } | |
| JType subjectHandler = parameters[0].getType(); | |
| if (handlerType.equals(subjectHandler)) { | |
| // Condition 3: does more than one method match the condition? | |
| if (handlerMethod != null) { | |
| logger.die( | |
| ("This handler cannot be generated. Methods '%s' and '%s' are " | |
| + "ambiguous. Which one to pick?"), method, handlerMethod); | |
| } | |
| handlerMethod = method; | |
| } | |
| /** | |
| * Normalize the parameter and check for an alternative handler method. | |
| * Might be the case where the given objectType is generic. In this | |
| * situation we need to normalize the method parameter to test for | |
| * equality. For instance: | |
| * | |
| * handlerType => TableHandler<String> | |
| * subjectHandler => TableHandler | |
| * | |
| * This is done as an alternative handler method to preserve the | |
| * original logic. | |
| */ | |
| JParameterizedType ptype = handlerType.isParameterized(); | |
| if (ptype != null) { | |
| if (subjectHandler.equals(ptype.getRawType())) { | |
| alternativeHandlerMethod = method; | |
| } | |
| } | |
| } | |
| } | |
| return (handlerMethod != null) ? handlerMethod : alternativeHandlerMethod; | |
| } | |
| /** | |
| * Retrieves the handler associated with the event. | |
| * | |
| * @param eventType the given event | |
| * @return the associated handler, <code>null</code> if not found | |
| */ | |
| private JClassType getHandlerForEvent(JClassType eventType) { | |
| // All handlers event must have an overrided method getAssociatedType(). | |
| // We take advantage of this information to get the associated handler. | |
| // Ex: | |
| // com.google.gwt.event.dom.client.ClickEvent | |
| // ---> com.google.gwt.event.dom.client.ClickHandler | |
| // | |
| // com.google.gwt.event.dom.client.BlurEvent | |
| // ---> com.google.gwt.event.dom.client.BlurHandler | |
| if (eventType == null) { | |
| return null; | |
| } | |
| JMethod method = eventType.findMethod("getAssociatedType", new JType[0]); | |
| if (method == null) { | |
| logger.warn( | |
| "Method 'getAssociatedType()' could not be found in the event '%s'.", | |
| eventType.getName()); | |
| return null; | |
| } | |
| JType returnType = method.getReturnType(); | |
| if (returnType == null) { | |
| logger.warn( | |
| "The method 'getAssociatedType()' in the event '%s' returns void.", | |
| eventType.getName()); | |
| return null; | |
| } | |
| JParameterizedType isParameterized = returnType.isParameterized(); | |
| if (isParameterized == null) { | |
| logger.warn( | |
| "The method 'getAssociatedType()' in '%s' does not return Type<? extends EventHandler>.", | |
| eventType.getName()); | |
| return null; | |
| } | |
| JClassType[] argTypes = isParameterized.getTypeArgs(); | |
| if ((argTypes.length != 1) | |
| && !argTypes[0].isAssignableTo(eventHandlerJClass)) { | |
| logger.warn( | |
| "The method 'getAssociatedType()' in '%s' does not return Type<? extends EventHandler>.", | |
| eventType.getName()); | |
| return null; | |
| } | |
| return argTypes[0]; | |
| } | |
| } |