All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sencha.gwt.uibinder.rebind.model.OwnerFieldClass Maven / Gradle / Ivy

The newest version!
/*
 * 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.sencha.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.sencha.gwt.uibinder.rebind.MortalLogger;
import com.sencha.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 TYPE_RANK;
  static {
    HashMap tmpTypeRank = new HashMap();
    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 ambiguousSetters;
  private final MortalLogger logger;
  private final JClassType rawType;
  private final Map setters = new HashMap();
  /**
   * Mapping from all of the @UiChild tags to their corresponding methods and
   * limits on being called.
   */
  private final Map> uiChildren = new HashMap>();

  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> 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> allSetters,
      String propertyName, JMethod method) {
    Collection propertyMethods = allSetters.get(propertyName);
    if (propertyMethods == null) {
      propertyMethods = new ArrayList();
      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 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> findAllSetters(JClassType fieldType) {
    Map> 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>();
    }

    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 = propertyName.substring(0, 1).toLowerCase()
          + 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> allSetters = findAllSetters(fieldType);

    // Pass two - disambiguate
    ambiguousSetters = new HashSet();
    for (String propertyName : allSetters.keySet()) {
      Collection 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();
            } 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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy