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

net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation Maven / Gradle / Ivy

There is a newer version: 1.7.0-async-beta
Show newest version
/* Copyright 2005-2006 Tim Fennell
 *
 * 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 net.sourceforge.stripes.util.bean;

import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.util.ReflectUtil;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The dynamic partner to a PropertyExpression that represents the evaluation of the expression
 * against a particular bean or starting object.  When constructed the evaluation will examine
 * type information on the bean and nested properties to create a chain of type information for
 * the expression.  The evaluation can then be used (repeatedly) to determine the type of the
 * expression, retrieve its value and set its value - all against the supplied object.
 *
 * @author Tim Fennell
 * @since Stripes 1.4
 */
public class PropertyExpressionEvaluation {
    private PropertyExpression expression;
    private Object bean;
    private NodeEvaluation root, leaf;

    /**
     * Constructs a new PropertyExpressionEvaluation for the expression and bean supplied.
     * Loops through the expression creating NodeEvaluation objects corresponding to each
     * node in the expression and then fills in type information so that it is accessible
     * to all further calls.
     *
     * @param expression a PropertyExpression
     * @param bean a non-null bean against which to evaluate the expression
     */
    public PropertyExpressionEvaluation(PropertyExpression expression, Object bean) {
        this.expression = expression;
        this.bean = bean;

        for (Node node = expression.getRootNode(); node != null; node = node.getNext()) {
            NodeEvaluation evaluation = new NodeEvaluation(this, node);
            if (this.root == null) {
                this.root = evaluation;
                this.leaf = evaluation;
            }
            else {
                this.leaf.setNext(evaluation);
                evaluation.setPrevious(this.leaf);
                this.leaf = evaluation;
            }
        }

        fillInTypeInformation();
        validateTypeInformation();
    }

    /**
     * Fetches the bean which was supplied as the starting point for evaluation in the
     * constructor to this evaluation.
     * @return the bean from which evaluation starts
     */
    public Object getBean() { return bean; }

    /**
     * Fetches the root (first) node in the evaluation, which can be used to traverse
     * through the nodes in series.
     * @return the root node in the evaluation
     */
    public NodeEvaluation getRootNode() {
        return this.root;
    }

    /**
     * Fetches the expression of which this is an evaluation.
     * @return the expression being evaluated
     */
    public PropertyExpression getExpression() {
        return expression;
    }

    /**
     * Examines the expression in context of the root bean provided to determine type
     * information for each node.  Does this by traversing a node at a time and examining
     * the various sources of type information available.
     */
    void fillInTypeInformation() {
        Type type = this.bean.getClass();

        for (NodeEvaluation current = this.root; current != null; current = current.getNext()) {
            // Firstly if the current type is a wildcard type of a type variable try and
            // figure out what the real value to use is
            while (type instanceof WildcardType || type instanceof TypeVariable) {
                if (type instanceof WildcardType) {
                    type = getWildcardTypeBound((WildcardType) type);
                }
                else {
                    type = getTypeVariableValue(current, ((TypeVariable) type));
                }
            }

            // If it's an array, return the component type
            if (type instanceof GenericArrayType) {
                type = ((GenericArrayType) type).getGenericComponentType();
                current.setValueType(type);
                current.setKeyType(Integer.class);
                current.setType(NodeType.ArrayEntry);
                continue;
            }
            else if (type instanceof Class && ((Class) type).isArray()) {
                type = ((Class) type).getComponentType();
                current.setValueType(type);
                current.setKeyType(Integer.class);
                current.setType(NodeType.ArrayEntry);
                continue;
            }

            // Else if it's parameterized and it's a List or Map, get the next type
            if (type instanceof ParameterizedType) {
                ParameterizedType ptype = (ParameterizedType) type;
                Type rawType = convertToClass(type, current);

                if (rawType instanceof Class) {
                    Class rawClass = (Class) rawType;
                    if (List.class.isAssignableFrom(rawClass)) {
                        type = ptype.getActualTypeArguments()[0];
                        current.setValueType(type);
                        current.setKeyType(Integer.class);
                        current.setType(NodeType.ListEntry);
                        continue;
                    }
                    else if (Map.class.isAssignableFrom(rawClass)) {
                        type = ptype.getActualTypeArguments()[1];
                        current.setValueType(type);
                        current.setKeyType( convertToClass(ptype.getActualTypeArguments()[0], current) );
                        current.setType(NodeType.MapEntry);
                        continue;
                    }
                    else {
                        // Since it could be user defined type with a type parameter we'll
                        // reassign the current type to be the raw type and let processing
                        // fall through to the bean property code
                        type = rawClass;
                    }
                }
                else {
                    // XXX Raw type is not a class?  What on earth do we do now?
                    break;
                }
            }

            // Else if it's just a regular class we can try looking for a property on it. If
            // no property exists, just bail out and return null immediately
            if (type instanceof Class) {
                Class clazz = (Class) type;
                String property = current.getNode().getStringValue();
                type = getBeanPropertyType(clazz, property);

                // XXX What do we do if type is a generic type?
                if (type != null) {
                    current.setValueType(type);
                    current.setType(NodeType.BeanProperty);
                }
            }

            // If we haven't gotten type information by now, try filling in with instance info
            if (type == null) {
                type = getTypeViaInstances(current);
                if (type == null) {
                    // FIXME: What do we do now?
                }
            }
        }
    }

