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

org.camunda.bpm.engine.impl.javax.el.BeanELResolver Maven / Gradle / Ivy

There is a newer version: 7.22.0-alpha1
Show newest version
/*
 * Based on JUEL 2.2.1 code, 2006-2009 Odysseus Software GmbH
 *
 * 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.camunda.bpm.engine.impl.javax.el;

import java.beans.FeatureDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Defines property resolution behavior on objects using the JavaBeans component architecture. This
 * resolver handles base objects of any type, as long as the base is not null. It accepts any object
 * as a property, and coerces it to a string. That string is then used to find a JavaBeans compliant
 * property on the base object. The value is accessed using JavaBeans getters and setters. This
 * resolver can be constructed in read-only mode, which means that isReadOnly will always return
 * true and {@link #setValue(ELContext, Object, Object, Object)} will always throw
 * PropertyNotWritableException. ELResolvers are combined together using {@link CompositeELResolver}
 * s, to define rich semantics for evaluating an expression. See the javadocs for {@link ELResolver}
 * for details. Because this resolver handles base objects of any type, it should be placed near the
 * end of a composite resolver. Otherwise, it will claim to have resolved a property before any
 * resolvers that come after it get a chance to test if they can do so as well.
 * 
 * @see CompositeELResolver
 * @see ELResolver
 */
public class BeanELResolver extends ELResolver {
	protected static final class BeanProperties {
		private final Map map = new HashMap();

		public BeanProperties(Class baseClass) {
			PropertyDescriptor[] descriptors;
			try {
				descriptors = Introspector.getBeanInfo(baseClass).getPropertyDescriptors();
			} catch (IntrospectionException e) {
				throw new ELException(e);
			}
			for (PropertyDescriptor descriptor : descriptors) {
				map.put(descriptor.getName(), new BeanProperty(descriptor));
			}
		}

		public BeanProperty getBeanProperty(String property) {
			return map.get(property);
		}
	}

	protected static final class BeanProperty {
		private final PropertyDescriptor descriptor;

		public BeanProperty(PropertyDescriptor descriptor) {
			this.descriptor = descriptor;
		}

		public Class getPropertyType() {
			return descriptor.getPropertyType();
		}

		public Method getReadMethod() {
			return findAccessibleMethod(descriptor.getReadMethod());
		}

		public Method getWriteMethod() {
			return findAccessibleMethod(descriptor.getWriteMethod());
		}

		public boolean isReadOnly() {
			return findAccessibleMethod(descriptor.getWriteMethod()) == null;
		}
	}

	private static Method findAccessibleMethod(Method method) {
		if (method == null || method.isAccessible()) {
			return method;
		}
		try {
			method.setAccessible(true);
		} catch (SecurityException e) {
			for (Class cls : method.getDeclaringClass().getInterfaces()) {
				Method mth = null;
				try {
					mth = cls.getMethod(method.getName(), method.getParameterTypes());
					mth = findAccessibleMethod(mth);
					if (mth != null) {
						return mth;
					}
				} catch (NoSuchMethodException ignore) {
					// do nothing
				}
			}
			Class cls = method.getDeclaringClass().getSuperclass();
			if (cls != null) {
				Method mth = null;
				try {
					mth = cls.getMethod(method.getName(), method.getParameterTypes());
					mth = findAccessibleMethod(mth);
					if (mth != null) {
						return mth;
					}
				} catch (NoSuchMethodException ignore) {
					// do nothing
				}
			}
			return null;
		}
		return method;
	}

	private final boolean readOnly;
	private final ConcurrentHashMap, BeanProperties> cache;
	
	private ExpressionFactory defaultFactory;

	/**
	 * Creates a new read/write BeanELResolver.
	 */
	public BeanELResolver() {
		this(false);
	}

	/**
	 * Creates a new BeanELResolver whose read-only status is determined by the given parameter.
	 */
	public BeanELResolver(boolean readOnly) {
		this.readOnly = readOnly;
		this.cache = new ConcurrentHashMap, BeanProperties>();
	}

	/**
	 * If the base object is not null, returns the most general type that this resolver accepts for
	 * the property argument. Otherwise, returns null. Assuming the base is not null, this method
	 * will always return Object.class. This is because any object is accepted as a key and is
	 * coerced into a string.
	 * 
	 * @param context
	 *            The context of this evaluation.
	 * @param base
	 *            The bean to analyze.
	 * @return null if base is null; otherwise Object.class.
	 */
	@Override
	public Class getCommonPropertyType(ELContext context, Object base) {
		return isResolvable(base) ? Object.class : null;
	}

