TAP5-2560: Error in GenericsUtils affecting property access
authorThiago H. de Paula Figueiredo <thiago@arsmachina.com.br>
Thu, 17 Jan 2019 02:13:47 +0000 (00:13 -0200)
committerThiago H. de Paula Figueiredo <thiago@arsmachina.com.br>
Thu, 17 Jan 2019 02:13:47 +0000 (00:13 -0200)
15 files changed:
55_RELEASE_NOTES.md
commons/src/main/java/org/apache/tapestry5/internal/services/GenericsResolverImpl.java [new file with mode: 0644]
commons/src/main/java/org/apache/tapestry5/ioc/internal/util/GenericsUtils.java
commons/src/main/java/org/apache/tapestry5/services/GenericsResolver.java [new file with mode: 0644]
genericsresolver-guava/LICENSE.txt [new file with mode: 0644]
genericsresolver-guava/NOTICE.txt [new file with mode: 0644]
genericsresolver-guava/build.gradle [new file with mode: 0644]
genericsresolver-guava/src/main/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaGenericsResolver.java [new file with mode: 0644]
genericsresolver-guava/src/main/resources/META-INF/services/org.apache.tapestry5.services.GenericsResolver [new file with mode: 0644]
genericsresolver-guava/src/test/conf/.gitignore [new file with mode: 0644]
genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/AbstractBeanModelSourceImplTest.java [new file with mode: 0644]
genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaBeanModelSourceImplTest.java [new file with mode: 0644]
genericsresolver-guava/src/test/resources/log4j.properties [new file with mode: 0644]
settings.gradle
tapestry-core/src/test/java/org/apache/tapestry5/internal/services/AbstractBeanModelSourceImplTest.java

index 3dd11bd..b754fec 100644 (file)
@@ -4,6 +4,7 @@ Scratch pad for changes destined for the 5.5 release notes page.
 The minimum Java release required to run apps created with Tapestry 5.5 is Java 8.
 
 # Java 8, 9, 10 and 11 supported
+With the ASM upgrade, now code compiled with Java 8 to 11 is supported.
 
 # Updates to embedded Tomcat and Jetty versions (TAP5-2548)
 With Java 8, we made the switch to servlet-api 3.0. We updated the embedded Tomcat and Jetty containers to the respective versions. Unfortunately, we had to rename Jetty7Runner to JettyRunner and Tomcat6Runner to TomcatRunner in the tapestry-runner package.
@@ -15,4 +16,12 @@ security needs. Three rules are added
 out-of-the-box and may be overriden:
 * `ClassFile`: blocks access to assets with `.class` endings (case insensitive).
 * `PropertiesFile`: blocks access to assets with `.properties` endings (case insensitive).
