blob: c8a7052a81ec88f3c3c14a3295d79375926b78a6 [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.model;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
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.dev.util.Pair;
import com.google.gwt.uibinder.client.UiChild;
import com.google.gwt.uibinder.client.UiConstructor;
import com.google.gwt.uibinder.rebind.MortalLogger;
import com.google.gwt.uibinder.rebind.UiBinderContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Descriptor for a class which can be used as a @UiField. This is usually a
* widget, but can also be a resource (such as Messages or an ImageBundle). Also
* notice that the existence of an OwnerFieldClass doesn't mean the class is
* actually present as a field in the owner.
*/
public class OwnerFieldClass {
/**
* Gets or creates the descriptor for the given field class.
*
* @param forType the field type to get a descriptor for
* @param logger TODO
* @param context
* @return the descriptor
*/
public static OwnerFieldClass getFieldClass(JClassType forType,
MortalLogger logger, UiBinderContext context)
throws UnableToCompleteException {
OwnerFieldClass clazz = context.getOwnerFieldClass(forType);
if (clazz == null) {
clazz = new OwnerFieldClass(forType, logger);
context.putOwnerFieldClass(forType, clazz);
}
return clazz;
}
private Set<String> ambiguousSetters;
private final MortalLogger logger;
private final JClassType rawType;
private final Map<String, JMethod> setters = new HashMap<String, JMethod>();
/**
* Mapping from all of the @UiChild tags to their corresponding methods and
* limits on being called.
*/
private final Map<String, Pair<JMethod, Integer>> uiChildren = new HashMap<String, Pair<JMethod, Integer>>();
private JConstructor uiConstructor;
/**
* Default constructor. This is package-visible for testing only.
*
* @param forType the type of the field class
* @param logger
* @throws UnableToCompleteException if the class is not valid
*/
OwnerFieldClass(JClassType forType, MortalLogger logger)
throws UnableToCompleteException {
this.rawType = forType;
this.logger = logger;
findUiConstructor(forType);
findSetters(forType);
findUiChildren(forType);
}
/**
* Returns the field's raw type.
*/
public JClassType getRawType() {
return rawType;
}
/**
* Finds the setter method for a given property.
*
* @param propertyName the name of the property
* @return the setter method, or null if none exists
*/
public JMethod getSetter(String propertyName)
throws UnableToCompleteException {
// TODO(rjrjr) This fails for CheckBox#setValue(Boolean) because it
// also finds CheckBox#setValue(Boolean, Boolean). Must fix for 2.0,
// when CheckBox#setChecked will go away and CheckBox#setValue must be used
if (ambiguousSetters != null && ambiguousSetters.contains(propertyName)) {
logger.die("Ambiguous setter requested: " + rawType.getName() + "."
+ propertyName);
}
return setters.get(propertyName);
}
/**
* Returns a list of methods annotated with @UiChild.
*
* @return a list of all add child methods
*/
public Map<String, Pair<JMethod, Integer>> getUiChildMethods() {
return uiChildren;
}
/**
* Returns the constructor annotated with @UiConstructor, or null if none
* exists.
*/
public JConstructor getUiConstructor() {
return uiConstructor;
}
/**
* Given a collection of setters for the same property, picks which one to
* use. Not having a proper setter is not an error unless of course the user
* tries to use it.
*
* @param propertySetters the collection of setters
* @return the setter to use, or null if none is good enough
*/
private JMethod disambiguateSetters(Collection<JMethod> propertySetters) {
if (propertySetters.size() == 1) {
return propertySetters.iterator().next();
}
// Pick the string setter, if there's one
for (JMethod method : propertySetters) {
JParameter[] parameters = method.getParameters();
if (parameters.length == 1
&& parameters[0].getType().getQualifiedSourceName().equals(
"java.lang.String")) {
return method;
}
}
// Check if all setters aren't just the same one being overridden in parent
// classes.
JMethod firstMethod = null;
for (JMethod method : propertySetters) {
if (firstMethod == null) {
firstMethod = method;
continue;
}
// If the method is not the same as the first one, there's still an
// ambiguity. Being equal means having the same parameter types.
if (!sameParameterTypes(method, firstMethod)) {
return null;
}
}
return firstMethod;
}
/**
* Recursively finds all setters for the given class and its superclasses.
*
* @param fieldType the leaf type to look at
* @return a multimap of property name to the setter methods
*/
private Map<String, Collection<JMethod>> findAllSetters(JClassType fieldType) {
Map<String, Collection<JMethod>> allSetters;
// First, get all setters from the parent class, recursively.
JClassType superClass = fieldType.getSuperclass();
if (superClass != null) {
allSetters = findAllSetters(superClass);
} else {
// Stop recursion - deepest level creates return value
allSetters = new HashMap<String, Collection<JMethod>>();
}
JMethod[] methods = fieldType.getMethods();
for (JMethod method : methods) {
if (!isSetterMethod(method)) {
continue;
}
// Take out "set"
String propertyName = method.getName().substring(3);
// turn "PropertyName" into "propertyName"
propertyName = propertyName.substring(0, 1).toLowerCase()
+ propertyName.substring(1);
Collection<JMethod> propertyMethods = allSetters.get(propertyName);
if (propertyMethods == null) {
propertyMethods = new ArrayList<JMethod>();
allSetters.put(propertyName, propertyMethods);
}
propertyMethods.add(method);
}
return allSetters;
}
/**
* Finds all setters in the class, and puts them in the {@link #setters}
* field.
*
* @param fieldType the type of the field
*/
private void findSetters(JClassType fieldType) {
// Pass one - get all setter methods
Map<String, Collection<JMethod>> allSetters = findAllSetters(fieldType);
// Pass two - disambiguate
for (String propertyName : allSetters.keySet()) {
Collection<JMethod> propertySetters = allSetters.get(propertyName);
JMethod setter = disambiguateSetters(propertySetters);
// If no setter could be disambiguated for this property, add it to the
// set of ambiguous setters. This is later consulted if and only if the
// setter is used.
if (setter == null) {
if (ambiguousSetters == null) {
ambiguousSetters = new HashSet<String>();
}
ambiguousSetters.add(propertyName);
}
setters.put(propertyName, setter);
}
}
/**
* Scans the class to find all methods annotated with @UiChild.
*
* @param ownerType the type of the owner class
* @throws UnableToCompleteException
*/
private void findUiChildren(JClassType ownerType)
throws UnableToCompleteException {
JMethod[] methods = ownerType.getMethods();
while (ownerType != null) {
for (JMethod method : methods) {
UiChild annotation = method.getAnnotation(UiChild.class);
if (annotation != null) {
String tag = annotation.tagname();
int limit = annotation.limit();
if (tag.equals("")) {
String name = method.getName();
if (name.startsWith("add")) {
tag = name.substring(3).toLowerCase();
} else {
logger.die(method.getName()
+ " must either specify a UiChild tagname or begin "
+ "with \"add\".");
}
}
uiChildren.put(tag, Pair.create(method, limit));
}
}
ownerType = ownerType.getSuperclass();
}
}
/**
* Finds the constructor annotated with @UiConcontructor if there is one, and
* puts it in the {@link #uiConstructor} field.
*
* @param fieldType the type of the field
*/
private void findUiConstructor(JClassType fieldType)
throws UnableToCompleteException {
for (JConstructor ctor : fieldType.getConstructors()) {
if (ctor.getAnnotation(UiConstructor.class) != null) {
if (uiConstructor != null) {
logger.die(fieldType.getName()
+ " has more than one constructor annotated with @UiConstructor");
}
uiConstructor = ctor;
}
}
}
/**
* Checks whether the given method qualifies as a setter. This looks at the
* method qualifiers, name and return type, but not at the parameter types.
*
* @param method the method to look at
* @return whether it's a setter
*/
private boolean isSetterMethod(JMethod method) {
// All setter methods should be public void setSomething(...)
return method.isPublic() && !method.isStatic()
&& method.getName().startsWith("set") && method.getName().length() > 3
&& method.getReturnType() == JPrimitiveType.VOID;
}
/**
* Checks whether two methods have the same parameter types.
*
* @param m1 the first method to compare
* @param m2 the second method to compare
* @return whether the methods have the same parameter types
*/
private boolean sameParameterTypes(JMethod m1, JMethod m2) {
JParameter[] p1 = m1.getParameters();
JParameter[] p2 = m2.getParameters();
if (p1.length != p2.length) {
return false;
}
for (int i = 0; i < p1.length; i++) {
JType type1 = p1[i].getType();
JType type2 = p2[i].getType();
if (!type1.equals(type2)) {
return false;
}
}
// No types were different
return true;
}
}