	/**
	 * If the base object is not null, returns an Iterator containing the set of JavaBeans
	 * properties available on the given object. Otherwise, returns null. The Iterator returned must
	 * contain zero or more instances of java.beans.FeatureDescriptor. Each info object contains
	 * information about a property in the bean, as obtained by calling the
	 * BeanInfo.getPropertyDescriptors method. The FeatureDescriptor is initialized using the same
	 * fields as are present in the PropertyDescriptor, with the additional required named
	 * attributes "type" and "resolvableAtDesignTime" set as follows:
	 * 
    *
  • {@link ELResolver#TYPE} - The runtime type of the property, from * PropertyDescriptor.getPropertyType().
  • *
  • {@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - true.
  • *
* * @param context * The context of this evaluation. * @param base * The bean to analyze. * @return An Iterator containing zero or more FeatureDescriptor objects, each representing a * property on this bean, or null if the base object is null. */ @Override public Iterator getFeatureDescriptors(ELContext context, Object base) { if (isResolvable(base)) { final PropertyDescriptor[] properties; try { properties = Introspector.getBeanInfo(base.getClass()).getPropertyDescriptors(); } catch (IntrospectionException e) { return Collections. emptyList().iterator(); } return new Iterator() { int next = 0; public boolean hasNext() { return properties != null && next < properties.length; } public FeatureDescriptor next() { PropertyDescriptor property = properties[next++]; FeatureDescriptor feature = new FeatureDescriptor(); feature.setDisplayName(property.getDisplayName()); feature.setName(property.getName()); feature.setShortDescription(property.getShortDescription()); feature.setExpert(property.isExpert()); feature.setHidden(property.isHidden()); feature.setPreferred(property.isPreferred()); feature.setValue(TYPE, property.getPropertyType()); feature.setValue(RESOLVABLE_AT_DESIGN_TIME, true); return feature; } public void remove() { throw new UnsupportedOperationException("cannot remove"); } }; } return null; } /** * If the base object is not null, returns the most general acceptable type that can be set on * this bean property. If the base is not null, the propertyResolved property of the ELContext * object must be set to true by this resolver, before returning. If this property is not true * after this method is called, the caller should ignore the return value. The provided property * will first be coerced to a String. If there is a BeanInfoProperty for this property and there * were no errors retrieving it, the propertyType of the propertyDescriptor is returned. * Otherwise, a PropertyNotFoundException is thrown. * * @param context * The context of this evaluation. * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @return If the propertyResolved property of ELContext was set to true, then the most general * acceptable type; otherwise undefined. * @throws NullPointerException * if context is null * @throws PropertyNotFoundException * if base is not null and the specified property does not exist or is not readable. * @throws ELException * if an exception was thrown while performing the property or variable resolution. * The thrown exception must be included as the cause property of this exception, if * available. */ @Override public Class getType(ELContext context, Object base, Object property) { if (context == null) { throw new NullPointerException(); } Class result = null; if (isResolvable(base)) { result = toBeanProperty(base, property).getPropertyType(); context.setPropertyResolved(true); } return result; } /** * If the base object is not null, returns the current value of the given property on this bean. * If the base is not null, the propertyResolved property of the ELContext object must be set to * true by this resolver, before returning. If this property is not true after this method is * called, the caller should ignore the return value. The provided property name will first be * coerced to a String. If the property is a readable property of the base object, as per the * JavaBeans specification, then return the result of the getter call. If the getter throws an * exception, it is propagated to the caller. If the property is not found or is not readable, a * PropertyNotFoundException is thrown. * * @param context * The context of this evaluation. * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @return If the propertyResolved property of ELContext was set to true, then the value of the * given property. Otherwise, undefined. * @throws NullPointerException * if context is null * @throws PropertyNotFoundException * if base is not null and the specified property does not exist or is not readable. * @throws ELException * if an exception was thrown while performing the property or variable resolution. * The thrown exception must be included as the cause property of this exception, if * available. */ @Override public Object getValue(ELContext context, Object base, Object property) { if (context == null) { throw new NullPointerException(); } Object result = null; if (isResolvable(base)) { Method method = toBeanProperty(base, property).getReadMethod(); if (method == null) { throw new PropertyNotFoundException("Cannot read property " + property); } try { result = method.invoke(base); } catch (InvocationTargetException e) { throw new ELException(e.getCause()); } catch (Exception e) { throw new ELException(e); } context.setPropertyResolved(true); } return result; } /** * If the base object is not null, returns whether a call to * {@link #setValue(ELContext, Object, Object, Object)} will always fail. If the base is not * null, the propertyResolved property of the ELContext object must be set to true by this * resolver, before returning. If this property is not true after this method is called, the * caller can safely assume no value was set. * * @param context * The context of this evaluation. * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @return If the propertyResolved property of ELContext was set to true, then true if calling * the setValue method will always fail or false if it is possible that such a call may * succeed; otherwise undefined. * @throws NullPointerException * if context is null * @throws PropertyNotFoundException * if base is not null and the specified property does not exist or is not readable. * @throws ELException * if an exception was thrown while performing the property or variable resolution. * The thrown exception must be included as the cause property of this exception, if * available. */ @Override public boolean isReadOnly(ELContext context, Object base, Object property) { if (context == null) { throw new NullPointerException(); } boolean result = readOnly; if (isResolvable(base)) { result |= toBeanProperty(base, property).isReadOnly(); context.setPropertyResolved(true); } return result; } /** * If the base object is not null, attempts to set the value of the given property on this bean. * If the base is not null, the propertyResolved property of the ELContext object must be set to * true by this resolver, before returning. If this property is not true after this method is * called, the caller can safely assume no value was set. If this resolver was constructed in * read-only mode, this method will always throw PropertyNotWritableException. The provided * property name will first be coerced to a String. If property is a writable property of base * (as per the JavaBeans Specification), the setter method is called (passing value). If the * property exists but does not have a setter, then a PropertyNotFoundException is thrown. If * the property does not exist, a PropertyNotFoundException is thrown. * * @param context * The context of this evaluation. * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @param value * The value to be associated with the specified key. * @throws NullPointerException * if context is null * @throws PropertyNotFoundException * if base is not null and the specified property does not exist or is not readable. * @throws PropertyNotWritableException * if this resolver was constructed in read-only mode, or if there is no setter for * the property * @throws ELException * if an exception was thrown while performing the property or variable resolution. * The thrown exception must be included as the cause property of this exception, if * available. */ @Override public void setValue(ELContext context, Object base, Object property, Object value) { if (context == null) { throw new NullPointerException(); } if (isResolvable(base)) { if (readOnly) { throw new PropertyNotWritableException("resolver is read-only"); } Method method = toBeanProperty(base, property).getWriteMethod(); if (method == null) { throw new PropertyNotWritableException("Cannot write property: " + property); } try { method.invoke(base, value); } catch (InvocationTargetException e) { throw new ELException("Cannot write property: " + property, e.getCause()); } catch (IllegalAccessException e) { throw new PropertyNotWritableException("Cannot write property: " + property, e); } context.setPropertyResolved(true); } } /** * If the base object is not null, invoke the method, with the given parameters on * this bean. The return value from the method is returned. * *

* If the base is not null, the propertyResolved property of the * ELContext object must be set to true by this resolver, before * returning. If this property is not true after this method is called, the caller * should ignore the return value. *

* *

* The provided method object will first be coerced to a String. The methods in the * bean is then examined and an attempt will be made to select one for invocation. If no * suitable can be found, a MethodNotFoundException is thrown. * * If the given paramTypes is not null, select the method with the given name and * parameter types. * * Else select the method with the given name that has the same number of parameters. If there * are more than one such method, the method selection process is undefined. * * Else select the method with the given name that takes a variable number of arguments. * * Note the resolution for overloaded methods will likely be clarified in a future version of * the spec. * * The provided parameters are coerced to the corresponding parameter types of the method, and * the method is then invoked. * * @param context * The context of this evaluation. * @param base * The bean on which to invoke the method * @param method * The simple name of the method to invoke. Will be coerced to a String. * If method is "<init>"or "<clinit>" a MethodNotFoundException is * thrown. * @param paramTypes * An array of Class objects identifying the method's formal parameter types, in * declared order. Use an empty array if the method has no parameters. Can be * null, in which case the method's formal parameter types are assumed * to be unknown. * @param params * The parameters to pass to the method, or null if no parameters. * @return The result of the method invocation (null if the method has a * void return type). * @throws MethodNotFoundException * if no suitable method can be found. * @throws ELException * if an exception was thrown while performing (base, method) resolution. The thrown * exception must be included as the cause property of this exception, if available. * If the exception thrown is an InvocationTargetException, extract its * cause and pass it to the ELException constructor. * @since 2.2 */ @Override public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { if (context == null) { throw new NullPointerException(); } Object result = null; if (isResolvable(base)) { if (params == null) { params = new Object[0]; } String name = method.toString(); Method target = findMethod(base, name, paramTypes, params.length); if (target == null) { throw new MethodNotFoundException("Cannot find method " + name + " with " + params.length + " parameters in " + base.getClass()); } try { result = target.invoke(base, coerceParams(getExpressionFactory(context), target, params)); } catch (InvocationTargetException e) { throw new ELException(e.getCause()); } catch (IllegalAccessException e) { throw new ELException(e); } context.setPropertyResolved(true); } return result; } private Method findMethod(Object base, String name, Class[] types, int paramCount) { if (types != null) { try { return findAccessibleMethod(base.getClass().getMethod(name, types)); } catch (NoSuchMethodException e) { return null; } } Method varArgsMethod = null; for (Method method : base.getClass().getMethods()) { if (method.getName().equals(name)) { int formalParamCount = method.getParameterTypes().length; if (method.isVarArgs() && paramCount >= formalParamCount - 1) { varArgsMethod = method; } else if (paramCount == formalParamCount) { return findAccessibleMethod(method); } } } return varArgsMethod == null ? null : findAccessibleMethod(varArgsMethod); } /** * Lookup an expression factory used to coerce method parameters in context under key * "javax.el.ExpressionFactory". * If no expression factory can be found under that key, use a default instance created with * {@link ExpressionFactory#newInstance()}. * @param context * The context of this evaluation. * @return expression factory instance */ private ExpressionFactory getExpressionFactory(ELContext context) { Object obj = context.getContext(ExpressionFactory.class); if (obj instanceof ExpressionFactory) { return (ExpressionFactory)obj; } if (defaultFactory == null) { defaultFactory = ExpressionFactory.newInstance(); } return defaultFactory; } private Object[] coerceParams(ExpressionFactory factory, Method method, Object[] params) { Class[] types = method.getParameterTypes(); Object[] args = new Object[types.length]; if (method.isVarArgs()) { int varargIndex = types.length - 1; if (params.length < varargIndex) { throw new ELException("Bad argument count"); } for (int i = 0; i < varargIndex; i++) { coerceValue(args, i, factory, params[i], types[i]); } Class varargType = types[varargIndex].getComponentType(); int length = params.length - varargIndex; Object array = null; if (length == 1) { Object source = params[varargIndex]; if (source != null && source.getClass().isArray()) { if (types[varargIndex].isInstance(source)) { // use source array as is array = source; } else { // coerce array elements length = Array.getLength(source); array = Array.newInstance(varargType, length); for (int i = 0; i < length; i++) { coerceValue(array, i, factory, Array.get(source, i), varargType); } } } else { // single element array array = Array.newInstance(varargType, 1); coerceValue(array, 0, factory, source, varargType); } } else { array = Array.newInstance(varargType, length); for (int i = 0; i < length; i++) { coerceValue(array, i, factory, params[varargIndex + i], varargType); } } args[varargIndex] = array; } else { if (params.length != args.length) { throw new ELException("Bad argument count"); } for (int i = 0; i < args.length; i++) { coerceValue(args, i, factory, params[i], types[i]); } } return args; } private void coerceValue(Object array, int index, ExpressionFactory factory, Object value, Class type) { if (value != null || type.isPrimitive()) { Array.set(array, index, factory.coerceToType(value, type)); } } /** * Test whether the given base should be resolved by this ELResolver. * * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @return base != null */ private final boolean isResolvable(Object base) { return base != null; } /** * Lookup BeanProperty for the given (base, property) pair. * * @param base * The bean to analyze. * @param property * The name of the property to analyze. Will be coerced to a String. * @return The BeanProperty representing (base, property). * @throws PropertyNotFoundException * if no BeanProperty can be found. */ private final BeanProperty toBeanProperty(Object base, Object property) { BeanProperties beanProperties = cache.get(base.getClass()); if (beanProperties == null) { BeanProperties newBeanProperties = new BeanProperties(base.getClass()); beanProperties = cache.putIfAbsent(base.getClass(), newBeanProperties); if (beanProperties == null) { // put succeeded, use new value beanProperties = newBeanProperties; } } BeanProperty beanProperty = property == null ? null : beanProperties.getBeanProperty(property.toString()); if (beanProperty == null) { throw new PropertyNotFoundException("Could not find property " + property + " in " + base.getClass()); } return beanProperty; } /** * This method is not part of the API, though it can be used (reflectively) by clients of this * class to remove entries from the cache when the beans are being unloaded. * * Note: this method is present in the reference implementation, so we're adding it here to ease * migration. * * @param classloader * The classLoader used to load the beans. */ @SuppressWarnings("unused") private final void purgeBeanClasses(ClassLoader loader) { Iterator> classes = cache.keySet().iterator(); while (classes.hasNext()) { if (loader == classes.next().getClassLoader()) { classes.remove(); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy