| /* | |
| * 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.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 defaultCost = 4; | |
| private static final Map<String, Integer> typeRank; | |
| 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); | |
| typeRank = 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; | |
| } | |
| /** | |
| * 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" | |
| 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 | |
| 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 { | |
| 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; | |
| } | |
| /** | |
| * 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 #typeRank 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 = defaultCost; | |
| if (typeRank.containsKey(paramType.getQualifiedSourceName())) { | |
| cost = typeRank.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; | |
| } | |
| } |