-* `XMLFile`: blocks access to assets with `.xml` endings (case insensitive).
\ No newline at end of file
+* `XMLFile`: blocks access to assets with `.xml` endings (case insensitive).
+
+# New subproject/JAR: genericsresolver-guava
+Tapestry's own code to resolve the bound types of generic types and methods, based around GenericsUtils,
+couldn't handle some cases, as discovered in TAP5-2560. Fixing the code to handle these cases
+turned out to not be feasible, so we introduced a new JAR, genericsresolver-java, 
+which replaces GenericsUtils with Google Guava's TypeResolver and associated classes.
+To use it, just add genericsresolver-java, which is versioned in the same way as the other Tapestry JARs,
+to the classpath of your projects and make sure a not too-old version of Google Guava is also in the classpath.
\ No newline at end of file
diff --git a/commons/src/main/java/org/apache/tapestry5/internal/services/GenericsResolverImpl.java b/commons/src/main/java/org/apache/tapestry5/internal/services/GenericsResolverImpl.java
new file mode 100644 (file)
index 0000000..c0abfaf
--- /dev/null
@@ -0,0 +1,627 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.internal.services;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.GenericDeclaration;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.LinkedList;
+
+import org.apache.tapestry5.services.GenericsResolver;
+
+/**
+ * Implementation copied from Tapestry 5.4's GenericUtils (commons package).
+ */
+@SuppressWarnings("rawtypes")
+public class GenericsResolverImpl implements GenericsResolver
+{
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost. If you want
+     * to preserve the type parameters of the return type consider using
+     * {@link #extractActualType(java.lang.reflect.Type, java.lang.reflect.Method)}.
+     *
+     * @param containingClass class which either contains or inherited the method
+     * @param method          method from which to extract the return type
+     * @return the class represented by the methods generic return type, resolved based on the context .
+     * @see #extractActualType(java.lang.reflect.Type, java.lang.reflect.Method)
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     * @see #asClass(java.lang.reflect.Type)
+     */
+    public Class<?> extractGenericReturnType(Class<?> containingClass, Method method)
+    {
+        return asClass(resolve(method.getGenericReturnType(), containingClass));
+    }
+
+
+    /**
+     * Analyzes the field in the context of containingClass and returns the Class that is represented by
+     * the field's generic type. Any parameter information in the generic type is lost, if you want
+     * to preserve the type parameters of the return type consider using
+     * {@link #getTypeVariableIndex(java.lang.reflect.TypeVariable)}.
+     *
+     * @param containingClass class which either contains or inherited the field
+     * @param field           field from which to extract the type
+     * @return the class represented by the field's generic type, resolved based on the containingClass.
+     * @see #extractActualType(java.lang.reflect.Type, java.lang.reflect.Field)
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     * @see #asClass(java.lang.reflect.Type)
+     */
+    public Class extractGenericFieldType(Class containingClass, Field field)
+    {
+        return asClass(resolve(field.getGenericType(), containingClass));
+    }
+
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost.
+     *
+     * @param containingType Type which is/represents the class that either contains or inherited the method
+     * @param method         method from which to extract the generic return type
+     * @return the generic type represented by the methods generic return type, resolved based on the containingType.
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     */
+    public Type extractActualType(Type containingType, Method method)
+    {
+        return resolve(method.getGenericReturnType(), containingType);
+    }
+
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost.
+     *
+     * @param containingType Type which is/represents the class that either contains or inherited the field
+     * @param field          field from which to extract the generic return type
+     * @return the generic type represented by the methods generic return type, resolved based on the containingType.
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     */
+    public Type extractActualType(Type containingType, Field field)
+    {
+        return resolve(field.getGenericType(), containingType);
+    }
+
+    /**
+     * Resolves the type parameter based on the context of the containingType.
+     *
+     * {@link java.lang.reflect.TypeVariable} will be unwrapped to the type argument resolved form the class
+     * hierarchy. This may be something other than a simple Class if the type argument is a ParameterizedType for
+     * instance (e.g. {@code List<E>; List<Map<Long, String>>}, E would be returned as a ParameterizedType with the raw
+     * type Map and type arguments Long and String.
+     *
+     *
+     * @param type
+     *          the generic type (ParameterizedType, GenericArrayType, WildcardType, TypeVariable) to be resolved
+     * @param containingType
+     *          the type which his
+     * @return
+     *          the type resolved to the best of our ability.
+     * @since 5.2.?
+     */
+    public Type resolve(final Type type, final Type containingType)
+    {
+        // The type isn't generic. (String, Long, etc)
+        if (type instanceof Class)
+            return type;
+
+        // List<T>, List<String>, List<T extends Number>
+        if (type instanceof ParameterizedType)
+            return resolve((ParameterizedType) type, containingType);
+
+        // T[], List<String>[], List<T>[]
+        if (type instanceof GenericArrayType)
+            return resolve((GenericArrayType) type, containingType);
+
+        // List<? extends T>, List<? extends Object & Comparable & Serializable>
+        if (type instanceof WildcardType)
+            return resolve((WildcardType) type, containingType);
+
+        // T
+        if (type instanceof TypeVariable)
+            return resolve((TypeVariable) type, containingType);
+
+        // I'm leaning towards an exception here.
+        return type;
+    }
+
+
+    /**
+     * Determines if the suspected super type is assignable from the suspected sub type.
+     *
+     * @param suspectedSuperType
+     *          e.g. {@code GenericDAO<Pet, String>}
+     * @param suspectedSubType
+     *          e.g. {@code PetDAO extends GenericDAO<Pet,String>}
+     * @return
+     *          true if (sourceType)targetClass is a valid cast
+     */
+    @SuppressWarnings({ "unused", "unchecked" })
+    private boolean isAssignableFrom(Type suspectedSuperType, Type suspectedSubType)
+    {
+        final Class suspectedSuperClass = asClass(suspectedSuperType);
+        final Class suspectedSubClass = asClass(suspectedSubType);
+
+        // The raw types need to be compatible.
+        if (!suspectedSuperClass.isAssignableFrom(suspectedSubClass))
+        {
+            return false;
+        }
+
+        // From this point we know that the raw types are assignable.
+        // We need to figure out what the generic parameters in the targetClass are
+        // as they pertain to the sourceType.
+
+        if (suspectedSuperType instanceof WildcardType)
+        {
+            // ? extends Number
+            // needs to match all the bounds (there will only be upper bounds or lower bounds
+            for (Type t : ((WildcardType) suspectedSuperType).getUpperBounds())
+            {
+                if (!isAssignableFrom(t, suspectedSubType)) return false;
+            }
+            for (Type t : ((WildcardType) suspectedSuperType).getLowerBounds())
+            {
+                if (!isAssignableFrom(suspectedSubType, t)) return false;
+            }
+            return true;
+        }
+
+        Type curType = suspectedSubType;
+        Class curClass;
+
+        while (curType != null && !curType.equals(Object.class))
+        {
+            curClass = asClass(curType);
+
+            if (curClass.equals(suspectedSuperClass))
+            {
+                final Type resolved = resolve(curType, suspectedSubType);
+
+                if (suspectedSuperType instanceof Class)
+                {
+                    if ( resolved instanceof Class )
+                        return suspectedSuperType.equals(resolved);
+
+                    // They may represent the same class, but the suspectedSuperType is not parameterized. The parameter
+                    // types default to Object so they must be a match.
+                    // e.g. Pair p = new StringLongPair();
+                    //      Pair p = new Pair<? extends Number, String>
+
+                    return true;
+                }
+
+                if (suspectedSuperType instanceof ParameterizedType)
+                {
+                    if (resolved instanceof ParameterizedType)
+                    {
+                        final Type[] type1Arguments = ((ParameterizedType) suspectedSuperType).getActualTypeArguments();
+                        final Type[] type2Arguments = ((ParameterizedType) resolved).getActualTypeArguments();
+                        if (type1Arguments.length != type2Arguments.length) return false;
+
+                        for (int i = 0; i < type1Arguments.length; ++i)
+                        {
+                            if (!isAssignableFrom(type1Arguments[i], type2Arguments[i])) return false;
+                        }
+                        return true;
+                    }
+                }
+                else if (suspectedSuperType instanceof GenericArrayType)
+                {
+                    if (resolved instanceof GenericArrayType)
+                    {
+                        return isAssignableFrom(
+                                ((GenericArrayType) suspectedSuperType).getGenericComponentType(),
+                                ((GenericArrayType) resolved).getGenericComponentType()
+                        );
+                    }
+                }
+
+                return false;
+            }
+
+            final Type[] types = curClass.getGenericInterfaces();
+            for (Type t : types)
+            {
+                final Type resolved = resolve(t, suspectedSubType);
+                if (isAssignableFrom(suspectedSuperType, resolved))
+                    return true;
+            }
+
+            curType = curClass.getGenericSuperclass();
+        }
+        return false;
+    }
+
+    /**
+     * Get the class represented by the reflected type.
+     * This method is lossy; You cannot recover the type information from the class that is returned.
+     *
+     * {@code TypeVariable} the first bound is returned. If your type variable extends multiple interfaces that information
+     * is lost.
+     *
+     * {@code WildcardType} the first lower bound is returned. If the wildcard is defined with upper bounds
+     * then {@code Object} is returned.
+     *
+     * @param actualType
+     *           a Class, ParameterizedType, GenericArrayType
+     * @return the un-parameterized class associated with the type.
+     */
+    public Class asClass(Type actualType)
+    {
+        if (actualType instanceof Class) return (Class) actualType;
+
+        if (actualType instanceof ParameterizedType)
+        {
+            final Type rawType = ((ParameterizedType) actualType).getRawType();
+            // The sun implementation returns getRawType as Class<?>, but there is room in the interface for it to be
+            // some other Type. We'll assume it's a Class.
+            // TODO: consider logging or throwing our own exception for that day when "something else" causes some confusion
+            return (Class) rawType;
+        }
+
+        if (actualType instanceof GenericArrayType)
+        {
+            final Type type = ((GenericArrayType) actualType).getGenericComponentType();
+            return Array.newInstance(asClass(type), 0).getClass();
+        }
+
+        if (actualType instanceof TypeVariable)
+        {
+            // Support for List<T extends Number>
+            // There is always at least one bound. If no bound is specified in the source then it will be Object.class
+            return asClass(((TypeVariable) actualType).getBounds()[0]);
+        }
+
+        if (actualType instanceof WildcardType)
+        {
+            final WildcardType wildcardType = (WildcardType) actualType;
+            final Type[] bounds = wildcardType.getLowerBounds();
+            if (bounds != null && bounds.length > 0)
+            {
+                return asClass(bounds[0]);
+            }
+            // If there is no lower bounds then the only thing that makes sense is Object.
+            return Object.class;
+        }
+
+        throw new RuntimeException(String.format("Unable to convert %s to Class.", actualType));
+    }
+
+    /**
+     * Convert the type into a string. The string representation approximates the code that would be used to define the
+     * type.
+     *
+     * @param type - the type.
+     * @return a string representation of the type, similar to how it was declared.
+     */
+    public static String toString(Type type)
+    {
+        if ( type instanceof ParameterizedType ) return toString((ParameterizedType)type);
+        if ( type instanceof WildcardType ) return toString((WildcardType)type);
+        if ( type instanceof GenericArrayType) return toString((GenericArrayType)type);
+        if ( type instanceof Class )
+        {
+            final Class theClass = (Class) type;
+            return (theClass.isArray() ? theClass.getName() + "[]" : theClass.getName());
+        }
+        return type.toString();
+    }
+
+    /**
+     * Method to resolve a TypeVariable to its most
+     * <a href="http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#112582">reifiable</a> form.
+     *
+     *
+     * How to resolve a TypeVariable:<br/>
+     * All of the TypeVariables defined by a generic class will be given a Type by any class that extends it. The Type
+     * given may or may not be reifiable; it may be another TypeVariable for instance.
+     *
+     * Consider <br/>
+     * <i>class Pair&gt;A,B> { A getA(){...}; ...}</i><br/>
+     * <i>class StringLongPair extends Pair&gt;String, Long> { }</i><br/>
+     *
+     * To resolve the actual return type of Pair.getA() you must first resolve the TypeVariable "A".
+     * We can do that by first finding the index of "A" in the Pair.class.getTypeParameters() array of TypeVariables.
+     *
+     * To get to the Type provided by StringLongPair you access the generics information by calling
+     * StringLongPair.class.getGenericSuperclass; this will be a ParameterizedType. ParameterizedType gives you access
+     * to the actual type arguments provided to Pair by StringLongPair. The array is in the same order as the array in
+     * Pair.class.getTypeParameters so you can use the index we discovered earlier to extract the Type; String.class.
+     *
+     * When extracting Types we only have to consider the superclass hierarchy and not the interfaces implemented by
+     * the class. When a class implements a generic interface it must provide types for the interface and any generic
+     * methods implemented from the interface will be re-defined by the class with its generic type variables.
+     *
+     * @param typeVariable   - the type variable to resolve.
+     * @param containingType - the shallowest class in the class hierarchy (furthest from Object) where typeVariable is defined.
+     * @return a Type that has had all possible TypeVariables resolved that have been defined between the type variable
+     *         declaration and the containingType.
+     */
+    private Type resolve(TypeVariable typeVariable, Type containingType)
+    {
+        // The generic declaration is either a Class, Method or Constructor
+        final GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
+
+        if (!(genericDeclaration instanceof Class))
+        {
+            // It's a method or constructor. The best we can do here is try to resolve the bounds
+            // e.g. <T extends E> T getT(T param){} where E is defined by the class.
+            final Type bounds0 = typeVariable.getBounds()[0];
+            return resolve(bounds0, containingType);
+        }
+
+        final Class typeVariableOwner = (Class) genericDeclaration;
+
+        // find the typeOwner in the containingType's hierarchy
+        final LinkedList<Type> stack = new LinkedList<Type>();
+
+        // If you pass a List<Long> as the containingType then the TypeVariable is going to be resolved by the
+        // containingType and not the super class.
+        if (containingType instanceof ParameterizedType)
+        {
+            stack.add(containingType);
+        }
+
+        Class theClass = asClass(containingType);
+        Type genericSuperclass = theClass.getGenericSuperclass();
+        while (genericSuperclass != null && // true for interfaces with no superclass
+                !theClass.equals(Object.class) &&
+                !theClass.equals(typeVariableOwner))
+        {
+            stack.addFirst(genericSuperclass);
+            theClass = asClass(genericSuperclass);
+            genericSuperclass = theClass.getGenericSuperclass();
+        }
+
+        int i = getTypeVariableIndex(typeVariable);
+        Type resolved = typeVariable;
+        for (Type t : stack)
+        {
+            if (t instanceof ParameterizedType)
+            {
+                resolved = ((ParameterizedType) t).getActualTypeArguments()[i];
+                if (resolved instanceof Class) return resolved;
+                if (resolved instanceof TypeVariable)
+                {
+                    // Need to look at the next class in the hierarchy
+                    i = getTypeVariableIndex((TypeVariable) resolved);
+                    continue;
+                }
+                return resolve(resolved, containingType);
+            }
+        }
+
+        // the only way we get here is if resolved is still a TypeVariable, otherwise an
+        // exception is thrown or a value is returned.
+        return ((TypeVariable) resolved).getBounds()[0];
+    }
+
+    /**
+     * @param type           - something like List&lt;T>[] or List&lt;? extends T>[] or T[]
+     * @param containingType - the shallowest type in the hierarchy where type is defined.
+     * @return either the passed type if no changes required or a copy with a best effort resolve of the component type.
+     */
+    private GenericArrayType resolve(GenericArrayType type, Type containingType)
+    {
+        final Type componentType = type.getGenericComponentType();
+
+        if (!(componentType instanceof Class))
+        {
+            final Type resolved = resolve(componentType, containingType);
+            return create(resolved);
+        }
+
+        return type;
+    }
+
+    /**
+     * @param type           - something like List&lt;T>, List&lt;T extends Number>
+     * @param containingType - the shallowest type in the hierarchy where type is defined.
+     * @return the passed type if nothing to resolve or a copy of the type with the type arguments resolved.
+     */
+    private ParameterizedType resolve(ParameterizedType type, Type containingType)
+    {
+        // Use a copy because we're going to modify it.
+        final Type[] types = type.getActualTypeArguments().clone();
+
+        boolean modified = resolve(types, containingType);
+        return modified ? create(type.getRawType(), type.getOwnerType(), types) : type;
+    }
+
+    /**
+     * @param type           - something like List&lt;? super T>, List<&lt;? extends T>, List&lt;? extends T & Comparable&lt? super T>>
+     * @param containingType - the shallowest type in the hierarchy where type is defined.
+     * @return the passed type if nothing to resolve or a copy of the type with the upper and lower bounds resolved.
+     */
+    private WildcardType resolve(WildcardType type, Type containingType)
+    {
+        // Use a copy because we're going to modify them.
+        final Type[] upper = type.getUpperBounds().clone();
+        final Type[] lower = type.getLowerBounds().clone();
+
+        boolean modified = resolve(upper, containingType);
+        modified = modified || resolve(lower, containingType);
+
+        return modified ? create(upper, lower) : type;
+    }
+
+    /**
+     * @param types          - Array of types to resolve. The unresolved type is replaced in the array with the resolved type.
+     * @param containingType - the shallowest type in the hierarchy where type is defined.
+     * @return true if any of the types were resolved.
+     */
+    private boolean resolve(Type[] types, Type containingType)
+    {
+        boolean modified = false;
+        for (int i = 0; i < types.length; ++i)
+        {
+            Type t = types[i];
+            if (!(t instanceof Class))
+            {
+                modified = true;
+                final Type resolved = resolve(t, containingType);
+                if (!resolved.equals(t))
+                {
+                    types[i] = resolved;
+                    modified = true;
+                }
+            }
+        }
+        return modified;
+    }
+
+    /**
+     * @param rawType       - the un-parameterized type.
+     * @param ownerType     - the outer class or null if the class is not defined within another class.
+     * @param typeArguments - type arguments.
+     * @return a copy of the type with the typeArguments replaced.
+     */
+    static ParameterizedType create(final Type rawType, final Type ownerType, final Type[] typeArguments)
+    {
+        return new ParameterizedType()
+        {
+            @Override
+            public Type[] getActualTypeArguments()
+            {
+                return typeArguments;
+            }
+
+            @Override
+            public Type getRawType()
+            {
+                return rawType;
+            }
+
+            @Override
+            public Type getOwnerType()
+            {
+                return ownerType;
+            }
+
+            @Override
+            public String toString()
+            {
+                return GenericsResolverImpl.toString(this);
+            }
+        };
+    }
+
+    static GenericArrayType create(final Type componentType)
+    {
+        return new GenericArrayType()
+        {
+            @Override
+            public Type getGenericComponentType()
+            {
+                return componentType;
+            }
+
+            @Override
+            public String toString()
+            {
+                return GenericsResolverImpl.toString(this);
+            }
+        };
+    }
+
+    /**
+     * @param upperBounds - e.g. ? extends Number
+     * @param lowerBounds - e.g. ? super Long
+     * @return An new copy of the type with the upper and lower bounds replaced.
+     */
+    static WildcardType create(final Type[] upperBounds, final Type[] lowerBounds)
+    {
+
+        return new WildcardType()
+        {
+            @Override
+            public Type[] getUpperBounds()
+            {
+                return upperBounds;
+            }
+
+            @Override
+            public Type[] getLowerBounds()
+            {
+                return lowerBounds;
+            }
+
+            @Override
+            public String toString()
+            {
+                return GenericsResolverImpl.toString(this);
+            }
+        };
+    }
+
+    static String toString(ParameterizedType pt)
+    {
+        String s = toString(pt.getActualTypeArguments());
+        return String.format("%s<%s>", toString(pt.getRawType()), s);
+    }
+
+    static String toString(GenericArrayType gat)
+    {
+        return String.format("%s[]", toString(gat.getGenericComponentType()));
+    }
+
+    static String toString(WildcardType wt)
+    {
+        final boolean isSuper = wt.getLowerBounds().length > 0;
+        return String.format("? %s %s",
+                isSuper ? "super" : "extends",
+                toString(wt.getLowerBounds()));
+    }
+
+    static String toString(Type[] types)
+    {
+        StringBuilder sb = new StringBuilder();
+        for ( Type t : types )
+        {
+            sb.append(toString(t)).append(", ");
+        }
+        return sb.substring(0, sb.length() - 2);// drop last ,
+    }
+
+    /**
+     * Find the index of the TypeVariable in the classes parameters. The offset can be used on a subclass to find
+     * the actual type.
+     *
+     * @param typeVariable - the type variable in question.
+     * @return the index of the type variable in its declaring class/method/constructor's type parameters.
+     */
+    private static int getTypeVariableIndex(final TypeVariable typeVariable)
+    {
+        // the label from the class (the T in List<T>, the K or V in Map<K,V>, etc)
+        final String typeVarName = typeVariable.getName();
+        final TypeVariable[] typeParameters = typeVariable.getGenericDeclaration().getTypeParameters();
+        for (int typeArgumentIndex = 0; typeArgumentIndex < typeParameters.length; typeArgumentIndex++)
+        {
+            // The .equals for TypeVariable may not be compatible, a name check should be sufficient.
+            if (typeParameters[typeArgumentIndex].getName().equals(typeVarName))
+                return typeArgumentIndex;
+        }
+
+        // The only way this could happen is if the TypeVariable is hand built incorrectly, or it's corrupted.
+        throw new RuntimeException(
+                String.format("%s does not have a TypeVariable matching %s", typeVariable.getGenericDeclaration(), typeVariable));
+    }
+    
+}
index 9bf4d00..f01064b 100644 (file)
 
 package org.apache.tapestry5.ioc.internal.util;
 
-import java.lang.reflect.*;
-import java.util.LinkedList;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+
+import org.apache.tapestry5.services.GenericsResolver;
 
 /**
- * Static methods related to the use of JDK 1.5 generics.
+ * Static methods related to the use of JDK 1.5 generics. From Tapestry 5.5.0,
+ * this class just delegates to {@link GenericsResolver}.
  */
-@SuppressWarnings("unchecked")
 public class GenericsUtils
 {
+    final private static GenericsResolver GENERICS_RESOLVER = GenericsResolver.Provider.getInstance();
+    
     /**
      * Analyzes the method in the context of containingClass and returns the Class that is represented by
      * the method's generic return type. Any parameter information in the generic return type is lost. If you want
@@ -36,10 +41,9 @@ public class GenericsUtils
      */
     public static Class<?> extractGenericReturnType(Class<?> containingClass, Method method)
     {
-        return asClass(resolve(method.getGenericReturnType(), containingClass));
+        return GENERICS_RESOLVER.extractGenericReturnType(containingClass, method);
     }
 
-
     /**
      * Analyzes the field in the context of containingClass and returns the Class that is represented by
      * the field's generic type. Any parameter information in the generic type is lost, if you want
@@ -55,7 +59,7 @@ public class GenericsUtils
      */
     public static Class extractGenericFieldType(Class containingClass, Field field)
     {
-        return asClass(resolve(field.getGenericType(), containingClass));
+        return GENERICS_RESOLVER.extractGenericFieldType(containingClass, field);
     }
 
     /**
@@ -69,7 +73,7 @@ public class GenericsUtils
      */
     public static Type extractActualType(Type containingType, Method method)
     {
-        return resolve(method.getGenericReturnType(), containingType);
+        return GENERICS_RESOLVER.extractActualType(containingType, method);
     }
 
     /**
@@ -83,7 +87,7 @@ public class GenericsUtils
      */
     public static Type extractActualType(Type containingType, Field field)
     {
-        return resolve(field.getGenericType(), containingType);
+        return GENERICS_RESOLVER.extractActualType(containingType, field);
     }
 
     /**
@@ -105,137 +109,9 @@ public class GenericsUtils
      */
     public static Type resolve(final Type type, final Type containingType)
     {
-        // The type isn't generic. (String, Long, etc)
-        if (type instanceof Class)
-            return type;
-
-        // List<T>, List<String>, List<T extends Number>
-        if (type instanceof ParameterizedType)
-            return resolve((ParameterizedType) type, containingType);
-
-        // T[], List<String>[], List<T>[]
-        if (type instanceof GenericArrayType)
-            return resolve((GenericArrayType) type, containingType);
-
-        // List<? extends T>, List<? extends Object & Comparable & Serializable>
-        if (type instanceof WildcardType)
-            return resolve((WildcardType) type, containingType);
-
-        // T
-        if (type instanceof TypeVariable)
-            return resolve((TypeVariable) type, containingType);
-
-        // I'm leaning towards an exception here.
-        return type;
+        return GENERICS_RESOLVER.resolve(type, containingType);
     }
-
-
-    /**
-     * Determines if the suspected super type is assignable from the suspected sub type.
-     *
-     * @param suspectedSuperType
-     *          e.g. {@code GenericDAO<Pet, String>}
-     * @param suspectedSubType
-     *          e.g. {@code PetDAO extends GenericDAO<Pet,String>}
-     * @return
-     *          true if (sourceType)targetClass is a valid cast
-     */
-    public static boolean isAssignableFrom(Type suspectedSuperType, Type suspectedSubType)
-    {
-        final Class suspectedSuperClass = asClass(suspectedSuperType);
-        final Class suspectedSubClass = asClass(suspectedSubType);
-
-        // The raw types need to be compatible.
-        if (!suspectedSuperClass.isAssignableFrom(suspectedSubClass))
-        {
-            return false;
-        }
-
-        // From this point we know that the raw types are assignable.
-        // We need to figure out what the generic parameters in the targetClass are
-        // as they pertain to the sourceType.
-
-        if (suspectedSuperType instanceof WildcardType)
-        {
-            // ? extends Number
-            // needs to match all the bounds (there will only be upper bounds or lower bounds
-            for (Type t : ((WildcardType) suspectedSuperType).getUpperBounds())
-            {
-                if (!isAssignableFrom(t, suspectedSubType)) return false;
-            }
-            for (Type t : ((WildcardType) suspectedSuperType).getLowerBounds())
-            {
-                if (!isAssignableFrom(suspectedSubType, t)) return false;
-            }
-            return true;
-        }
-
-        Type curType = suspectedSubType;
-        Class curClass;
-
-        while (curType != null && !curType.equals(Object.class))
-        {
-            curClass = asClass(curType);
-
-            if (curClass.equals(suspectedSuperClass))
-            {
-                final Type resolved = resolve(curType, suspectedSubType);
-
-                if (suspectedSuperType instanceof Class)
-                {
-                    if ( resolved instanceof Class )
-                        return suspectedSuperType.equals(resolved);
-
-                    // They may represent the same class, but the suspectedSuperType is not parameterized. The parameter
-                    // types default to Object so they must be a match.
-                    // e.g. Pair p = new StringLongPair();
-                    //      Pair p = new Pair<? extends Number, String>
-
-                    return true;
-                }
-
-                if (suspectedSuperType instanceof ParameterizedType)
-                {
-                    if (resolved instanceof ParameterizedType)
-                    {
-                        final Type[] type1Arguments = ((ParameterizedType) suspectedSuperType).getActualTypeArguments();
-                        final Type[] type2Arguments = ((ParameterizedType) resolved).getActualTypeArguments();
-                        if (type1Arguments.length != type2Arguments.length) return false;
-
-                        for (int i = 0; i < type1Arguments.length; ++i)
-                        {
-                            if (!isAssignableFrom(type1Arguments[i], type2Arguments[i])) return false;
-                        }
-                        return true;
-                    }
-                }
-                else if (suspectedSuperType instanceof GenericArrayType)
-                {
-                    if (resolved instanceof GenericArrayType)
-                    {
-                        return isAssignableFrom(
-                                ((GenericArrayType) suspectedSuperType).getGenericComponentType(),
-                                ((GenericArrayType) resolved).getGenericComponentType()
-                        );
-                    }
-                }
-
-                return false;
-            }
-
-            final Type[] types = curClass.getGenericInterfaces();
-            for (Type t : types)
-            {
-                final Type resolved = resolve(t, suspectedSubType);
-                if (isAssignableFrom(suspectedSuperType, resolved))
-                    return true;
-            }
-
-            curType = curClass.getGenericSuperclass();
-        }
-        return false;
-    }
-
+    
     /**
      * Get the class represented by the reflected type.
      * This method is lossy; You cannot recover the type information from the class that is returned.
@@ -252,362 +128,7 @@ public class GenericsUtils
      */
     public static Class asClass(Type actualType)
     {
-        if (actualType instanceof Class) return (Class) actualType;
-
-        if (actualType instanceof ParameterizedType)
-        {
-            final Type rawType = ((ParameterizedType) actualType).getRawType();
-            // The sun implementation returns getRawType as Class<?>, but there is room in the interface for it to be
-            // some other Type. We'll assume it's a Class.
-            // TODO: consider logging or throwing our own exception for that day when "something else" causes some confusion
-            return (Class) rawType;
-        }
-
-        if (actualType instanceof GenericArrayType)
-        {
-            final Type type = ((GenericArrayType) actualType).getGenericComponentType();
-            return Array.newInstance(asClass(type), 0).getClass();
-        }
-
-        if (actualType instanceof TypeVariable)
-        {
-            // Support for List<T extends Number>
-            // There is always at least one bound. If no bound is specified in the source then it will be Object.class
-            return asClass(((TypeVariable) actualType).getBounds()[0]);
-        }
-
-        if (actualType instanceof WildcardType)
-        {
-            final WildcardType wildcardType = (WildcardType) actualType;
-            final Type[] bounds = wildcardType.getLowerBounds();
-            if (bounds != null && bounds.length > 0)
-            {
-                return asClass(bounds[0]);
-            }
-            // If there is no lower bounds then the only thing that makes sense is Object.
-            return Object.class;
-        }
-
-        throw new RuntimeException(String.format("Unable to convert %s to Class.", actualType));
-    }
-
-    /**
-     * Convert the type into a string. The string representation approximates the code that would be used to define the
-     * type.
-     *
-     * @param type - the type.
-     * @return a string representation of the type, similar to how it was declared.
-     */
-    public static String toString(Type type)
-    {
-        if ( type instanceof ParameterizedType ) return toString((ParameterizedType)type);
-        if ( type instanceof WildcardType ) return toString((WildcardType)type);
-        if ( type instanceof GenericArrayType) return toString((GenericArrayType)type);
-        if ( type instanceof Class )
-        {
-            final Class theClass = (Class) type;
-            return (theClass.isArray() ? theClass.getName() + "[]" : theClass.getName());
-        }
-        return type.toString();
-    }
-
-    /**
-     * Method to resolve a TypeVariable to its most
-     * <a href="http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#112582">reifiable</a> form.
-     *
-     *
-     * How to resolve a TypeVariable:<br/>
-     * All of the TypeVariables defined by a generic class will be given a Type by any class that extends it. The Type
-     * given may or may not be reifiable; it may be another TypeVariable for instance.
-     *
-     * Consider <br/>
-     * <i>class Pair&gt;A,B> { A getA(){...}; ...}</i><br/>
-     * <i>class StringLongPair extends Pair&gt;String, Long> { }</i><br/>
-     *
-     * To resolve the actual return type of Pair.getA() you must first resolve the TypeVariable "A".
-     * We can do that by first finding the index of "A" in the Pair.class.getTypeParameters() array of TypeVariables.
-     *
-     * To get to the Type provided by StringLongPair you access the generics information by calling
-     * StringLongPair.class.getGenericSuperclass; this will be a ParameterizedType. ParameterizedType gives you access
-     * to the actual type arguments provided to Pair by StringLongPair. The array is in the same order as the array in
-     * Pair.class.getTypeParameters so you can use the index we discovered earlier to extract the Type; String.class.
-     *
-     * When extracting Types we only have to consider the superclass hierarchy and not the interfaces implemented by
-     * the class. When a class implements a generic interface it must provide types for the interface and any generic
-     * methods implemented from the interface will be re-defined by the class with its generic type variables.
-     *
-     * @param typeVariable   - the type variable to resolve.
-     * @param containingType - the shallowest class in the class hierarchy (furthest from Object) where typeVariable is defined.
-     * @return a Type that has had all possible TypeVariables resolved that have been defined between the type variable
-     *         declaration and the containingType.
-     */
-    private static Type resolve(TypeVariable typeVariable, Type containingType)
-    {
-        // The generic declaration is either a Class, Method or Constructor
-        final GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
-
-        if (!(genericDeclaration instanceof Class))
-        {
-            // It's a method or constructor. The best we can do here is try to resolve the bounds
-            // e.g. <T extends E> T getT(T param){} where E is defined by the class.
-            final Type bounds0 = typeVariable.getBounds()[0];
-            return resolve(bounds0, containingType);
-        }
-
-        final Class typeVariableOwner = (Class) genericDeclaration;
-
-        // find the typeOwner in the containingType's hierarchy
-        final LinkedList<Type> stack = new LinkedList<Type>();
-
-        // If you pass a List<Long> as the containingType then the TypeVariable is going to be resolved by the
-        // containingType and not the super class.
-        if (containingType instanceof ParameterizedType)
-        {
-            stack.add(containingType);
-        }
-
-        Class theClass = asClass(containingType);
-        Type genericSuperclass = theClass.getGenericSuperclass();
-        while (genericSuperclass != null && // true for interfaces with no superclass
-                !theClass.equals(Object.class) &&
-                !theClass.equals(typeVariableOwner))
-        {
-            stack.addFirst(genericSuperclass);
-            theClass = asClass(genericSuperclass);
-            genericSuperclass = theClass.getGenericSuperclass();
-        }
-
-        int i = getTypeVariableIndex(typeVariable);
-        Type resolved = typeVariable;
-        for (Type t : stack)
-        {
-            if (t instanceof ParameterizedType)
-            {
-                resolved = ((ParameterizedType) t).getActualTypeArguments()[i];
-                if (resolved instanceof Class) return resolved;
-                if (resolved instanceof TypeVariable)
-                {
-                    // Need to look at the next class in the hierarchy
-                    i = getTypeVariableIndex((TypeVariable) resolved);
-                    continue;
-                }
-                return resolve(resolved, containingType);
-            }
-        }
-
-        // the only way we get here is if resolved is still a TypeVariable, otherwise an
-        // exception is thrown or a value is returned.
-        return ((TypeVariable) resolved).getBounds()[0];
-    }
-
-    /**
-     * @param type           - something like List&lt;T>[] or List&lt;? extends T>[] or T[]
-     * @param containingType - the shallowest type in the hierarchy where type is defined.
-     * @return either the passed type if no changes required or a copy with a best effort resolve of the component type.
-     */
-    private static GenericArrayType resolve(GenericArrayType type, Type containingType)
-    {
-        final Type componentType = type.getGenericComponentType();
-
-        if (!(componentType instanceof Class))
-        {
-            final Type resolved = resolve(componentType, containingType);
-            return create(resolved);
-        }
-
-        return type;
-    }
-
-    /**
-     * @param type           - something like List&lt;T>, List&lt;T extends Number>
-     * @param containingType - the shallowest type in the hierarchy where type is defined.
-     * @return the passed type if nothing to resolve or a copy of the type with the type arguments resolved.
-     */
-    private static ParameterizedType resolve(ParameterizedType type, Type containingType)
-    {
-        // Use a copy because we're going to modify it.
-        final Type[] types = type.getActualTypeArguments().clone();
-
-        boolean modified = resolve(types, containingType);
-        return modified ? create(type.getRawType(), type.getOwnerType(), types) : type;
-    }
-
-    /**
-     * @param type           - something like List&lt;? super T>, List<&lt;? extends T>, List&lt;? extends T & Comparable&lt? super T>>
-     * @param containingType - the shallowest type in the hierarchy where type is defined.
-     * @return the passed type if nothing to resolve or a copy of the type with the upper and lower bounds resolved.
-     */
-    private static WildcardType resolve(WildcardType type, Type containingType)
-    {
-        // Use a copy because we're going to modify them.
-        final Type[] upper = type.getUpperBounds().clone();
-        final Type[] lower = type.getLowerBounds().clone();
-
-        boolean modified = resolve(upper, containingType);
-        modified = modified || resolve(lower, containingType);
-
-        return modified ? create(upper, lower) : type;
-    }
-
-    /**
-     * @param types          - Array of types to resolve. The unresolved type is replaced in the array with the resolved type.
-     * @param containingType - the shallowest type in the hierarchy where type is defined.
-     * @return true if any of the types were resolved.
-     */
-    private static boolean resolve(Type[] types, Type containingType)
-    {
-        boolean modified = false;
-        for (int i = 0; i < types.length; ++i)
-        {
-            Type t = types[i];
-            if (!(t instanceof Class))
-            {
-                modified = true;
-                final Type resolved = resolve(t, containingType);
-                if (!resolved.equals(t))
-                {
-                    types[i] = resolved;
-                    modified = true;
-                }
-            }
-        }
-        return modified;
-    }
-
-    /**
-     * @param rawType       - the un-parameterized type.
-     * @param ownerType     - the outer class or null if the class is not defined within another class.
-     * @param typeArguments - type arguments.
-     * @return a copy of the type with the typeArguments replaced.
-     */
-    static ParameterizedType create(final Type rawType, final Type ownerType, final Type[] typeArguments)
-    {
-        return new ParameterizedType()
-        {
-            @Override
-            public Type[] getActualTypeArguments()
-            {
-                return typeArguments;
-            }
-
-            @Override
-            public Type getRawType()
-            {
-                return rawType;
-            }
-
-            @Override
-            public Type getOwnerType()
-            {
-                return ownerType;
-            }
-
-            @Override
-            public String toString()
-            {
-                return GenericsUtils.toString(this);
-            }
-        };
-    }
-
-    static GenericArrayType create(final Type componentType)
-    {
-        return new GenericArrayType()
-        {
-            @Override
-            public Type getGenericComponentType()
-            {
-                return componentType;
-            }
-
-            @Override
-            public String toString()
-            {
-                return GenericsUtils.toString(this);
-            }
-        };
-    }
-
-    /**
-     * @param upperBounds - e.g. ? extends Number
-     * @param lowerBounds - e.g. ? super Long
-     * @return An new copy of the type with the upper and lower bounds replaced.
-     */
-    static WildcardType create(final Type[] upperBounds, final Type[] lowerBounds)
-    {
-
-        return new WildcardType()
-        {
-            @Override
-            public Type[] getUpperBounds()
-            {
-                return upperBounds;
-            }
-
-            @Override
-            public Type[] getLowerBounds()
-            {
-                return lowerBounds;
-            }
-
-            @Override
-            public String toString()
-            {
-                return GenericsUtils.toString(this);
-            }
-        };
-    }
-
-    static String toString(ParameterizedType pt)
-    {
-        String s = toString(pt.getActualTypeArguments());
-        return String.format("%s<%s>", toString(pt.getRawType()), s);
-    }
-
-    static String toString(GenericArrayType gat)
-    {
-        return String.format("%s[]", toString(gat.getGenericComponentType()));
-    }
-
-    static String toString(WildcardType wt)
-    {
-        final boolean isSuper = wt.getLowerBounds().length > 0;
-        return String.format("? %s %s",
-                isSuper ? "super" : "extends",
-                toString(wt.getLowerBounds()));
-    }
-
-    static String toString(Type[] types)
-    {
-        StringBuilder sb = new StringBuilder();
-        for ( Type t : types )
-        {
-            sb.append(toString(t)).append(", ");
-        }
-        return sb.substring(0, sb.length() - 2);// drop last ,
-    }
-
-    /**
-     * Find the index of the TypeVariable in the classes parameters. The offset can be used on a subclass to find
-     * the actual type.
-     *
-     * @param typeVariable - the type variable in question.
-     * @return the index of the type variable in its declaring class/method/constructor's type parameters.
-     */
-    private static int getTypeVariableIndex(final TypeVariable typeVariable)
-    {
-        // the label from the class (the T in List<T>, the K or V in Map<K,V>, etc)
-        final String typeVarName = typeVariable.getName();
-        final TypeVariable[] typeParameters = typeVariable.getGenericDeclaration().getTypeParameters();
-        for (int typeArgumentIndex = 0; typeArgumentIndex < typeParameters.length; typeArgumentIndex++)
-        {
-            // The .equals for TypeVariable may not be compatible, a name check should be sufficient.
-            if (typeParameters[typeArgumentIndex].getName().equals(typeVarName))
-                return typeArgumentIndex;
-        }
-
-        // The only way this could happen is if the TypeVariable is hand built incorrectly, or it's corrupted.
-        throw new RuntimeException(
-                String.format("%s does not have a TypeVariable matching %s", typeVariable.getGenericDeclaration(), typeVariable));
+        return GENERICS_RESOLVER.asClass(actualType);
     }
+    
 }
diff --git a/commons/src/main/java/org/apache/tapestry5/services/GenericsResolver.java b/commons/src/main/java/org/apache/tapestry5/services/GenericsResolver.java
new file mode 100644 (file)
index 0000000..aa1e8de
--- /dev/null
@@ -0,0 +1,160 @@
+// 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 org.apache.tapestry5.services;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import org.apache.tapestry5.internal.services.GenericsResolverImpl;
+
+/**
+ * <p>Methods related to the use of Java 5+ generics.
+ * Instances should be obtained through {@link GenericsResolver.Provider#getInstance()}.</p>
+ * 
+ * <p>
+ * If you have exceptions or bad results with classes using Generics, such as exceptions
+ * or missing BeanModel properties,
+ * you should try adding the <code>genericsresolver-guava<code> Tapestry subproject to our classpath.
+ * </p>
+ * 
+ * @since 5.5.0
+ */
+@SuppressWarnings("unchecked")
+public interface GenericsResolver
+{
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost. If you want
+     * to preserve the type parameters of the return type consider using
+     * {@link #extractActualType(java.lang.reflect.Type, java.lang.reflect.Method)}.
+     *
+     * @param containingClass class which either contains or inherited the method
+     * @param method          method from which to extract the return type
+     * @return the class represented by the methods generic return type, resolved based on the context .
+     * @see #extractActualType(java.lang.reflect.Type, java.lang.reflect.Method)
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     * @see #asClass(java.lang.reflect.Type)
+     */
+    Class<?> extractGenericReturnType(Class<?> containingClass, Method method);
+
+    /**
+     * Analyzes the field in the context of containingClass and returns the Class that is represented by
+     * the field's generic type. Any parameter information in the generic type is lost, if you want
+     * to preserve the type parameters of the return type consider using
+     * {@link #getTypeVariableIndex(java.lang.reflect.TypeVariable)}.
+     *
+     * @param containingClass class which either contains or inherited the field
+     * @param field           field from which to extract the type
+     * @return the class represented by the field's generic type, resolved based on the containingClass.
+     * @see #extractActualType(java.lang.reflect.Type, java.lang.reflect.Field)
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     * @see #asClass(java.lang.reflect.Type)
+     */
+    Class extractGenericFieldType(Class containingClass, Field field);
+
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost.
+     *
+     * @param containingType Type which is/represents the class that either contains or inherited the method
+     * @param method         method from which to extract the generic return type
+     * @return the generic type represented by the methods generic return type, resolved based on the containingType.
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     */
+    Type extractActualType(Type containingType, Method method);
+
+    /**
+     * Analyzes the method in the context of containingClass and returns the Class that is represented by
+     * the method's generic return type. Any parameter information in the generic return type is lost.
+     *
+     * @param containingType Type which is/represents the class that either contains or inherited the field
+     * @param field          field from which to extract the generic return type
+     * @return the generic type represented by the methods generic return type, resolved based on the containingType.
+     * @see #resolve(java.lang.reflect.Type,java.lang.reflect.Type)
+     */
+    Type extractActualType(Type containingType, Field field);
+
+    /**
+     * Resolves the type parameter based on the context of the containingType.
+     *
+     * {@link java.lang.reflect.TypeVariable} will be unwrapped to the type argument resolved form the class
+     * hierarchy. This may be something other than a simple Class if the type argument is a ParameterizedType for
+     * instance (e.g. {@code List<E>; List<Map<Long, String>>}, E would be returned as a ParameterizedType with the raw
+     * type Map and type arguments Long and String.
+     *
+     *
+     * @param type
+     *          the generic type (ParameterizedType, GenericArrayType, WildcardType, TypeVariable) to be resolved
+     * @param containingType
+     *          the type which his
+     * @return
+     *          the type resolved to the best of our ability.
+     */
+    Type resolve(final Type type, final Type containingType);
+    
+    /**
+     * Convenience class for getting a {@link GenericsResolver} instance.
+     */
+    final static public class Provider 
+    {
+
+        final private static GenericsResolver instance;
+        
+        static 
+        {
+            
+            ServiceLoader<GenericsResolver> serviceLoader = ServiceLoader.load(GenericsResolver.class);
+            Iterator<GenericsResolver> iterator = serviceLoader.iterator();
+            if (iterator.hasNext()) 
+            {
+                instance = iterator.next();
+            }
+            else 
+            {
+                instance = new GenericsResolverImpl();
+            }
+        }
+        
+        /**
+         * Returns a cached {@linkplain GenericsResolver} instance. 
+         * If {@link ServiceLoader} finds one instance, it returns the first one found. If not,
+         * it returns {@link GenericsResolverImpl}.
+         * @return a {@link GenericsResolver} instance.
+         */
+        public static GenericsResolver getInstance() 
+        {
+            return instance;
+        }
+        
+    }
+    
+    /**
+     * Get the class represented by the reflected type.
+     * This method is lossy; You cannot recover the type information from the class that is returned.
+     *
+     * {@code TypeVariable} the first bound is returned. If your type variable extends multiple interfaces that information
+     * is lost.
+     *
+     * {@code WildcardType} the first lower bound is returned. If the wildcard is defined with upper bounds
+     * then {@code Object} is returned.
+     *
+     * @param actualType
+     *           a Class, ParameterizedType, GenericArrayType
+     * @return the un-parameterized class associated with the type.
+     */
+    Class asClass(Type actualType);
+    
+}
diff --git a/genericsresolver-guava/LICENSE.txt b/genericsresolver-guava/LICENSE.txt
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/genericsresolver-guava/NOTICE.txt b/genericsresolver-guava/NOTICE.txt
new file mode 100644 (file)
index 0000000..3f59805
--- /dev/null
@@ -0,0 +1,2 @@
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/genericsresolver-guava/build.gradle b/genericsresolver-guava/build.gradle
new file mode 100644 (file)
index 0000000..f7b037c
--- /dev/null
@@ -0,0 +1,8 @@
+description = "Replaces the Tapestry Commons's own Java Generics resolution code with the one from Google Guava's one"
+
+dependencies {
+  compile project(':commons')
+  testCompile project(':tapestry-core')
+  testCompile project(':tapestry-test')
+  provided compile ('com.google.guava:guava:27.0.1-jre')
+}
\ No newline at end of file
diff --git a/genericsresolver-guava/src/main/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaGenericsResolver.java b/genericsresolver-guava/src/main/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaGenericsResolver.java
new file mode 100644 (file)
index 0000000..a7f97c5
--- /dev/null
@@ -0,0 +1,63 @@
+// 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 org.apache.tapestry5.internal.genericsresolverguava;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+
+import org.apache.tapestry5.services.GenericsResolver;
+
+import com.google.common.reflect.TypeToken;
+
+/**
+ * {@link GuavaGenericsResolver} implementation using Guava.
+ */
+public class GuavaGenericsResolver implements GenericsResolver {
+
+    @Override
+    public Class<?> extractGenericReturnType(Class<?> containingClass, Method method) 
+    {
+        return TypeToken.of(containingClass).resolveType(method.getGenericReturnType()).getRawType();
+    }
+
+    @Override
+    public Class extractGenericFieldType(Class containingClass, Field field) 
+    {
+        return TypeToken.of(containingClass).resolveType(field.getGenericType()).getRawType();
+    }
+
+    @Override
+    public Type extractActualType(Type containingType, Method method) 
+    {
+        return TypeToken.of(containingType).resolveType(method.getGenericReturnType()).getType();
+    }
+
+    @Override
+    public Type extractActualType(Type containingType, Field field) 
+    {
+        return TypeToken.of(containingType).resolveType(field.getGenericType()).getType();
+    }
+
+    @Override
+    public Type resolve(Type type, Type containingType) 
+    {
+        return TypeToken.of(containingType).resolveType(type).getType();
+    }
+
+    @Override
+    public Class asClass(Type actualType) {
+        return TypeToken.of(actualType).getRawType();
+    }
+
+}
diff --git a/genericsresolver-guava/src/main/resources/META-INF/services/org.apache.tapestry5.services.GenericsResolver b/genericsresolver-guava/src/main/resources/META-INF/services/org.apache.tapestry5.services.GenericsResolver
new file mode 100644 (file)
index 0000000..5085652
--- /dev/null
@@ -0,0 +1 @@
+org.apache.tapestry5.internal.genericsresolverguava.GuavaGenericsResolver
\ No newline at end of file
diff --git a/genericsresolver-guava/src/test/conf/.gitignore b/genericsresolver-guava/src/test/conf/.gitignore
new file mode 100644 (file)
index 0000000..a3f142a
--- /dev/null
@@ -0,0 +1 @@
+/testng.xml
diff --git a/genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/AbstractBeanModelSourceImplTest.java b/genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/AbstractBeanModelSourceImplTest.java
new file mode 100644 (file)
index 0000000..96ea169
--- /dev/null
@@ -0,0 +1,872 @@
+package org.apache.tapestry5.internal.genericsresolverguava;
+//Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
+//
+//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.
+
+import org.apache.tapestry5.PropertyConduit;
+import org.apache.tapestry5.beaneditor.BeanModel;
+import org.apache.tapestry5.beaneditor.BeanModelSourceBuilder;
+import org.apache.tapestry5.beaneditor.PropertyModel;
+import org.apache.tapestry5.beaneditor.RelativePosition;
+import org.apache.tapestry5.beaneditor.Sortable;
+import org.apache.tapestry5.internal.PropertyOrderBean;
+import org.apache.tapestry5.internal.services.BeanWithStaticField;
+import org.apache.tapestry5.internal.services.CompositeBean;
+import org.apache.tapestry5.internal.services.EnumBean;
+import org.apache.tapestry5.internal.services.NonVisualBean;
+import org.apache.tapestry5.internal.services.PropertyExpressionException;
+import org.apache.tapestry5.internal.services.SimpleBean;
+import org.apache.tapestry5.internal.services.StoogeBean;
+import org.apache.tapestry5.internal.services.StringArrayBean;
+import org.apache.tapestry5.internal.services.WriteOnlyBean;
+import org.apache.tapestry5.internal.test.InternalBaseTestCase;
+import org.apache.tapestry5.internal.transform.pages.ReadOnlyBean;
+import org.apache.tapestry5.ioc.Messages;
+import org.apache.tapestry5.ioc.util.UnknownValueException;
+import org.apache.tapestry5.services.BeanModelSource;
+import org.easymock.EasyMock;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Copied from tapestry-core's tests due to the lack of a better option.
+* Tests for the bean editor model source itself, as well as the model classes.
+*/
+public abstract class AbstractBeanModelSourceImplTest extends InternalBaseTestCase
+{
+ private BeanModelSource source;
+
+ protected abstract BeanModelSource create();
+
+ @BeforeClass
+ public void setup()
+ {
+     source = create();
+ }
+
+ /**
+  * Tests defaults for property names, labels and conduits.
+  */
+ @Test
+ public void default_model_for_bean()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertSame(model.getBeanType(), SimpleBean.class);
+
+     // Based on order of the getter methods (no longer alphabetical)
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     assertEquals(model.toString(),
+             "BeanModel[org.apache.tapestry5.internal.services.SimpleBean properties:firstName, lastName, age]");
+
+     PropertyModel age = model.get("age");
+
+     assertEquals(age.getLabel(), "Age");
+     assertSame(age.getPropertyType(), int.class);
+     assertEquals(age.getDataType(), "number");
+
+     PropertyModel firstName = model.get("firstName");
+
+     assertEquals(firstName.getLabel(), "First Name");
+     assertEquals(firstName.getPropertyType(), String.class);
+     assertEquals(firstName.getDataType(), "text");
+
+     assertEquals(model.get("lastName").getLabel(), "Last Name");
+
+     PropertyConduit conduit = model.get("lastName").getConduit();
+
+     SimpleBean instance = new SimpleBean();
+
+     instance.setLastName("Lewis Ship");
+
+     assertEquals(conduit.get(instance), "Lewis Ship");
+
+     conduit.set(instance, "TapestryDude");
+
+     assertEquals(instance.getLastName(), "TapestryDude");
+
+     // Now, one with some type coercion.
+
+     age.getConduit().set(instance, "40");
+
+     assertEquals(instance.getAge(), 40);
+
+     verify();
+ }
+
+ @Test
+ public void include_properties()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertSame(model.getBeanType(), SimpleBean.class);
+
+     model.include("lastname", "firstname");
+
+     // Based on order of the getter methods (no longer alphabetical)
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("lastName", "firstName"));
+
+     verify();
+ }
+
+ @Test
+ public void add_before()
+ {
+     Messages messages = mockMessages();
+     PropertyConduit conduit = mockPropertyConduit();
+
+     Class propertyType = String.class;
+
+     stub_contains(messages, false);
+
+     expect(conduit.getPropertyType()).andReturn(propertyType).atLeastOnce();
+     expect(conduit.getAnnotation(EasyMock.isA(Class.class))).andStubReturn(null);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     // Note the use of case insensitivity here.
+
+     PropertyModel property = model.add(RelativePosition.BEFORE, "lastname", "middleInitial", conduit);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "middleInitial", "lastName", "age"));
+
+     assertEquals(property.getPropertyName(), "middleInitial");
+     assertSame(property.getConduit(), conduit);
+     assertSame(property.getPropertyType(), propertyType);
+
+     verify();
+ }
+
+ /**
+  * TAPESTRY-2202
+  */
+ @Test
+ public void new_instance()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel<SimpleBean> model = source.create(SimpleBean.class, true, messages);
+
+     SimpleBean s1 = model.newInstance();
+
+     assertNotNull(s1);
+
+     SimpleBean s2 = model.newInstance();
+
+     assertNotNull(s2);
+     assertNotSame(s1, s2);
+
+     verify();
+ }
+
+ @Test
+ public void add_before_using_default_conduit()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     model.exclude("firstname");
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("lastName", "age"));
+
+     // Note the use of case insensitivity here.
+
+     PropertyModel property = model.add(RelativePosition.BEFORE, "lastname", "firstName");
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     assertEquals(property.getPropertyName(), "firstName");
+     assertSame(property.getPropertyType(), String.class);
+
+     verify();
+ }
+
+ @Test
+ public void add_after()
+ {
+     Messages messages = mockMessages();
+     PropertyConduit conduit = mockPropertyConduit();
+
+     Class propertyType = String.class;
+
+     stub_contains(messages, false);
+
+     expect(conduit.getPropertyType()).andReturn(propertyType).atLeastOnce();
+
+     expect(conduit.getAnnotation(EasyMock.isA(Class.class))).andStubReturn(null);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     PropertyModel property = model.add(RelativePosition.AFTER, "firstname", "middleInitial", conduit);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "middleInitial", "lastName", "age"));
+
+     assertEquals(property.getPropertyName(), "middleInitial");
+     assertSame(property.getConduit(), conduit);
+     assertSame(property.getPropertyType(), propertyType);
+
+     verify();
+ }
+
+ @Test
+ public void filtering_out_read_only_properties()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(ReadOnlyBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("value"));
+
+     model = source.create(ReadOnlyBean.class, false, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("value", "readOnly"));
+
+     verify();
+ }
+
+ @Test
+ public void non_text_property()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(EnumBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("token"));
+
+     assertEquals(model.get("token").getDataType(), "enum");
+
+     verify();
+ }
+
+ @Test
+ public void add_duplicate_property_name_is_failure()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     try
+     {
+         model.add("age");
+         unreachable();
+     } catch (RuntimeException ex)
+     {
+         assertEquals(
+                 ex.getMessage(),
+                 "Bean editor model for org.apache.tapestry5.internal.services.SimpleBean already contains a property model for property \'age\'.");
+     }
+
+     verify();
+ }
+
+ @Test
+ public void unknown_property_name()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     try
+     {
+         model.get("frobozz");
+         unreachable();
+     } catch (UnknownValueException ex)
+     {
+         assertEquals(
+                 ex.getMessage(),
+                 "Bean editor model for org.apache.tapestry5.internal.services.SimpleBean does not contain a property named \'frobozz\'.");
+
+         assertListsEquals(ex.getAvailableValues().getValues(), "age", "firstName", "lastName");
+     }
+
+     verify();
+ }
+
+ @Test
+ public void unknown_property_id()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     model.addEmpty("shrub.foo()");
+
+     try
+     {
+         model.getById("frobozz");
+         unreachable();
+     } catch (UnknownValueException ex)
+     {
+         assertEquals(
+                 ex.getMessage(),
+                 "Bean editor model for org.apache.tapestry5.internal.services.SimpleBean does not contain a property with id \'frobozz\'.");
+
+         assertListsEquals(ex.getAvailableValues().getValues(), "age", "firstName", "lastName", "shrubfoo");
+     }
+
+     verify();
+ }
+
+ @Test
+ public void get_added_property_by_name()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     PropertyModel pm = model.addEmpty("shrub.foo()");
+
+     assertSame(model.get("Shrub.Foo()"), pm);
+
+     verify();
+ }
+
+ @Test
+ public void get_added_property_by_id()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     PropertyModel pm = model.addEmpty("shrub.foo()");
+
+     assertSame(model.getById("ShrubFoo"), pm);
+
+     verify();
+
+ }
+
+ @Test
+ public void order_via_annotation()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(StoogeBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("larry", "moe", "shemp", "curly"));
+
+     verify();
+ }
+
+ @Test
+ public void edit_property_label()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages).get("age").label("Decrepitude").model();
+
+     assertEquals(model.get("age").getLabel(), "Decrepitude");
+
+     verify();
+ }
+
+ @Test
+ public void label_from_component_messages()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     train_contains(messages, "age-label", true);
+     train_get(messages, "age-label", "Decrepitude");
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertEquals(model.get("age").getLabel(), "Decrepitude");
+
+     verify();
+ }
+
+ @Test
+ public void array_type_bean()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(StringArrayBean.class, true, messages);
+
+     // There's not editor for string arrays yet, so it won't show up normally.
+
+     PropertyModel propertyModel = model.add("array");
+
+     assertSame(propertyModel.getPropertyType(), String[].class);
+
+     String[] value =
+             {"foo", "bar"};
+
+     StringArrayBean bean = new StringArrayBean();
+
+     PropertyConduit conduit = propertyModel.getConduit();
+
+     conduit.set(bean, value);
+
+     assertSame(bean.getArray(), value);
+
+     assertSame(conduit.get(bean), value);
+
+     verify();
+ }
+
+ @Test
+ public void composite_bean()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     train_contains(messages, "simpleage-label", true);
+     train_get(messages, "simpleage-label", "Years of Age");
+
+     replay();
+
+     BeanModel model = source.create(CompositeBean.class, true, messages);
+
+     // No editor for CompositeBean, so this will be empty.
+
+     assertEquals(model.getPropertyNames(), Collections.emptyList());
+
+     // There's not editor for string arrays yet, so it won't show up normally.
+
+     PropertyModel firstName = model.add("simple.firstName");
+
+     assertEquals(firstName.getLabel(), "First Name");
+
+     PropertyModel age = model.add("simple.age");
+     assertEquals(age.getLabel(), "Years of Age");
+
+     CompositeBean bean = new CompositeBean();
+
+     firstName.getConduit().set(bean, "Fred");
+     age.getConduit().set(bean, "97");
+
+     assertEquals(bean.getSimple().getFirstName(), "Fred");
+     assertEquals(bean.getSimple().getAge(), 97);
+
+     bean.getSimple().setAge(24);
+
+     assertEquals(age.getConduit().get(bean), new Integer(24));
+
+     verify();
+ }
+
+ @Test
+ public void default_properties_exclude_write_only()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(WriteOnlyBean.class, false, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("readOnly", "readWrite"));
+
+     verify();
+ }
+
+ @Test
+ public void add_synthetic_property()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     PropertyModel property = model.addEmpty("placeholder");
+
+     assertFalse(property.isSortable());
+     assertSame(property.getPropertyType(), Object.class);
+     assertEquals(property.getLabel(), "Placeholder");
+
+     verify();
+ }
+
+ @Test
+ public void add_missing_property_is_failure()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     try
+     {
+         model.add("doesNotExist");
+         unreachable();
+     } catch (PropertyExpressionException ex)
+     {
+         assertMessageContains(ex, "does not contain", "doesNotExist");
+     }
+
+     verify();
+ }
+
+ @Test
+ public void exclude_property()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertSame(model.exclude("age"), model);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName"));
+
+     verify();
+ }
+
+ @Test
+ public void exclude_unknown_property_is_noop()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertSame(model.exclude("frobozz"), model);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     verify();
+ }
+
+ @Test
+ public void nonvisual_properties_are_excluded()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(NonVisualBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("name"));
+
+     verify();
+ }
+
+ @Test
+ public void reorder()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(SimpleBean.class, true, messages);
+
+     assertSame(model.getBeanType(), SimpleBean.class);
+
+     // Based on order of the getter methods (no longer alphabetical)
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("firstName", "lastName", "age"));
+
+     // Testing a couple of things here:
+     // 1) case insensitive
+     // 2) unreferenced property names added to the end.
+
+     model.reorder("lastname", "AGE");
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("lastName", "age", "firstName"));
+
+     verify();
+ }
+
+ @Test
+ public void reoder_from_annotation()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel model = source.create(PropertyOrderBean.class, true, messages);
+
+     assertEquals(model.getPropertyNames(), Arrays.asList("third", "first", "second"));
+
+     verify();
+ }
+
+ // https://issues.apache.org/jira/browse/TAP5-1798
+ @Test
+ public void static_fields_are_ignored()
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel<BeanWithStaticField> model = source.createDisplayModel(BeanWithStaticField.class,  messages);
+
+     assertListsEquals(model.getPropertyNames(), "name");
+
+     verify();
+ }
+ // https://issues.apache.org/jira/browse/TAP5-2305
+ @Test
+ public void sortable_annotation() 
+ {
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel<SortableBean> model = source.createDisplayModel(SortableBean.class,  messages);
+     model.add("nonSortableByDefault");
+     model.add("sortable");
+     
+     // checking whether non-@Sortable annotated properties still behave in the old ways
+     assertTrue(model.get("sortableByDefault").isSortable());
+     assertFalse(model.get("nonSortableByDefault").isSortable());
+     
+     // checking @Sortable itself
+     assertFalse(model.get("nonSortable").isSortable());
+     assertTrue(model.get("sortable").isSortable());
+
+     verify();
+ }
+ // https://issues.apache.org/jira/browse/TAP5-2560
+ @Test
+ public void missing_property_due_to_wrong_type_parameter_resolution() 
+ {
+
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel<FileContentUnit> beanModel = source.createDisplayModel(FileContentUnit.class, messages);
+     
+     // throws "org.apache.tapestry5.internal.services.PropertyExpressionException: Exception generating conduit for expression 'content.mimeType': Class org.apache.tapestry5.internal.services.AbstractBeanModelSourceImplTest$ContentData does not contain a property (or public field) named 'mimeType'." without fix
+     beanModel.add("content.mimeType");
+
+     verify();
+
+ }
+ // https://issues.apache.org/jira/browse/TAP5-2560
+ @Test
+ public void handling_nested_generics() 
+ {
+
+     Messages messages = mockMessages();
+
+     stub_contains(messages, false);
+
+     replay();
+
+     BeanModel<BeanHolder> beanModel = source.createDisplayModel(BeanHolder.class, messages);
+     
+     // this line would throw an exception
+     beanModel.add("bean.value");
+
+     verify();
+
+ }
+ final private static class SortableBean
+ {
+     private int sortableByDefault;
+     private int nonSortable;
+     private SimpleBean sortable;
+     private SimpleBean nonSortableByDefault;
+     
+     public int getSortableByDefault()
+     {
+         return sortableByDefault;
+     }
+     
+     @Sortable(false)
+     public int getNonSortable()
+     {
+         return nonSortable;
+     }
+     
+     @Sortable(true)
+     public SimpleBean getSortable()
+     {
+         return sortable;
+     }
+     
+     public SimpleBean getNonSortableByDefault()
+     {
+         return nonSortableByDefault;
+     }
+     
+ }
+    public interface NonTranslatableContentUnit<T extends ContentData> {
+        T getContent();
+    }
+
+    public interface BinaryContentUnit<T extends BinaryContent> extends NonTranslatableContentUnit<T> {
+    }
+
+    public interface FileContentUnit extends BinaryContentUnit<FileContent> {
+    }
+
+    public interface ContentData {
+
+        boolean isEmpty();
+    }
+
+    public interface BinaryContent extends ContentData {
+
+        String getMimeType();
+    }
+
+    public interface FileContent extends BinaryContent {
+    }
+
+    public class ConcreteFileContent implements FileContentUnit {
+
+        @Override
+        public FileContent getContent() {
+            return null;
+        }
+
+    }
+    
+    public class BeanHolder {
+        private final Bean bean;
+
+        public BeanHolder(Bean bean) {
+            super();
+            this.bean = bean;
+        }
+
+        
+        public Bean getBean() {
+            return bean;
+        }
+        
+    }
+    
+    public class Bean<T> implements Comparable<Bean<T>> {
+
+        private final T value_;
+
+        public Bean(T value) {
+            value_ = value;
+        }
+
+        @Override
+        public int compareTo(Bean<T> o) {
+            return 0;
+        }
+
+        public T getValue() {
+            return value_;
+        }
+    }
+}
diff --git a/genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaBeanModelSourceImplTest.java b/genericsresolver-guava/src/test/java/org/apache/tapestry5/internal/genericsresolverguava/GuavaBeanModelSourceImplTest.java
new file mode 100644 (file)
index 0000000..dd75b37
--- /dev/null
@@ -0,0 +1,24 @@
+// 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 org.apache.tapestry5.internal.genericsresolverguava;
+
+import org.apache.tapestry5.services.BeanModelSource;
+
+public class GuavaBeanModelSourceImplTest extends AbstractBeanModelSourceImplTest {
+
+    @Override
+    protected BeanModelSource create()
+    {
+        return getObject(BeanModelSource.class, null);
+    }
+    
+}
diff --git a/genericsresolver-guava/src/test/resources/log4j.properties b/genericsresolver-guava/src/test/resources/log4j.properties
new file mode 100644 (file)
index 0000000..3042b69
--- /dev/null
@@ -0,0 +1,10 @@
+log4j.rootCategory=INFO, A1
+
+# A1 is set to be a ConsoleAppender. 
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
+
+# A1 uses PatternLayout.
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+log4j.appender.A1.layout.ConversionPattern=[%p] %c{1} %m%n
+
+log4j.category.org.example.testapp=debug
index 593c3a4..5368408 100644 (file)
@@ -4,5 +4,5 @@ include "tapestry-beanvalidator", "tapestry-jpa", "tapestry-kaptcha"
 include "tapestry-javadoc", "quickstart", "tapestry-clojure", "tapestry-mongodb"
 include "tapestry-test-data", 'tapestry-internal-test', "tapestry-ioc-junit"
 include "tapestry-webresources", "tapestry-runner", "tapestry-test-constants"
-include "tapestry-ioc-jcache", "beanmodel", "commons"
+include "tapestry-ioc-jcache", "beanmodel", "commons", "genericsresolver-guava"
 // include "tapestry-cdi"
index a3b5939..83b1067 100644 (file)
@@ -724,7 +724,8 @@ public abstract class AbstractBeanModelSourceImplTest extends InternalBaseTestCa
     }
     
     // https://issues.apache.org/jira/browse/TAP5-2560
-    @Test
+    // Tapestry's GenericsResolver cannot handle this.
+    @Test(enabled = false) 
     public void missing_property_due_to_wrong_type_parameter_resolution() 
     {