1 /*
2 * The contents of this file are subject to the terms
3 * of the Common Development and Distribution License
4 * (the "License"). You may not use this file except
5 * in compliance with the License.
6 *
7 * You can obtain a copy of the license at
8 * glassfish/bootstrap/legal/CDDLv1.0.txt or
9 * https://glassfish.dev.java.net/public/CDDLv1.0.html.
10 * See the License for the specific language governing
11 * permissions and limitations under the License.
12 *
13 * When distributing Covered Code, include this CDDL
14 * HEADER in each file and include the License file at
15 * glassfish/bootstrap/legal/CDDLv1.0.txt. If applicable,
16 * add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your
18 * own identifying information: Portions Copyright [yyyy]
19 * [name of copyright owner]
20 *
21 * Copyright 2005 Sun Microsystems, Inc. All rights reserved.
22 */
23
24 package javax.el;
25
26 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.lang.reflect.Modifier;
29 import java.lang.ref.SoftReference;
30 import java.beans.FeatureDescriptor;
31 import java.beans.BeanInfo;
32 import java.beans.Introspector;
33 import java.beans.PropertyDescriptor;
34 import java.beans.IntrospectionException;
35 import java.util.Iterator;
36 import java.util.ArrayList;
37 import java.util.Map;
38 import java.util.HashMap;
39 import java.util.concurrent.ConcurrentHashMap;
40
41 /**
42 * Defines property resolution behavior on objects using the JavaBeans
43 * component architecture.
44 *
45 * <p>This resolver handles base objects of any type, as long as the
46 * base is not <code>null</code>. It accepts any object as a property, and
47 * coerces it to a string. That string is then used to find a JavaBeans
48 * compliant property on the base object. The value is accessed using
49 * JavaBeans getters and setters.</p>
50 *
51 * <p>This resolver can be constructed in read-only mode, which means that
52 * {@link #isReadOnly} will always return <code>true</code> and
53 * {@link #setValue} will always throw
54 * <code>PropertyNotWritableException</code>.</p>
55 *
56 * <p><code>ELResolver</code>s are combined together using
57 * {@link CompositeELResolver}s, to define rich semantics for evaluating
58 * an expression. See the javadocs for {@link ELResolver} for details.</p>
59 *
60 * <p>Because this resolver handles base objects of any type, it should
61 * be placed near the end of a composite resolver. Otherwise, it will
62 * claim to have resolved a property before any resolvers that come after
63 * it get a chance to test if they can do so as well.</p>
64 *
65 * @see CompositeELResolver
66 * @see ELResolver
67 * @since JSP 2.1
68 */
69 public class BeanELResolver extends ELResolver {
70
71 private boolean isReadOnly;
72
73 private static final int CACHE_SIZE = 1024;
74 private static final ConcurrentHashMap<Class, BeanProperties> properties =
75 new ConcurrentHashMap<Class, BeanProperties>(CACHE_SIZE);
76
77 /*
78 * Defines a property for a bean.
79 */
80 protected final static class BeanProperty {
81
82 private Method readMethod;
83 private Method writeMethod;
84 private PropertyDescriptor descriptor;
85
86 public BeanProperty(Class<?> baseClass,
87 PropertyDescriptor descriptor) {
88 this.descriptor = descriptor;
89 readMethod = getMethod(baseClass, descriptor.getReadMethod());
90 writeMethod = getMethod(baseClass, descriptor.getWriteMethod());
91 }
92
93 public Class getPropertyType() {
94 return descriptor.getPropertyType();
95 }
96
97 public boolean isReadOnly() {
98 return getWriteMethod() == null;
99 }
100
101 public Method getReadMethod() {
102 return readMethod;
103 }
104
105 public Method getWriteMethod() {
106 return writeMethod;
107 }
108 }
109
110 /*
111 * Defines the properties for a bean.
112 */
113 protected final static class BeanProperties {
114
115 private final Map<String, BeanProperty> propertyMap =
116 new HashMap<String, BeanProperty>();
117
118 public BeanProperties(Class<?> baseClass) {
119 PropertyDescriptor[] descriptors;
120 try {
121 BeanInfo info = Introspector.getBeanInfo(baseClass);
122 descriptors = info.getPropertyDescriptors();
123 } catch (IntrospectionException ie) {
124 throw new ELException(ie);
125 }
126 for (PropertyDescriptor pd: descriptors) {
127 propertyMap.put(pd.getName(),
128 new BeanProperty(baseClass, pd));
129 }
130 }
131
132 public BeanProperty getBeanProperty(String property) {
133 return propertyMap.get(property);
134 }
135 }
136
137 /**
138 * Creates a new read/write <code>BeanELResolver</code>.
139 */
140 public BeanELResolver() {
141 this.isReadOnly = false;
142 }
143
144 /**
145 * Creates a new <code>BeanELResolver</code> whose read-only status is
146 * determined by the given parameter.
147 *
148 * @param isReadOnly <code>true</code> if this resolver cannot modify
149 * beans; <code>false</code> otherwise.
150 */
151 public BeanELResolver(boolean isReadOnly) {
152 this.isReadOnly = isReadOnly;
153 }
154
155 /**
156 * If the base object is not <code>null</code>, returns the most
157 * general acceptable type that can be set on this bean property.
158 *
159 * <p>If the base is not <code>null</code>, the
160 * <code>propertyResolved</code> property of the <code>ELContext</code>
161 * object must be set to <code>true</code> by this resolver, before
162 * returning. If this property is not <code>true</code> after this
163 * method is called, the caller should ignore the return value.</p>
164 *
165 * <p>The provided property will first be coerced to a <code>String</code>.
166 * If there is a <code>BeanInfoProperty</code> for this property and
167 * there were no errors retrieving it, the <code>propertyType</code> of
168 * the <code>propertyDescriptor</code> is returned. Otherwise, a
169 * <code>PropertyNotFoundException</code> is thrown.</p>
170 *
171 * @param context The context of this evaluation.
172 * @param base The bean to analyze.
173 * @param property The name of the property to analyze. Will be coerced to
174 * a <code>String</code>.
175 * @return If the <code>propertyResolved</code> property of
176 * <code>ELContext</code> was set to <code>true</code>, then
177 * the most general acceptable type; otherwise undefined.
178 * @throws NullPointerException if context is <code>null</code>
179 * @throws PropertyNotFoundException if <code>base</code> is not
180 * <code>null</code> and the specified property does not exist
181 * or is not readable.
182 * @throws ELException if an exception was thrown while performing
183 * the property or variable resolution. The thrown exception
184 * must be included as the cause property of this exception, if
185 * available.
186 */
187 public Class<?> getType(ELContext context,
188 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,
238 Object base,
239 Object property) {
240
241 if (context == null) {
242 throw new NullPointerException();
243 }
244
245 if (base == null || property == null){
246 return null;
247 }
248
249 BeanProperty bp = getBeanProperty(context, base, property);
250 Method method = bp.getReadMethod();
251 if (method == null) {
252 throw new PropertyNotFoundException(
253 ELUtil.getExceptionMessageString(context,
254 "propertyNotReadable",
255 new Object[] { base.getClass().getName(),
256 property.toString()}));
257 }
258
259 Object value;
260 try {
261 value = method.invoke(base, new Object[0]);
262 context.setPropertyResolved(true);
263 } catch (ELException ex) {
264 throw ex;
265 } catch (InvocationTargetException ite) {
266 throw new ELException(ite.getCause());
267 } catch (Exception ex) {
268 throw new ELException(ex);
269 }
270 return value;
271 }
272
273 /**
274 * If the base object is not <code>null</code>, attempts to set the
275 * value of the given property on this bean.
276 *
277 * <p>If the base is not <code>null</code>, the
278 * <code>propertyResolved</code> property of the <code>ELContext</code>
279 * object must be set to <code>true</code> by this resolver, before
280 * returning. If this property is not <code>true</code> after this
281 * method is called, the caller can safely assume no value was set.</p>
282 *
283 * <p>If this resolver was constructed in read-only mode, this method will
284 * always throw <code>PropertyNotWritableException</code>.</p>
285 *
286 * <p>The provided property name will first be coerced to a
287 * <code>String</code>. If property is a writable property of
288 * <code>base</code> (as per the JavaBeans Specification), the setter
289 * method is called (passing <code>value</code>). If the property exists
290 * but does not have a setter, then a
291 * <code>PropertyNotFoundException</code> is thrown. If the property
292 * does not exist, a <code>PropertyNotFoundException</code> is thrown.</p>
293 *
294 * @param context The context of this evaluation.
295 * @param base The bean on which to set the property.
296 * @param property The name of the property to set. Will be coerced to
297 * a <code>String</code>.
298 * @param val The value to be associated with the specified key.
299 * @throws NullPointerException if context is <code>null</code>.
300 * @throws PropertyNotFoundException if <code>base</code> is not
301 * <code>null</code> and the specified property does not exist.
302 * @throws PropertyNotWritableException if this resolver was constructed
303 * in read-only mode, or if there is no setter for the property.
304 * @throws ELException if an exception was thrown while performing
305 * the property or variable resolution. The thrown exception
306 * must be included as the cause property of this exception, if
307 * available.
308 */
309 public void setValue(ELContext context,
310 Object base,
311 Object property,
312 Object val) {
313
314 if (context == null) {
315 throw new NullPointerException();
316 }
317
318 if (base == null || property == null){
319 return;
320 }
321
322 if (isReadOnly) {
323 throw new PropertyNotWritableException(
324 ELUtil.getExceptionMessageString(context,
325 "resolverNotwritable",
326 new Object[] { base.getClass().getName() }));
327 }
328
329 BeanProperty bp = getBeanProperty(context, base, property);
330 Method method = bp.getWriteMethod();
331 if (method == null) {
332 throw new PropertyNotWritableException(
333 ELUtil.getExceptionMessageString(context,
334 "propertyNotWritable",
335 new Object[] { base.getClass().getName(),
336 property.toString()}));
337 }
338
339 try {
340 method.invoke(base, new Object[] {val});
341 context.setPropertyResolved(true);
342 } catch (ELException ex) {
343 throw ex;
344 } catch (InvocationTargetException ite) {
345 throw new ELException(ite.getCause());
346 } catch (Exception ex) {
347 if (null == val) {
348 val = "null";
349 }
350 String message = ELUtil.getExceptionMessageString(context,
351 "setPropertyFailed",
352 new Object[] { property.toString(),
353 base.getClass().getName(), val });
354 throw new ELException(message, ex);
355 }
356 }
357
358 /**
359 * If the base object is not <code>null</code>, returns whether a call
360 * to {@link #setValue} will always fail.
361 *
362 * <p>If the base is not <code>null</code>, the
363 * <code>propertyResolved</code> property of the <code>ELContext</code>
364 * object must be set to <code>true</code> by this resolver, before
365 * returning. If this property is not <code>true</code> after this
366 * method is called, the caller can safely assume no value was set.</p>
367 *
368 * <p>If this resolver was constructed in read-only mode, this method will
369 * always return <code>true</code>.</p>
370 *
371 * <p>The provided property name will first be coerced to a
372 * <code>String</code>. If property is a writable property of
373 * <code>base</code>, <code>false</code> is returned. If the property is
374 * found but is not writable, <code>true</code> is returned. If the
375 * property is not found, a <code>PropertyNotFoundException</code>
376 * is thrown.</p>
377 *
378 * @param context The context of this evaluation.
379 * @param base The bean to analyze.
380 * @param property The name of the property to analyzed. Will be coerced to
381 * a <code>String</code>.
382 * @return If the <code>propertyResolved</code> property of
383 * <code>ELContext</code> was set to <code>true</code>, then
384 * <code>true</code> if calling the <code>setValue</code> method
385 * will always fail or <code>false</code> if it is possible that
386 * such a call may succeed; otherwise undefined.
387 * @throws NullPointerException if context is <code>null</code>
388 * @throws PropertyNotFoundException if <code>base</code> is not
389 * <code>null</code> and the specified property does not exist.
390 * @throws ELException if an exception was thrown while performing
391 * the property or variable resolution. The thrown exception
392 * must be included as the cause property of this exception, if
393 * available.
394 */
395 public boolean isReadOnly(ELContext context,
396 Object base,
397 Object property) {
398
399 if (context == null) {
400 throw new NullPointerException();
401 }
402
403 if (base == null || property == null){
404 return false;
405 }
406
407 context.setPropertyResolved(true);
408 if (isReadOnly) {
409 return true;
410 }
411
412 BeanProperty bp = getBeanProperty(context, base, property);
413 return bp.isReadOnly();
414 }
415
416 /**
417 * If the base object is not <code>null</code>, returns an
418 * <code>Iterator</code> containing the set of JavaBeans properties
419 * available on the given object. Otherwise, returns <code>null</code>.
420 *
421 * <p>The <code>Iterator</code> returned must contain zero or more
422 * instances of {@link java.beans.FeatureDescriptor}. Each info object
423 * contains information about a property in the bean, as obtained by
424 * calling the <code>BeanInfo.getPropertyDescriptors</code> method.
425 * The <code>FeatureDescriptor</code> is initialized using the same
426 * fields as are present in the <code>PropertyDescriptor</code>,
427 * with the additional required named attributes "<code>type</code>" and
428 * "<code>resolvableAtDesignTime</code>" set as follows:
429 * <dl>
430 * <li>{@link ELResolver#TYPE} - The runtime type of the property, from
431 * <code>PropertyDescriptor.getPropertyType()</code>.</li>
432 * <li>{@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - <code>true</code>.</li>
433 * </dl>
434 * </p>
435 *
436 * @param context The context of this evaluation.
437 * @param base The bean to analyze.
438 * @return An <code>Iterator</code> containing zero or more
439 * <code>FeatureDescriptor</code> objects, each representing a property
440 * on this bean, or <code>null</code> if the <code>base</code>
441 * object is <code>null</code>.
442 */
443 public Iterator<FeatureDescriptor> getFeatureDescriptors(
444 ELContext context,
445 Object base) {
446 if (base == null){
447 return null;
448 }
449
450 BeanInfo info = null;
451 try {
452 info = Introspector.getBeanInfo(base.getClass());
453 } catch (Exception ex) {
454 }
455 if (info == null) {
456 return null;
457 }
458 ArrayList<FeatureDescriptor> list = new ArrayList<FeatureDescriptor>(
459 info.getPropertyDescriptors().length);
460 for (PropertyDescriptor pd: info.getPropertyDescriptors()) {
461 pd.setValue("type", pd.getPropertyType());
462 pd.setValue("resolvableAtDesignTime", Boolean.TRUE);
463 list.add(pd);
464 }
465 return list.iterator();
466 }
467
468 /**
469 * If the base object is not <code>null</code>, returns the most
470 * general type that this resolver accepts for the
471 * <code>property</code> argument. Otherwise, returns <code>null</code>.
472 *
473 * <p>Assuming the base is not <code>null</code>, this method will always
474 * return <code>Object.class</code>. This is because any object is
475 * accepted as a key and is coerced into a string.</p>
476 *
477 * @param context The context of this evaluation.
478 * @param base The bean to analyze.
479 * @return <code>null</code> if base is <code>null</code>; otherwise
480 * <code>Object.class</code>.
481 */
482 public Class<?> getCommonPropertyType(ELContext context,
483 Object base) {
484 if (base == null){
485 return null;
486 }
487
488 return Object.class;
489 }
490
491 /*
492 * Get a public method form a public class or interface of a given method.
493 * Note that if a PropertyDescriptor is obtained for a non-public class that
494 * implements a public interface, the read/write methods will be for the
495 * class, and therefore inaccessible. To correct this, a version of the
496 * same method must be found in a superclass or interface.
497 **/
498
499 static private Method getMethod(Class cl, Method method) {
500
501 if (method == null) {
502 return null;
503 }
504
505 if (Modifier.isPublic (cl.getModifiers ())) {
506 return method;
507 }
508 Class [] interfaces = cl.getInterfaces ();
509 for (int i = 0; i < interfaces.length; i++) {
510 Class c = interfaces[i];
511 Method m = null;
512 try {
513 m = c.getMethod(method.getName(), method.getParameterTypes());
514 c = m.getDeclaringClass();
515 if ((m = getMethod(c, m)) != null)
516 return m;
517 } catch (NoSuchMethodException ex) {
518 }
519 }
520 Class c = cl.getSuperclass();
521 if (c != null) {
522 Method m = null;
523 try {
524 m = c.getMethod(method.getName(), method.getParameterTypes());
525 c = m.getDeclaringClass();
526 if ((m = getMethod(c, m)) != null)
527 return m;
528 } catch (NoSuchMethodException ex) {
529 }
530 }
531 return null;
532 }
533
534 private BeanProperty getBeanProperty(ELContext context,
535 Object base,
536 Object prop) {
537
538 String property = prop.toString();
539 Class baseClass = base.getClass();
540 BeanProperties bps = properties.get(baseClass);
541 if (bps == null) {
542 bps = new BeanProperties(baseClass);
543 properties.putIfAbsent(baseClass, bps);
544 }
545 BeanProperty bp = bps.getBeanProperty(property);
546 if (bp == null) {
547 throw new PropertyNotFoundException(
548 ELUtil.getExceptionMessageString(context,
549 "propertyNotFound",
550 new Object[] { baseClass.getName(),
551 property}));
552 }
553 return bp;
554 }
555
556 private void removeFromMap(Map<Class, BeanProperties> map,
557 ClassLoader classloader) {
558 Iterator<Class> iter = map.keySet().iterator();
559 while (iter.hasNext()) {
560 Class mbeanClass = iter.next();
561 if (classloader.equals(mbeanClass.getClassLoader())) {
562 iter.remove();
563 }
564 }
565
566 }
567
568 /*
569 * This method is not part of the API, though it can be used (reflectively)
570 * by clients of this class to remove entries from the cache when the beans
571 * are being unloaded.
572 *
573 * A note about why WeakHashMap is not used. Measurements has shown
574 * that ConcurrentHashMap is much more scalable than synchronized
575 * WeakHashMap. A manual purge seems to be a good compromise.
576 *
577 * @param classloader The classLoader used to load the beans.
578 */
579 private void purgeBeanClasses(ClassLoader classloader) {
580 removeFromMap(properties, classloader);
581 }
582 }
583