| /* |
| * 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.uibinder.client.UiHandler; |
| import com.google.gwt.uibinder.rebind.model.OwnerClass; |
| import com.google.gwt.uibinder.rebind.model.OwnerField; |
| import com.google.web.bindery.event.shared.HandlerRegistration; |
| |
| /** |
| * 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; |
| private final boolean useLazyWidgetBuilders; |
| |
| /** |
| * 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, boolean useLazyWidgetBuilders) { |
| this.ownerClass = ownerClass; |
| this.logger = logger; |
| this.useLazyWidgetBuilders = useLazyWidgetBuilders; |
| |
| 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(); |
| if (eventType == null) { |
| logger.die("Parameter type is not a class."); |
| } |
| |
| 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); |
| } |
| JClassType objectType = fieldWriter.getInstantiableType(); |
| if (objectType.isGenericType() != null) { |
| objectType = tryEnhancingTypeInfo(objectName, objectType); |
| } |
| |
| // Retrieves the "add handler" method in the object. |
| JMethod addHandlerMethodType = getAddHandlerMethodForObject(objectType, 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, fieldManager, handlerVarName, |
| addHandlerMethodType.getName(), objectName); |
| } |
| } |
| } |
| |
| private JClassType tryEnhancingTypeInfo(String objectName, JClassType objectType) { |
| OwnerField uiField = ownerClass.getUiField(objectName); |
| if (uiField != null) { |
| JParameterizedType pType = uiField.getRawType().isParameterized(); |
| if (pType != null) { |
| // Even field is parameterized, it might be a super class. In that case, if we use the field |
| // type then we might miss some add handlers methods from the objectType itself; something |
| // we don't want to happen! |
| if (pType.getBaseType().equals(objectType)) { |
| // Now we proved type from UiField is more specific, let's use that one |
| return pType; |
| } |
| } |
| } |
| return objectType; |
| } |
| |
| /** |
| * 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(); |
| // Create the anonymous class extending the raw type to avoid errors under the new JDT |
| // if the type has a wildcard. |
| writer.write("final %1$s %2$s = new %1$s() {", |
| handlerType.getQualifiedSourceName(), handlerVarName); |
| writer.indent(); |
| writer.write("public void %1$s(%2$s event) {", methods[0].getName(), |
| // Use the event raw type to match the signature as we are using implementing the raw type |
| // interface. |
| eventType.getQualifiedSourceName()); |
| writer.indent(); |
| // Cast the event to the parameterized type to avoid warnings.. |
| writer.write("%1$s.%2$s((%3$s) event);", uiOwner, boundMethod, |
| eventType.getParameterizedQualifiedSourceName()); |
| 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, FieldManager fieldManager, |
| String handlerVarName, String addHandlerMethodName, String objectName) { |
| if (useLazyWidgetBuilders) { |
| fieldManager.require(objectName).addStatement("%1$s.%2$s(%3$s);", |
| objectName, addHandlerMethodName, handlerVarName); |
| } else { |
| 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; |
| JMethod alternativeHandlerMethod2 = null; |
| for (JMethod method : objectType.getInheritableMethods()) { |
| |
| // Condition 1: returns HandlerRegistration? |
| JClassType returnClassType = method.getReturnType().isClassOrInterface(); |
| if (returnClassType != null && handlerRegistrationJClass.isAssignableFrom(returnClassType)) { |
| |
| // Condition 2: single parameter of the same type of handlerType? |
| JParameter[] parameters = method.getParameters(); |
| if (parameters.length != 1) { |
| continue; |
| } |
| |
| JClassType methodParam = parameters[0].getType().isClassOrInterface(); |
| if (methodParam == null) { |
| continue; |
| } |
| |
| if (handlerType.equals(methodParam)) { |
| |
| // 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; |
| continue; |
| } |
| |
| /** |
| * 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 => Alt 1: TableHandler or Alt 2: TableHandler<T> |
| * |
| * This is done as an alternative handler method to preserve the |
| * original logic. |
| */ |
| if (handlerType.isAssignableFrom(methodParam)) { |
| // Alt 1: TableHandler<String> => TableHandler or TableHandler<?> => TableHandler<String> |
| alternativeHandlerMethod = method; |
| } else if (handlerType.isParameterized() != null && objectType.isGenericType() != null) { |
| // Alt 2: TableHandler<String> => TableHandler<T> |
| if (methodParam.getErasedType().equals(handlerType.isParameterized().getErasedType())) { |
| // Unfortunately this is overly lenient but it was always like this |
| alternativeHandlerMethod2 = method; |
| } |
| } |
| } |
| } |
| |
| return (handlerMethod != null) ? handlerMethod |
| : (alternativeHandlerMethod != null) ? alternativeHandlerMethod : alternativeHandlerMethod2; |
| } |
| |
| /** |
| * 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]; |
| } |
| } |