blob: ecce7e63d1ae74e1fef7af9b5967bd239d54bac7 [file] [log] [blame]
/*
* 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];
}
}