001: /*
002: * The contents of this file are subject to the terms
003: * of the Common Development and Distribution License
004: * (the "License"). You may not use this file except
005: * in compliance with the License.
006: *
007: * You can obtain a copy of the license at
008: * glassfish/bootstrap/legal/CDDLv1.0.txt or
009: * https://glassfish.dev.java.net/public/CDDLv1.0.html.
010: * See the License for the specific language governing
011: * permissions and limitations under the License.
012: *
013: * When distributing Covered Code, include this CDDL
014: * HEADER in each file and include the License file at
015: * glassfish/bootstrap/legal/CDDLv1.0.txt. If applicable,
016: * add the following below this CDDL HEADER, with the
017: * fields enclosed by brackets "[]" replaced with your
018: * own identifying information: Portions Copyright [yyyy]
019: * [name of copyright owner]
020: *
021: * Copyright 2005 Sun Microsystems, Inc. All rights reserved.
022: */
023:
024: package javax.el;
025:
026: import java.lang.reflect.InvocationTargetException;
027: import java.lang.reflect.Method;
028: import java.lang.reflect.Modifier;
029: import java.lang.ref.SoftReference;
030: import java.beans.FeatureDescriptor;
031: import java.beans.BeanInfo;
032: import java.beans.Introspector;
033: import java.beans.PropertyDescriptor;
034: import java.beans.IntrospectionException;
035: import java.util.Iterator;
036: import java.util.ArrayList;
037: import java.util.Map;
038: import java.util.HashMap;
039: import java.util.concurrent.ConcurrentHashMap;
040:
041: /**
042: * Defines property resolution behavior on objects using the JavaBeans
043: * component architecture.
044: *
045: * <p>This resolver handles base objects of any type, as long as the
046: * base is not <code>null</code>. It accepts any object as a property, and
047: * coerces it to a string. That string is then used to find a JavaBeans
048: * compliant property on the base object. The value is accessed using
049: * JavaBeans getters and setters.</p>
050: *
051: * <p>This resolver can be constructed in read-only mode, which means that
052: * {@link #isReadOnly} will always return <code>true</code> and
053: * {@link #setValue} will always throw
054: * <code>PropertyNotWritableException</code>.</p>
055: *
056: * <p><code>ELResolver</code>s are combined together using
057: * {@link CompositeELResolver}s, to define rich semantics for evaluating
058: * an expression. See the javadocs for {@link ELResolver} for details.</p>
059: *
060: * <p>Because this resolver handles base objects of any type, it should
061: * be placed near the end of a composite resolver. Otherwise, it will
062: * claim to have resolved a property before any resolvers that come after
063: * it get a chance to test if they can do so as well.</p>
064: *
065: * @see CompositeELResolver
066: * @see ELResolver
067: * @since JSP 2.1
068: */
069: public class BeanELResolver extends ELResolver {
070:
071: private boolean isReadOnly;
072:
073: private static final int CACHE_SIZE = 1024;
074: private static final ConcurrentHashMap<Class, BeanProperties> properties = new ConcurrentHashMap<Class, BeanProperties>(
075: CACHE_SIZE);
076:
077: /*
078: * Defines a property for a bean.
079: */
080: protected final static class BeanProperty {
081:
082: private Method readMethod;
083: private Method writeMethod;
084: private PropertyDescriptor descriptor;
085:
086: public BeanProperty(Class<?> baseClass,
087: PropertyDescriptor descriptor) {
088: this .descriptor = descriptor;
089: readMethod = getMethod(baseClass, descriptor
090: .getReadMethod());
091: writeMethod = getMethod(baseClass, descriptor
092: .getWriteMethod());
093: }
094:
095: public Class getPropertyType() {
096: return descriptor.getPropertyType();
097: }
098:
099: public boolean isReadOnly() {
100: return getWriteMethod() == null;
101: }
102:
103: public Method getReadMethod() {
104: return readMethod;
105: }
106:
107: public Method getWriteMethod() {
108: return writeMethod;
109: }
110: }
111:
112: /*
113: * Defines the properties for a bean.
114: */
115: protected final static class BeanProperties {
116:
117: private final Map<String, BeanProperty> propertyMap = new HashMap<String, BeanProperty>();
118:
119: public BeanProperties(Class<?> baseClass) {
120: PropertyDescriptor[] descriptors;
121: try {
122: BeanInfo info = Introspector.getBeanInfo(baseClass);
123: descriptors = info.getPropertyDescriptors();
124: } catch (IntrospectionException ie) {
125: throw new ELException(ie);
126: }
127: for (PropertyDescriptor pd : descriptors) {
128: propertyMap.put(pd.getName(), new BeanProperty(
129: baseClass, pd));
130: }
131: }
132:
133: public BeanProperty getBeanProperty(String property) {
134: return propertyMap.get(property);
135: }
136: }
137:
138: /**
139: * Creates a new read/write <code>BeanELResolver</code>.
140: */
141: public BeanELResolver() {
142: this .isReadOnly = false;
143: }
144:
145: /**
146: * Creates a new <code>BeanELResolver</code> whose read-only status is
147: * determined by the given parameter.
148: *
149: * @param isReadOnly <code>true</code> if this resolver cannot modify
150: * beans; <code>false</code> otherwise.
151: */
152: public BeanELResolver(boolean isReadOnly) {
153: this .isReadOnly = isReadOnly;
154: }
155:
156: /**
157: * If the base object is not <code>null</code>, returns the most
158: * general acceptable type that can be set on this bean property.
159: *
160: * <p>If the base is not <code>null</code>, the
161: * <code>propertyResolved</code> property of the <code>ELContext</code>
162: * object must be set to <code>true</code> by this resolver, before
163: * returning. If this property is not <code>true</code> after this
164: * method is called, the caller should ignore the return value.</p>
165: *
166: * <p>The provided property will first be coerced to a <code>String</code>.
167: * If there is a <code>BeanInfoProperty</code> for this property and
168: * there were no errors retrieving it, the <code>propertyType</code> of
169: * the <code>propertyDescriptor</code> is returned. Otherwise, a
170: * <code>PropertyNotFoundException</code> is thrown.</p>
171: *
172: * @param context The context of this evaluation.
173: * @param base The bean to analyze.
174: * @param property The name of the property to analyze. Will be coerced to
175: * a <code>String</code>.
176: * @return If the <code>propertyResolved</code> property of
177: * <code>ELContext</code> was set to <code>true</code>, then
178: * the most general acceptable type; otherwise undefined.
179: * @throws NullPointerException if context is <code>null</code>
180: * @throws PropertyNotFoundException if <code>base</code> is not
181: * <code>null</code> and the specified property does not exist
182: * or is not readable.
183: * @throws ELException if an exception was thrown while performing
184: * the property or variable resolution. The thrown exception
185: * must be included as the cause property of this exception, if
186: * available.
187: */
188: public Class<?> getType(ELContext context, Object base,
189: Object property) {
190:
191: if (context == null) {
192: throw new NullPointerException();
193: }
194:
195: if (base == null || property == null) {
196: return null;
197: }
198:
199: BeanProperty bp = getBeanProperty(context, base, property);
200: context.setPropertyResolved(true);
201: return bp.getPropertyType();
202: }
203:
204: /**
205: * If the base object is not <code>null</code>, returns the current
206: * value of the given property on this bean.
207: *
208: * <p>If the base is not <code>null</code>, the
209: * <code>propertyResolved</code> property of the <code>ELContext</code>
210: * object must be set to <code>true</code> by this resolver, before
211: * returning. If this property is not <code>true</code> after this
212: * method is called, the caller should ignore the return value.</p>
213: *
214: * <p>The provided property name will first be coerced to a
215: * <code>String</code>. If the property is a readable property of the
216: * base object, as per the JavaBeans specification, then return the
217: * result of the getter call. If the getter throws an exception,
218: * it is propagated to the caller. If the property is not found or is
219: * not readable, a <code>PropertyNotFoundException</code> is thrown.</p>
220: *
221: * @param context The context of this evaluation.
222: * @param base The bean on which to get the property.
223: * @param property The name of the property to get. Will be coerced to
224: * a <code>String</code>.
225: * @return If the <code>propertyResolved</code> property of
226: * <code>ELContext</code> was set to <code>true</code>, then
227: * the value of the given property. Otherwise, undefined.
228: * @throws NullPointerException if context is <code>null</code>.
229: * @throws PropertyNotFoundException if <code>base</code> is not
230: * <code>null</code> and the specified property does not exist
231: * or is not readable.
232: * @throws ELException if an exception was thrown while performing
233: * the property or variable resolution. The thrown exception
234: * must be included as the cause property of this exception, if
235: * available.
236: */
237: public Object getValue(ELContext context, Object base,
238: Object property) {
239:
240: if (context == null) {
241: throw new NullPointerException();
242: }
243:
244: if (base == null || property == null) {
245: return null;
246: }
247:
248: BeanProperty bp = getBeanProperty(context, base, property);
249: Method method = bp.getReadMethod();
250: if (method == null) {
251: throw new PropertyNotFoundException(ELUtil
252: .getExceptionMessageString(context,
253: "propertyNotReadable", new Object[] {
254: base.getClass().getName(),
255: property.toString() }));
256: }
257:
258: Object value;
259: try {
260: value = method.invoke(base, new Object[0]);
261: context.setPropertyResolved(true);
262: } catch (ELException ex) {
263: throw ex;
264: } catch (InvocationTargetException ite) {
265: throw new ELException(ite.getCause());
266: } catch (Exception ex) {
267: throw new ELException(ex);
268: }
269: return value;
270: }
271:
272: /**
273: * If the base object is not <code>null</code>, attempts to set the
274: * value of the given property on this bean.
275: *
276: * <p>If the base is not <code>null</code>, the
277: * <code>propertyResolved</code> property of the <code>ELContext</code>
278: * object must be set to <code>true</code> by this resolver, before
279: * returning. If this property is not <code>true</code> after this
280: * method is called, the caller can safely assume no value was set.</p>
281: *
282: * <p>If this resolver was constructed in read-only mode, this method will
283: * always throw <code>PropertyNotWritableException</code>.</p>
284: *
285: * <p>The provided property name will first be coerced to a
286: * <code>String</code>. If property is a writable property of
287: * <code>base</code> (as per the JavaBeans Specification), the setter
288: * method is called (passing <code>value</code>). If the property exists
289: * but does not have a setter, then a
290: * <code>PropertyNotFoundException</code> is thrown. If the property
291: * does not exist, a <code>PropertyNotFoundException</code> is thrown.</p>
292: *
293: * @param context The context of this evaluation.
294: * @param base The bean on which to set the property.
295: * @param property The name of the property to set. Will be coerced to
296: * a <code>String</code>.
297: * @param val The value to be associated with the specified key.
298: * @throws NullPointerException if context is <code>null</code>.
299: * @throws PropertyNotFoundException if <code>base</code> is not
300: * <code>null</code> and the specified property does not exist.
301: * @throws PropertyNotWritableException if this resolver was constructed
302: * in read-only mode, or if there is no setter for the property.
303: * @throws ELException if an exception was thrown while performing
304: * the property or variable resolution. The thrown exception
305: * must be included as the cause property of this exception, if
306: * available.
307: */
308: public void setValue(ELContext context, Object base,
309: Object property, Object val) {
310:
311: if (context == null) {
312: throw new NullPointerException();
313: }
314:
315: if (base == null || property == null) {
316: return;
317: }
318:
319: if (isReadOnly) {
320: throw new PropertyNotWritableException(ELUtil
321: .getExceptionMessageString(context,
322: "resolverNotwritable", new Object[] { base
323: .getClass().getName() }));
324: }
325:
326: BeanProperty bp = getBeanProperty(context, base, property);
327: Method method = bp.getWriteMethod();
328: if (method == null) {
329: throw new PropertyNotWritableException(ELUtil
330: .getExceptionMessageString(context,
331: "propertyNotWritable", new Object[] {
332: base.getClass().getName(),
333: property.toString() }));
334: }
335:
336: try {
337: method.invoke(base, new Object[] { val });
338: context.setPropertyResolved(true);
339: } catch (ELException ex) {
340: throw ex;
341: } catch (InvocationTargetException ite) {
342: throw new ELException(ite.getCause());
343: } catch (Exception ex) {
344: if (null == val) {
345: val = "null";
346: }
347: String message = ELUtil.getExceptionMessageString(context,
348: "setPropertyFailed", new Object[] {
349: property.toString(),
350: base.getClass().getName(), val });
351: throw new ELException(message, ex);
352: }
353: }
354:
355: /**
356: * If the base object is not <code>null</code>, returns whether a call
357: * to {@link #setValue} will always fail.
358: *
359: * <p>If the base is not <code>null</code>, the
360: * <code>propertyResolved</code> property of the <code>ELContext</code>
361: * object must be set to <code>true</code> by this resolver, before
362: * returning. If this property is not <code>true</code> after this
363: * method is called, the caller can safely assume no value was set.</p>
364: *
365: * <p>If this resolver was constructed in read-only mode, this method will
366: * always return <code>true</code>.</p>
367: *
368: * <p>The provided property name will first be coerced to a
369: * <code>String</code>. If property is a writable property of
370: * <code>base</code>, <code>false</code> is returned. If the property is
371: * found but is not writable, <code>true</code> is returned. If the
372: * property is not found, a <code>PropertyNotFoundException</code>
373: * is thrown.</p>
374: *
375: * @param context The context of this evaluation.
376: * @param base The bean to analyze.
377: * @param property The name of the property to analyzed. Will be coerced to
378: * a <code>String</code>.
379: * @return If the <code>propertyResolved</code> property of
380: * <code>ELContext</code> was set to <code>true</code>, then
381: * <code>true</code> if calling the <code>setValue</code> method
382: * will always fail or <code>false</code> if it is possible that
383: * such a call may succeed; otherwise undefined.
384: * @throws NullPointerException if context is <code>null</code>
385: * @throws PropertyNotFoundException if <code>base</code> is not
386: * <code>null</code> and the specified property does not exist.
387: * @throws ELException if an exception was thrown while performing
388: * the property or variable resolution. The thrown exception
389: * must be included as the cause property of this exception, if
390: * available.
391: */
392: public boolean isReadOnly(ELContext context, Object base,
393: Object property) {
394:
395: if (context == null) {
396: throw new NullPointerException();
397: }
398:
399: if (base == null || property == null) {
400: return false;
401: }
402:
403: context.setPropertyResolved(true);
404: if (isReadOnly) {
405: return true;
406: }
407:
408: BeanProperty bp = getBeanProperty(context, base, property);
409: return bp.isReadOnly();
410: }
411:
412: /**
413: * If the base object is not <code>null</code>, returns an
414: * <code>Iterator</code> containing the set of JavaBeans properties
415: * available on the given object. Otherwise, returns <code>null</code>.
416: *
417: * <p>The <code>Iterator</code> returned must contain zero or more
418: * instances of {@link java.beans.FeatureDescriptor}. Each info object
419: * contains information about a property in the bean, as obtained by
420: * calling the <code>BeanInfo.getPropertyDescriptors</code> method.
421: * The <code>FeatureDescriptor</code> is initialized using the same
422: * fields as are present in the <code>PropertyDescriptor</code>,
423: * with the additional required named attributes "<code>type</code>" and
424: * "<code>resolvableAtDesignTime</code>" set as follows:
425: * <dl>
426: * <li>{@link ELResolver#TYPE} - The runtime type of the property, from
427: * <code>PropertyDescriptor.getPropertyType()</code>.</li>
428: * <li>{@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - <code>true</code>.</li>
429: * </dl>
430: * </p>
431: *
432: * @param context The context of this evaluation.
433: * @param base The bean to analyze.
434: * @return An <code>Iterator</code> containing zero or more
435: * <code>FeatureDescriptor</code> objects, each representing a property
436: * on this bean, or <code>null</code> if the <code>base</code>
437: * object is <code>null</code>.
438: */
439: public Iterator<FeatureDescriptor> getFeatureDescriptors(
440: ELContext context, Object base) {
441: if (base == null) {
442: return null;
443: }
444:
445: BeanInfo info = null;
446: try {
447: info = Introspector.getBeanInfo(base.getClass());
448: } catch (Exception ex) {
449: }
450: if (info == null) {
451: return null;
452: }
453: ArrayList<FeatureDescriptor> list = new ArrayList<FeatureDescriptor>(
454: info.getPropertyDescriptors().length);
455: for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
456: pd.setValue("type", pd.getPropertyType());
457: pd.setValue("resolvableAtDesignTime", Boolean.TRUE);
458: list.add(pd);
459: }
460: return list.iterator();
461: }
462:
463: /**
464: * If the base object is not <code>null</code>, returns the most
465: * general type that this resolver accepts for the
466: * <code>property</code> argument. Otherwise, returns <code>null</code>.
467: *
468: * <p>Assuming the base is not <code>null</code>, this method will always
469: * return <code>Object.class</code>. This is because any object is
470: * accepted as a key and is coerced into a string.</p>
471: *
472: * @param context The context of this evaluation.
473: * @param base The bean to analyze.
474: * @return <code>null</code> if base is <code>null</code>; otherwise
475: * <code>Object.class</code>.
476: */
477: public Class<?> getCommonPropertyType(ELContext context, Object base) {
478: if (base == null) {
479: return null;
480: }
481:
482: return Object.class;
483: }
484:
485: /*
486: * Get a public method form a public class or interface of a given method.
487: * Note that if a PropertyDescriptor is obtained for a non-public class that
488: * implements a public interface, the read/write methods will be for the
489: * class, and therefore inaccessible. To correct this, a version of the
490: * same method must be found in a superclass or interface.
491: **/
492:
493: static private Method getMethod(Class cl, Method method) {
494:
495: if (method == null) {
496: return null;
497: }
498:
499: if (Modifier.isPublic(cl.getModifiers())) {
500: return method;
501: }
502: Class[] interfaces = cl.getInterfaces();
503: for (int i = 0; i < interfaces.length; i++) {
504: Class c = interfaces[i];
505: Method m = null;
506: try {
507: m = c.getMethod(method.getName(), method
508: .getParameterTypes());
509: c = m.getDeclaringClass();
510: if ((m = getMethod(c, m)) != null)
511: return m;
512: } catch (NoSuchMethodException ex) {
513: }
514: }
515: Class c = cl.getSuperclass();
516: if (c != null) {
517: Method m = null;
518: try {
519: m = c.getMethod(method.getName(), method
520: .getParameterTypes());
521: c = m.getDeclaringClass();
522: if ((m = getMethod(c, m)) != null)
523: return m;
524: } catch (NoSuchMethodException ex) {
525: }
526: }
527: return null;
528: }
529:
530: private BeanProperty getBeanProperty(ELContext context,
531: Object base, Object prop) {
532:
533: String property = prop.toString();
534: Class baseClass = base.getClass();
535: BeanProperties bps = properties.get(baseClass);
536: if (bps == null) {
537: bps = new BeanProperties(baseClass);
538: properties.putIfAbsent(baseClass, bps);
539: }
540: BeanProperty bp = bps.getBeanProperty(property);
541: if (bp == null) {
542: throw new PropertyNotFoundException(ELUtil
543: .getExceptionMessageString(context,
544: "propertyNotFound", new Object[] {
545: baseClass.getName(), property }));
546: }
547: return bp;
548: }
549:
550: private void removeFromMap(Map<Class, BeanProperties> map,
551: ClassLoader classloader) {
552: Iterator<Class> iter = map.keySet().iterator();
553: while (iter.hasNext()) {
554: Class mbeanClass = iter.next();
555: if (classloader.equals(mbeanClass.getClassLoader())) {
556: iter.remove();
557: }
558: }
559:
560: }
561:
562: /*
563: * This method is not part of the API, though it can be used (reflectively)
564: * by clients of this class to remove entries from the cache when the beans
565: * are being unloaded.
566: *
567: * A note about why WeakHashMap is not used. Measurements has shown
568: * that ConcurrentHashMap is much more scalable than synchronized
569: * WeakHashMap. A manual purge seems to be a good compromise.
570: *
571: * @param classloader The classLoader used to load the beans.
572: */
573: private void purgeBeanClasses(ClassLoader classloader) {
574: removeFromMap(properties, classloader);
575: }
576: }
|