| /* |
| * 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.thirdparty.guava.common.collect.LinkedHashMultimap; |
| import com.google.gwt.thirdparty.guava.common.collect.Multimap; |
| 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.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Locale; |
| 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; |
| } |
| |
| /** |
| * 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 && !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 Multimap<String, JMethod> findAllSetters(JClassType fieldType) { |
| Multimap<String, JMethod> allSetters = LinkedHashMultimap.create(); |
| for (JMethod method : fieldType.getInheritableMethods()) { |
| if (!isSetterMethod(method)) { |
| continue; |
| } |
| |
| // Take out "set" |
| String propertyName = method.getName().substring(3); |
| |
| // turn "PropertyName" into "propertyName" |
| String beanPropertyName = Introspector.decapitalize(propertyName); |
| allSetters.put(beanPropertyName, method); |
| |
| // keep backwards compatibility (i.e. hTML instead of HTML for setHTML) |
| String legacyPropertyName = propertyName.substring(0, 1).toLowerCase(Locale.ROOT) |
| + propertyName.substring(1); |
| if (!legacyPropertyName.equals(beanPropertyName)) { |
| allSetters.put(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 |
| Multimap<String, 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 = name.substring(3).toLowerCase(Locale.ROOT); |
| } 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; |
| } |
| } |