    /**
     * Ensures no violations exist in the expression in the context of this evaluation. Currently,
     * this ensures that no attempt is made to access a bean property via a bracket expression. Such
     * an expression could be used to circumvent validations that use dot notation for the same
     * property. See STS-841 for
     * more information.
     */
    protected void validateTypeInformation() {
        for (NodeEvaluation n = getRootNode(); n != null; n = n.getNext()) {
            if (n.getType() == NodeType.BeanProperty && n.getNode().isBracketed()) {
                throw new EvaluationException("The expression \"" + getExpression().getSource()
                        + "\" illegally attempts to access a bean property using bracket notation");
            }
        }
    }

    /**
     * Fetches the type of a property with the given name on the Class of the specified type.
     * Uses the methods first to fetch the generic type if a PropertyDescriptor can be found,
     * otherwise looks for a public field and returns its generic type.
     *
     * @param beanClass the class of the JavaBean containing the property
     * @param property the name of the property
     * @return the Type if it can be determined, or null otherwise
     */
    protected Type getBeanPropertyType(Class beanClass, String property) {
        PropertyDescriptor pd = ReflectUtil.getPropertyDescriptor(beanClass, property);
        if (pd != null) {
            if (pd.getReadMethod() != null) {
                return pd.getReadMethod().getGenericReturnType();
            }
            else {
                return pd.getWriteMethod().getGenericParameterTypes()[0];
            }
        }
        else {
            Field field = ReflectUtil.getField(beanClass, property);
            if (field == null) {
                return null;
            }
            else  {
                return field.getGenericType();
            }
        }
    }

