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

jakarta.el.BeanELResolver Maven / Gradle / Ivy

/*
 * Copyright (c) 1997, 2021 Oracle and/or its affiliates and others.
 * All rights reserved.
 * Copyright 2004 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 jakarta.el;

import static java.lang.Boolean.TRUE;
import static jakarta.el.ELUtil.getExceptionMessageString;

import java.beans.BeanInfo;
import java.beans.FeatureDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
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 or method, and coerces it to a string. * *

* For property resolution, the property string is used to find a JavaBeans compliant property on the base object. The * value is accessed using JavaBeans getters and setters. *

* *

* For method resolution, the method string is the name of the method in the bean. The parameter types can be optionally * specified to identify the method. If the parameter types are not specified, the parameter objects are used in the * method resolution. *

* *

* The JavaBeans specification predates the introduction of default method implementations defined on an interface. In * addition to the JavaBeans specification requirements for looking up property getters, property setters and methods, * this resolver also considers default methods and includes them in the results. *

* *

* This resolver can be constructed in read-only mode, which means that {@link #isReadOnly} will always return * true and {@link #setValue} 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 * * @since Jakarta Server Pages 2.1 */ public class BeanELResolver extends ELResolver { static private class BPSoftReference extends SoftReference { final Class key; BPSoftReference(Class key, BeanProperties beanProperties, ReferenceQueue refQ) { super(beanProperties, refQ); this.key = key; } } static private class SoftConcurrentHashMap extends ConcurrentHashMap, BeanProperties> { private static final long serialVersionUID = -178867497897782229L; private static final int CACHE_INIT_SIZE = 1024; private ConcurrentHashMap, BPSoftReference> map = new ConcurrentHashMap<>(CACHE_INIT_SIZE); private ReferenceQueue refQ = new ReferenceQueue<>(); // Remove map entries that have been placed on the queue by GC. private void cleanup() { BPSoftReference BPRef = null; while ((BPRef = (BPSoftReference) refQ.poll()) != null) { map.remove(BPRef.key); } } @Override public BeanProperties put(Class key, BeanProperties value) { cleanup(); BPSoftReference prev = map.put(key, new BPSoftReference(key, value, refQ)); return prev == null ? null : prev.get(); } @Override public BeanProperties putIfAbsent(Class key, BeanProperties value) { cleanup(); BPSoftReference prev = map.putIfAbsent(key, new BPSoftReference(key, value, refQ)); return prev == null ? null : prev.get(); } @Override public BeanProperties get(Object key) { cleanup(); BPSoftReference BPRef = map.get(key); if (BPRef == null) { return null; } if (BPRef.get() == null) { // value has been garbage collected, remove entry in map map.remove(key); return null; } return BPRef.get(); } } private boolean isReadOnly; private final SoftConcurrentHashMap properties = new SoftConcurrentHashMap(); /* * Defines a property for a bean. */ final static class BeanProperty { private Method readMethod; private Method writeMethod; private PropertyDescriptor descriptor; public BeanProperty(Class baseClass, PropertyDescriptor descriptor) { this.descriptor = descriptor; readMethod = ELUtil.getMethod(baseClass, descriptor.getReadMethod()); writeMethod = ELUtil.getMethod(baseClass, descriptor.getWriteMethod()); } public Class getPropertyType() { return descriptor.getPropertyType(); } public boolean isReadOnly() { return getWriteMethod() == null; } public Method getReadMethod() { return readMethod; } public Method getWriteMethod() { return writeMethod; } } /* * Defines the properties for a bean. */ final static class BeanProperties { private final Map propertyMap = new HashMap<>(); public BeanProperties(Class baseClass) { PropertyDescriptor[] descriptors; try { BeanInfo info = Introspector.getBeanInfo(baseClass); descriptors = info.getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { propertyMap.put(descriptor.getName(), new BeanProperty(baseClass, descriptor)); } /** * Populating from any interfaces solves two distinct problems: * 1. When running under a security manager, classes may be * unaccessible but have accessible interfaces. * 2. It enables default methods to be included. */ populateFromInterfaces(baseClass, baseClass); } catch (IntrospectionException ie) { throw new ELException(ie); } } private void populateFromInterfaces(Class baseClass, Class aClass) throws IntrospectionException { Class interfaces[] = aClass.getInterfaces(); if (interfaces.length > 0) { for (Class ifs : interfaces) { BeanInfo info = Introspector.getBeanInfo(ifs); PropertyDescriptor[] pds = info.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { if (!this.propertyMap.containsKey(pd.getName())) { this.propertyMap.put(pd.getName(), new BeanProperty( baseClass, pd)); } } } } Class superclass = aClass.getSuperclass(); if (superclass != null) { populateFromInterfaces(baseClass, superclass); } } public BeanProperty getBeanProperty(String property) { return propertyMap.get(property); } } /** * Creates a new read/write BeanELResolver. */ public BeanELResolver() { this.isReadOnly = false; } /** * Creates a new BeanELResolver whose read-only status is determined by the given parameter. * * @param isReadOnly true if this resolver cannot modify beans; false otherwise. */ public BeanELResolver(boolean isReadOnly) { this.isReadOnly = isReadOnly; } /** * 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, there were no errors retrieving it and neither the property nor the resolver are read-only, * the propertyType of the propertyDescriptor is returned. If the property is resolved but * either the property or the resolver is read-only then {@code null} will be 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 which must be {@code null} if the either the property or the resolver is * read-only; 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(); } if (base == null || property == null) { return null; } BeanProperty beanProperty = getBeanProperty(context, base, property); context.setPropertyResolved(true); if (isReadOnly || beanProperty.isReadOnly()) { return null; } return beanProperty.getPropertyType(); } /** * 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 on which to get the property. * @param property The name of the property to get. 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(); } if (base == null || property == null) { return null; } Method method = getBeanProperty(context, base, property).getReadMethod(); if (method == null) { throw new PropertyNotFoundException( getExceptionMessageString(context, "propertyNotReadable", new Object[] { base.getClass().getName(), property.toString() })); } Object value; try { value = method.invoke(base, new Object[0]); context.setPropertyResolved(base, property); } catch (ELException ex) { throw ex; } catch (InvocationTargetException ite) { throw new ELException(ite.getCause()); } catch (Exception ex) { throw new ELException(ex); } return value; } /** * 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 on which to set the property. * @param property The name of the property to set. Will be coerced to a String. * @param val 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. * @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 val) { if (context == null) { throw new NullPointerException(); } if (base == null || property == null) { return; } if (isReadOnly) { throw new PropertyNotWritableException(getExceptionMessageString(context, "resolverNotwritable", new Object[] { base.getClass().getName() })); } Method method = getBeanProperty(context, base, property).getWriteMethod(); if (method == null) { throw new PropertyNotWritableException( getExceptionMessageString(context, "propertyNotWritable", new Object[] { base.getClass().getName(), property.toString() })); } try { method.invoke(base, new Object[] { val }); context.setPropertyResolved(base, property); } catch (ELException ex) { throw ex; } catch (InvocationTargetException ite) { throw new ELException(ite.getCause()); } catch (Exception ex) { if (null == val) { val = "null"; } String message = getExceptionMessageString(context, "setPropertyFailed", new Object[] { property.toString(), base.getClass().getName(), val }); throw new ELException(message, ex); } } /** * 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 provide 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 methodName 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 Jakarta Expression Language 2.2 */ @Override public Object invoke(ELContext context, Object base, Object methodName, Class[] paramTypes, Object[] params) { if (base == null || methodName == null) { return null; } Method method = ELUtil.findMethod(base.getClass(), methodName.toString(), paramTypes, params, false); for (Object param : params) { // If the parameters is a LambdaExpression, set the ELContext // for its evaluation if (param instanceof LambdaExpression) { ((LambdaExpression) param).setELContext(context); } } Object ret = ELUtil.invokeMethod(context, method, base, params); context.setPropertyResolved(base, methodName); return ret; } /** * If the base object is not null, returns whether a call to {@link #setValue} 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. *

* *

* If this resolver was constructed in read-only mode, this method will always return true. *

* *

* The provided property name will first be coerced to a String. If property is a writable property of * base, false is returned. If the property is found but is not writable, true is * returned. If the property is not found, 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 analyzed. 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. * @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(); } if (base == null || property == null) { return false; } context.setPropertyResolved(true); if (isReadOnly) { return true; } return getBeanProperty(context, base, property).isReadOnly(); } /** * 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 {@link 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. * * @deprecated This method will be removed without replacement in EL 6.0 */ @Deprecated(forRemoval = true, since = "5.0") @Override public Iterator getFeatureDescriptors(ELContext context, Object base) { if (base == null) { return null; } BeanInfo info = null; try { info = Introspector.getBeanInfo(base.getClass()); } catch (Exception ex) { } if (info == null) { return null; } ArrayList featureDescriptors = new ArrayList<>(info.getPropertyDescriptors().length); for (PropertyDescriptor propertyDescriptor : info.getPropertyDescriptors()) { propertyDescriptor.setValue("type", propertyDescriptor.getPropertyType()); propertyDescriptor.setValue("resolvableAtDesignTime", TRUE); featureDescriptors.add(propertyDescriptor); } return featureDescriptors.iterator(); } /** * 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) { if (base == null) { return null; } return Object.class; } private BeanProperty getBeanProperty(ELContext context, Object base, Object prop) { String property = prop.toString(); Class baseClass = base.getClass(); BeanProperties beanProperties = properties.get(baseClass); if (beanProperties == null) { beanProperties = new BeanProperties(baseClass); properties.put(baseClass, beanProperties); } BeanProperty beanProperty = beanProperties.getBeanProperty(property); if (beanProperty == null) { throw new PropertyNotFoundException(getExceptionMessageString(context, "propertyNotFound", new Object[] { baseClass.getName(), property })); } return beanProperty; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy