/*
* 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.core.shared.impl.StringCase;
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.beans.Introspector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 {
private static final int DEFAULT_COST = 4;
private static final Map<String, Integer> TYPE_RANK;
static {
HashMap<String, Integer> tmpTypeRank = new HashMap<String, Integer>();
tmpTypeRank.put("java.lang.String", 1);
tmpTypeRank.put("boolean", 2);
tmpTypeRank.put("byte", 2);
tmpTypeRank.put("char", 2);
tmpTypeRank.put("double", 2);
tmpTypeRank.put("float", 2);
tmpTypeRank.put("int", 2);
tmpTypeRank.put("long", 2);
tmpTypeRank.put("short", 2);
tmpTypeRank.put("java.lang.Boolean", 3);
tmpTypeRank.put("java.lang.Byte", 3);
tmpTypeRank.put("java.lang.Character", 3);
tmpTypeRank.put("java.lang.Double", 3);
tmpTypeRank.put("java.lang.Float", 3);
tmpTypeRank.put("java.lang.Integer", 3);
tmpTypeRank.put("java.lang.Long", 3);
tmpTypeRank.put("java.lang.Short", 3);
TYPE_RANK = Collections.unmodifiableMap(tmpTypeRank);
}
/**
* 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 {
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;
}
/**
* Adds a setter for a given property to the given map of setters.
*
* @param allSetters the map of setters (keyed by property name)
* @param propertyName the property name to use
* @param method the setter to use
*/
private void addSetter(Map<String, Collection<JMethod>> allSetters,
String propertyName, JMethod method) {
Collection<JMethod> propertyMethods = allSetters.get(propertyName);
if (propertyMethods == null) {
propertyMethods = new ArrayList<JMethod>();
allSetters.put(propertyName, propertyMethods);
}
propertyMethods.add(method);
}
/**
* 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 propertyName the name of the property/setter.
* @param propertySetters the collection of setters.
* @return the setter to use, or null if none is good enough.
*/
private JMethod disambiguateSetters(String propertyName,
Collection<JMethod> propertySetters) {
// if only have one overload, there is no need to rank them.
if (propertySetters.size() == 1) {
return propertySetters.iterator().next();
}
// rank overloads and pick the one with minimum 'cost' of conversion.
JMethod preferredMethod = null;
int minRank = Integer.MAX_VALUE;
for (JMethod method : propertySetters) {
int rank = rankMethodOnParameters(method);
if (rank < minRank) {
minRank = rank;
preferredMethod = method;
ambiguousSetters.remove(propertyName);
} else if (rank == minRank &&
!sameParameterTypes(preferredMethod, method)) {
// sameParameterTypes test is necessary because a setter can be
// overridden by a subclass and that is not considered ambiguous.
if (!ambiguousSetters.contains(propertyName)) {
ambiguousSetters.add(propertyName);
}
}
}
// if the setter is ambiguous, return null.
if (ambiguousSetters.contains(propertyName)) {
return null;
}
// the setter is not ambiguous therefore return the preferred overload.
return preferredMethod;
}
/**
* 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"
String beanPropertyName = Introspector.decapitalize(propertyName);
addSetter(allSetters, beanPropertyName, method);
// keep backwards compatibility (i.e. hTML instead of HTML for setHTML)
String legacyPropertyName = StringCase.toLower(propertyName.substring(0, 1))
+ propertyName.substring(1);
if (!legacyPropertyName.equals(beanPropertyName)) {
addSetter(allSetters, legacyPropertyName, 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
ambiguousSetters = new HashSet<String>();
for (String propertyName : allSetters.keySet()) {
Collection<JMethod> propertySetters = allSetters.get(propertyName);
JMethod setter = disambiguateSetters(propertyName, propertySetters);
setters.put(propertyName, setter);
}
if (ambiguousSetters.size() == 0) {
ambiguousSetters = null;
}
}
/**
* 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 {
while (ownerType != null) {
JMethod[] methods = ownerType.getMethods();
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 = StringCase.toLower(name.substring(3));
} else {
logger.die(method.getName()
+ " must either specify a UiChild tagname or begin "
+ "with \"add\".");
}
}
JParameter[] parameters = method.getParameters();
if (parameters.length == 0) {
logger.die("%s must take at least one Object argument", method.getName());
}
JType type = parameters[0].getType();
if (type.isClassOrInterface() == null) {
logger.die("%s first parameter must be an object type, found %s",
method.getName(), type.getQualifiedSourceName());
}
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;
}
/**
* Ranks given method based on parameter conversion cost. A lower rank is
* preferred over a higher rank since it has a lower cost of conversion.
*
* The ranking criteria is as follows:
* 1) methods with fewer arguments are preferred. for instance:
* 'setValue(int)' is preferred 'setValue(int, int)'.
* 2) within a set of overloads with the same number of arguments:
* 2.1) String has the lowest cost = 1
* 2.2) primitive types, cost = 2
* 2.3) boxed primitive types, cost = 3
* 2.4) any (reference types, etc), cost = 4.
* 3) if a setter is overridden by a subclass and have the exact same argument
* types, it will not be considered ambiguous.
*
* The cost mapping is defined in
* {@link #TYPE_RANK typeRank }
* @param method
* @return the rank of the method.
*/
private int rankMethodOnParameters(JMethod method) {
JParameter[] params = method.getParameters();
int rank = 0;
for (int i = 0; i < Math.min(params.length, 10); i++) {
JType paramType = params[i].getType();
int cost = DEFAULT_COST;
if (TYPE_RANK.containsKey(paramType.getQualifiedSourceName())) {
cost = TYPE_RANK.get(paramType.getQualifiedSourceName());
}
assert (cost >= 0 && cost <= 0x07);
rank = rank | (cost << (3 * i));
}
assert (rank >= 0);
return rank;
}
/**
* 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;
}
}
return true;
}
}