    /**
     * 

Locates and returns a non-bridge method for the method supplied. In certain cases the * Introspector will return PropertyDescriptors that contain bridge methods for read * and write methods. This usually results from classes implementing generic interfaces * that contain accessor method specifications with type parameters. Since the bridge * methods have inappropriate/unhelpful return and parameter types it is necessary to * locate the non-bridge method and use that instead.

* *

When supplied with a non-bridge method, the method parameter passed in is returned * immediately and no other work is performed.

* * @param m a Method instance, potentially a bridge method * @return a non-bridge method instance if one is locatable, otherwise the method passed in * @deprecated This method is no longer used. Bridge methods are handled transparently by * {@link ReflectUtil#getPropertyDescriptors(Class)}. */ @Deprecated protected Method untangleBridgeMethod(Method m) { if (!m.isBridge()) return m; try { // If it's a setter method the only way to really find the right method // is to hope that there's only one setter with the same name and a single // parameter!! if (m.getParameterTypes().length == 1) { // deal with set methods String name = m.getName(); for (Method m2 : m.getDeclaringClass().getMethods()) { if (name.equals(m2.getName()) && m2 != m && m2.getParameterTypes().length == m.getParameterTypes().length) { return m2; } } } else { // deal with get methods return m.getDeclaringClass().getMethod(m.getName()); } } catch (Exception e) { /* Suppress. */ } return m; } /** *

Determines the type of the supplied node and sets appropriate information on the node. * The type is discovered by fetching (and instantiating if necessary) all prior values * in the expression to determine the actual type of the prior node. The prior node is * then examined to determine the type of the node provided.

* *

After this method executes either 1) all necessary type information will be set on the * node and the appropriate type object returned or 2) an exception will be thrown.

* * @param end the node to instantiate up to and determine the type of * @return the Type of the node if possible * @throws NoSuchPropertyException if the previous node is a JavaBean (i.e. non-collection) * node and does not contain a property with the corresponding name * @throws EvaluationException if the previous node is a List or Map and does not contain * enough information to determine the type */ @SuppressWarnings("unchecked") protected Type getTypeViaInstances(NodeEvaluation end) throws EvaluationException, NoSuchPropertyException { Object previous; Object value = this.bean; // First loop through and get to the pre-cursor node using the type info we have for (NodeEvaluation node = this.root; node != end; node = node.getNext()) { PropertyAccessor accessor = node.getType().getPropertyAccessor(); previous = value; value = accessor.getValue(node, previous); if (value == null) { value = getDefaultValue(node); } } // Then determine how to fish for the next property in line previous = value; if (value instanceof Map) { value = ((Map) value).get(end.getNode().getTypedValue()); if (value != null) { end.setType(NodeType.MapEntry); end.setValueType(value.getClass()); end.setKeyType(end.getNode().getTypedValue().getClass()); return value.getClass(); } else { throw new EvaluationException("Not enough type information available to " + "evaluate expression. Expression: '" + expression + "'. Type information ran " + "out at node '" + end.getNode().getStringValue() + "', which represents a Map " + "entry. Please ensure that either the getter for the Map contains appropriate " + "generic type information or that it contains a value with the key type " + end.getNode().getTypedValue().getClass().getName() + " and value " + end.getNode().getStringValue()); } } else if (value instanceof List) { List list = (List) value; if (end.getNode().getTypedValue() instanceof Integer) { Integer index = (Integer) end.getNode().getTypedValue(); if (index < list.size()) { value = list.get(index); if (value != null) { end.setType(NodeType.ListEntry); end.setValueType(value.getClass()); end.setKeyType(Integer.class); return value.getClass(); } } } throw new EvaluationException("Not enough type information available to " + "evaluate expression. Expression: '" + expression + "'. Type information ran " + "out at node '" + end.getNode().getStringValue() + "', which represents a List " + "entry. Please ensure that either the getter for the List contains appropriate " + "generic type information or that the index is numeric and a value exists at " + "the supplied index (" + end.getNode().getStringValue() + ")."); } else { Type type = getBeanPropertyType(value.getClass(), end.getNode().getStringValue()); if (type != null) { end.setType(NodeType.BeanProperty); end.setValueType(type); return type; } else { throw new NoSuchPropertyException("Bean class " + previous.getClass().getName() + " does not contain a property called '" + end.getNode().getStringValue() + "'. As a result the following expression could not be evaluated: " + this.expression); } } } /** * Attempts to convert the {@link Type} object into a Class object. Currently will extract the * raw type from a {@link ParameterizedType} and the appropriate bound from a * {@link WildcardType}. If the result after these operations is a Class object it will * be cast and returned. Otherwise will return null. * * @param type the Type object to try and render as a Class * @return the Class if one can be determined, otherwise null */ protected Class convertToClass(Type type, NodeEvaluation evaluation) { // First extract any candidate type from Wildcards and Parameterized types if (type instanceof ParameterizedType) { type = ((ParameterizedType) type).getRawType(); } while (type instanceof WildcardType || type instanceof TypeVariable) { if (type instanceof WildcardType) { type = getWildcardTypeBound((WildcardType) type); } else if (type instanceof TypeVariable) { type = getTypeVariableValue(evaluation, (TypeVariable) type); } } // And now that we should have a single type, try and get a Class if (type instanceof Class) { return (Class) type; } else { return null; } } /** * Scans backwards in the expression for the last node which contained a JavaBean type * and attempts to use the type arguments to that class to find a match for the * TypeParameter provided. On its way also collects information from any parameterized * types and their super-types. * * @param evaluation the current NodeEvaluation * @param typeVar the TypeVariable to try and find a more concrete type for * @return the actual type argument for the type variable if possible, or null */ protected Type getTypeVariableValue(NodeEvaluation evaluation, TypeVariable typeVar) { // Type maps from TypeVariables to the corresponding Type. The first map contains entries // from parameterized types (and their super-types) discovered while going back up the // nodes. The second map contains information gathered by going up the superclasses // from the last concrete Class in the expression List, Type>> typemap1 = new ArrayList, Type>>(); List, Type>> typemap2 = new ArrayList, Type>>(); // Scan the evaluation chain for the first class or any parameterized types. Class lastBean = this.bean.getClass(); for (NodeEvaluation n = evaluation.getPrevious(); n != null; n = n.getPrevious()) { Type type = n.getValueType(); // Bean class found? Stop searching. if (type instanceof Class) { lastBean = (Class) n.getValueType(); break; } // Parameterized type? Add to the typemap along with parent parameterized types else if (type instanceof ParameterizedType) { ParameterizedType ptype = (ParameterizedType) type; while (ptype != null) { addTypeMappings(typemap1, ptype); // Now find the parent of the ptype and see if it's a ptype too! Type rawtype = ptype.getRawType(); if (rawtype instanceof Class) { Class superclass = (Class) rawtype; Type supertype = superclass.getGenericSuperclass(); ptype = (supertype instanceof ParameterizedType) ? (ParameterizedType) supertype : null; } } } } // Add the bean class and all its superclasses to the typemap. for (Class c = lastBean; c != null; c = c.getSuperclass()) { Type t = c.getGenericSuperclass(); if (t instanceof ParameterizedType) { addTypeMappings(typemap2, (ParameterizedType) t); } } Class declaration = (Class) typeVar.getGenericDeclaration(); Type type = null; // If the type variable doesn't come from a direct superclass of the // the last bean, check the mappings from parameterized types first if (!declaration.isAssignableFrom(lastBean)) { for (int i = typemap1.size() - 1; i >= 0; i--) { // Map the type variable to a type. if ((type = typemap1.get(i).get(typeVar)) != null) { // Reached a real class? Done. if (type instanceof Class) { return type; } else if (type instanceof TypeVariable) { typeVar = (TypeVariable) type; } } } } // If we did the above traverse and still ended up at another type // variable, check the last bean (and parents') mappings for (int i = typemap2.size() - 1; i >= 0; i--) { // Map the type variable to a type. if ((type = typemap2.get(i).get(typeVar)) != null) { // Reached a real class? Done. if (type instanceof Class) { return type; } else if (type instanceof TypeVariable) { typeVar = (TypeVariable) type; } } } return type; } /** * Build a map of TypeVariables to Types. We have to traverse the class hierarchy * from subclass to superclass, but the mapping of TypeVariables to Types has to start * from the place were the type variable was originally defined (superclass) and map * to the place where the type is bound to an actual class (subclass). We therefore * need to build up the mappings sub to super and then traverse super to sub. * * @param paramType parameterized type to add to the map. */ private void addTypeMappings(List, Type>> typemap, ParameterizedType paramType) { Type rawType = paramType.getRawType(); if (rawType instanceof Class) { Class rawClass = (Class) rawType; TypeVariable[] vars = rawClass.getTypeParameters(); Type[] args = paramType.getActualTypeArguments(); HashMap, Type> entry = new HashMap, Type>(vars.length); for (int i = 0; i < vars.length && i < args.length; ++i) { entry.put(vars[i], args[i]); } typemap.add(entry); } } /** * Gets the preferred bound from the WildcardType provided. In the case of * '? super SomeClass' then 'SomeClass' will be returned. In the case of * '? extends AnotherClass' then 'AnotherClass' will be returned. * * @param wtype the WildcardType to fetch the bounds of * @return the appropriate bound type */ protected Type getWildcardTypeBound(WildcardType wtype) { Type[] bounds = wtype.getLowerBounds(); if (bounds.length == 0) { bounds = wtype.getUpperBounds(); } if (bounds.length > 0) { return bounds[0]; } return null; } /** * Fetches the type of value that can be get/set with this expression evaluation. This is * equivalent (though more efficient) to calling getValue().getClass(). If the type information * on this expression is not complete then null will be returned. * * @return the Class of object that can be set/get with this evaluation or null */ public Class getType() { return convertToClass(this.leaf.getValueType(), this.leaf); } /** * Returns a scalar type appropriate to the expression evaluation. When {@link #getType()} * returns a scalar type then this method will return the identical class. However, when * getType() returns an Array, a Collection or a Map this method will attempt to determine * the type of element stored in that Array/Collection/Map and return that Class. If * getType() returns null due to insufficient type information this method will also * return null. Similarly if the type of item in the Array/Collection/May cannot be determined * then String.class will be returned. * * @return The scalar type to which values should be converted in order to either be set * using this expression or set into the Array/Collection/Map should this expression * point at a non scalar property */ public Class getScalarType() { Type type = this.leaf.getValueType(); Class clazz = convertToClass(type, this.leaf); if (clazz.isArray()) { return clazz.getComponentType(); } else if (Collection.class.isAssignableFrom(clazz)) { if (type instanceof ParameterizedType) { return convertToClass(((ParameterizedType) type).getActualTypeArguments()[0], this.leaf); } else { return String.class; } } else if (Map.class.isAssignableFrom(clazz)) { if (type instanceof ParameterizedType) { return convertToClass(((ParameterizedType) type).getActualTypeArguments()[1], this.leaf); } else { return String.class; } } else { return clazz; } } /** *

Fetches the value of this expression evaluated against the bean. This is equivalent * to calling the appropriate get() methods in sequence in order to fetch the property * indicated by the expression.

* *

If the property or any intermediate property in the expression is null this method * will return null and will not alter the state of the object graph.

* * @return the value stored on the bean for this expression/property */ @SuppressWarnings("unchecked") public Object getValue() { Object nodeValue = this.bean; for (NodeEvaluation node = this.root; node != null && nodeValue != null; node = node.getNext()) { nodeValue = node.getType().getPropertyAccessor().getValue(node, nodeValue); } return nodeValue; } /** *

Sets the value of the expression evaluated against the bean. This is loosely equivalent * to calling the appropriate getter() on intermediate properties and then calling the * appropriate setter on the final sub-property.

* *

During set operations null intermediate nodes will be instantiated and linked into the * object graph in order to persistently set the desired property. When this is not possible * (e.g. because of a lack of default constructors) an exception will be thrown.

* * @param propertyValue the value to be set for the property of the bean * @throws EvaluationException if intermediate null properties cannot be instantiated */ @SuppressWarnings("unchecked") public void setValue(Object propertyValue) throws EvaluationException { Object nodeValue = this.bean; for (NodeEvaluation node = this.root; node != this.leaf && nodeValue != null; node = node.getNext()) { PropertyAccessor accessor = node.getType().getPropertyAccessor(); Object previous = nodeValue; nodeValue = accessor.getValue(node, previous); if (nodeValue == null) { nodeValue = getDefaultValue(node); node.getType().getPropertyAccessor().setValue(node, previous, nodeValue); } } this.leaf.getType().getPropertyAccessor().setValue(this.leaf, nodeValue, propertyValue); } /** *

Attempts to create a default value for a given node by either a) creating a new array * instance for arrays, b) fetching the first enum for enum classes, c) creating a default * instance for interfaces and abstract classes using ReflectUtil or d) calling a default * constructor. * * @param node the node for which to find a default value * @return an instance of the appropriate type * @throws EvaluationException if an instance cannot be created */ @SuppressWarnings("unchecked") private Object getDefaultValue(NodeEvaluation node) throws EvaluationException { try { Class clazz = convertToClass(node.getValueType(), node); if (clazz.isArray()) { return Array.newInstance(clazz.getComponentType(), 0); } else if (clazz.isEnum()) { return clazz.getEnumConstants()[0]; } else { return StripesFilter.getConfiguration().getObjectFactory().newInstance(clazz); } } catch (Exception e) { throw new EvaluationException("Encountered an exception while trying to create " + " a default instance for property '" + node.getNode().getStringValue() + "' in " + "expression '" + this.expression.getSource() + "'.", e); } } /** *

Sets the value of this expression to "null" for the bean. In reality this is not always * null, but the logical interpretation of "null" for a given type. For primitives the * value is set to be the default value for the primitive type as used by the JVM when * initializing instance variables. For Map entries the key is removed from the Map instead * of leaving the key present with a null value.

* *

If any intermediate properties in the expression are null this method will return * immediately. The sole purpose of this method is to blank out a value if one is present. * Therefore if no value is present, nothing will be changed.

* * @throws EvaluationException if any exceptions are thrown during the process of nulling out */ @SuppressWarnings("unchecked") public void setToNull() throws EvaluationException { Object nodeValue = this.bean; for (NodeEvaluation node = this.root; node != this.leaf && nodeValue != null; node = node.getNext()) { nodeValue = node.getType().getPropertyAccessor().getValue(node, nodeValue); } if (nodeValue != null) { Class leafType = convertToClass(this.leaf.getValueType(), this.leaf); if (Map.class.isAssignableFrom(leafType) || Collection.class.isAssignableFrom(leafType)) { nodeValue = this.leaf.getType().getPropertyAccessor().getValue(this.leaf, nodeValue); if (nodeValue != null && Map.class.isAssignableFrom(leafType)) { ((Map) nodeValue).clear(); } else if (nodeValue != null && Collection.class.isAssignableFrom(leafType)) { ((Collection) nodeValue).clear(); } } else { try { Object nvl = ReflectUtil.getDefaultValue(leafType); this.leaf.getType().getPropertyAccessor().setValue(this.leaf, nodeValue, nvl); } catch (RuntimeException re) { throw re; } catch (Exception e) { throw new EvaluationException("Could not set a null value for property '" + this.expression + "' on bean of type " + this.bean.getClass().getName(), e); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy