View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts.faces.application;
23  
24  
25  import java.beans.FeatureDescriptor;
26  import java.util.Arrays;
27  import java.util.Iterator;
28  
29  import jakarta.el.CompositeELResolver;
30  import jakarta.el.ELContext;
31  import jakarta.el.ELException;
32  import jakarta.el.ELResolver;
33  import jakarta.el.PropertyNotFoundException;
34  import jakarta.el.PropertyNotWritableException;
35  
36  import org.apache.commons.beanutils.ConversionException;
37  import org.apache.commons.beanutils.DynaBean;
38  import org.apache.commons.beanutils.DynaProperty;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  
43  
44  /**
45   * Defines property resolution behavior on instances of {@link DynaBean}.
46   *
47   * <p>This resolver handles base objects of type {@code DynaBean}.
48   * It accepts any object as a property and uses that object as a key in
49   * the map. The resulting value is the value in the map that is associated with
50   * that key.</p>
51   *
52   * <p>This resolver can be constructed in read-only mode, which means that
53   * {@link #isReadOnly} will always return {@code true} and {@link #setValue}
54   * will always throw {@code PropertyNotWritableException}.</p>
55   *
56   * <p>{@code ELResolver}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   * @see CompositeELResolver
61   * @see ELResolver
62   * @see DynaBean
63   * @since Struts 1.4.1
64   */
65  public class DynaBeanELResolver extends ELResolver {
66  
67      /**
68       * The {@code Log} instance for this class.
69       */
70      private final Logger log =
71          LoggerFactory.getLogger(DynaBeanELResolver.class);
72  
73      /**
74       * Flag if this {@code ELRsolver} is in read-only-mode {@code true}.
75       */
76      private final boolean readOnly;
77  
78      /**
79       * Creates a new read/write {@code DynaBeanELResolver}.
80       */
81      public DynaBeanELResolver() {
82          this(false);
83      }
84  
85      /**
86       * Creates a new {@code DynaBeanELResolver} whose read-only status is
87       * determined by the given parameter.
88       *
89       * @param readOnly {@code true} if this resolver cannot modify
90       *     properties; {@code false} otherwise.
91       */
92      public DynaBeanELResolver(boolean readOnly) {
93          log.debug("Creating new Dyna-Action-From-ELResolver "
94              + "instance with read-only: '{}'", readOnly);
95  
96          this.readOnly = readOnly;
97      }
98  
99      /**
100      * If the base object is a {@code DynaBean}, returns the most general
101      * acceptable type for a value in this map..
102      *
103      * <p>If the base is a {@code DynaBean}, the {@code propertyResolved}
104      * property of the {@code ELContext} object must be set to {@code true}
105      * by this resolver, before returning. If this property is not
106      * {@code true} after this method is called, the caller should ignore
107      * the return value.</p>
108      *
109      * <p>Assuming the base is a {@code DynaBean}, this method will always
110      * return the Java class representing the data type of the underlying
111      * property value.</p>
112      *
113      * @param context The context of this evaluation.
114      * @param base The base to analyze. Only bases of type
115      *     {@code DynaBean} are handled by this resolver.
116      * @param property The key to return the acceptable type for.
117      *
118      * @return If the {@code propertyResolved} property of {@code ELContext}
119      *     was set to {@code true}, then the most general acceptable type;
120      *     otherwise undefined.
121      *
122      * @throws NullPointerException if context is {@code null}
123      * @throws PropertyNotFoundException if the given property name is
124      *     not found.
125      * @throws ELException if an exception was thrown while performing
126      *     the property or variable resolution. The thrown exception
127      *     must be included as the cause property of this exception, if
128      *     available.
129      */
130     @Override
131     public Class<?> getType(ELContext context, Object base, Object property) {
132         if (context == null) {
133             throw new NullPointerException();
134         }
135 
136         if (base instanceof DynaBean) {
137             log.trace("Returning property-type '{}' for DynaBean '{}'",
138                 property, base);
139 
140             final DynaBean dynaBean = (DynaBean) base;
141             final String key = property.toString();
142             final DynaProperty dynaProperty = getDynaProperty(dynaBean, key);
143             if (dynaProperty == null) {
144                 throw new PropertyNotFoundException(key);
145             }
146 
147             context.setPropertyResolved(true);
148             return dynaProperty.getType();
149         }
150 
151         return null;
152     }
153 
154     /**
155      * If the base object is a {@code DynaBean}, returns the value associated
156      * with the given key, as specified by the {@code property} argument. If
157      * the key was not found, {@code PropertyNotFoundException} is thrown.
158      *
159      * <p>If the base is a {@code DynaBean}, the {@code propertyResolved}
160      * property of the {@code ELContext} object must be set to {@code true}
161      * by this resolver, before returning. If this property is not
162      * {@code true} after this method is called, the caller should ignore
163      * the return value.</p>
164      *
165      * <p>Just as in {@link DynaBean#get(String)}, just because {@code null}
166      * is returned doesn't mean there is no mapping for the key; it's also
167      * possible that the method explicitly returns {@code null}.</p>
168      *
169      * @param context The context of this evaluation.
170      * @param base The base to be analyzed. Only bases of type
171      *     {@code DynaBean} are handled by this resolver.
172      * @param property The key whose associated value is to be returned.
173      *
174      * @return If the {@code propertyResolved} property of {@code ELContext}
175      *     was set to {@code true}, then the value associated with the given key.
176      *
177      * @throws NullPointerException if context is {@code null}.
178      * @throws PropertyNotFoundException if the given property does not
179      *     exists.
180      * @throws ELException if an exception was thrown while performing
181      *     the property or variable resolution. The thrown exception
182      *     must be included as the cause property of this exception, if
183      *     available.
184      */
185     @Override
186     public Object getValue(ELContext context, Object base, Object property) {
187         if (context == null) {
188             throw new NullPointerException();
189         }
190 
191         if (base instanceof DynaBean) {
192             log.trace("Returning dynamic property '{}' for DynaBean '{}'",
193                 property, base);
194 
195             final DynaBean dynaBean = (DynaBean) base;
196             final String key = property.toString();
197             final DynaProperty dynaProperty = getDynaProperty(dynaBean, key);
198             if (dynaProperty == null) {
199                 throw new PropertyNotFoundException(key);
200             }
201 
202             context.setPropertyResolved(true);
203             return dynaBean.get(key);
204         }
205 
206         return null;
207     }
208 
209 
210     /**
211      * If the base is a {@code DynaBean}, attempts to set the value
212      * associated with the given key, as specified by the
213      * {@code property} argument.
214      *
215      * <p>If the base is a {@code DynaBean}, the {@code propertyResolved}
216      * property of the {@code ELContext} object must be set to {@code true}
217      * by this resolver, before returning. If this property is not
218      * {@code true} after this method is called, the caller can safely
219      * assume no value was set.</p>
220      *
221      * <p>If this resolver was constructed in read-only mode, this method will
222      * always throw {@code PropertyNotWritableException}.</p>
223      *
224      * @param context The context of this evaluation.
225      * @param base The base to be modified. Only bases of type
226      *     {@code DynaBean} are handled by this resolver.
227      * @param property The key with which the specified value is to be
228      *     associated.
229      * @param value The value to be associated with the specified key.
230 
231      * @throws NullPointerException if context is {@code null} or if
232      *     an attempt is made to set a primitive property to {@code null}.
233      * @throws ConversionException if the specified value cannot be
234      *      converted to the type required for this property.
235      * @throws PropertyNotFoundException if the given property does not
236      *     exists.
237      * @throws PropertyNotWritableException if this resolver was constructed
238      *     in read-only mode.
239      * @throws ELException if an exception was thrown while performing
240      *     the property or variable resolution. The thrown exception
241      *     must be included as the cause property of this exception, if
242      *     available.
243      */
244     @Override
245     public void setValue(ELContext context, Object base, Object property, Object value) {
246         if (context == null) {
247             throw new NullPointerException();
248         }
249 
250         if (base instanceof DynaBean) {
251             log.trace("Setting dynamic property '{}' for DynaBean '{}'",
252                 property, base);
253 
254             final DynaBean dynaBean = (DynaBean) base;
255             final String key = property.toString();
256             final DynaProperty dynaProperty = getDynaProperty(dynaBean, key);
257             if (dynaProperty == null) {
258                 throw new PropertyNotFoundException(key);
259             }
260 
261             if (readOnly) {
262                 throw new PropertyNotWritableException();
263             }
264 
265             context.setPropertyResolved(true);
266             dynaBean.set(key, value);
267         }
268     }
269 
270     /**
271      * If the base object is a {@code DynaBean}, returns whether a call to
272      * {@link #setValue} will always fail.
273      *
274      * <p>If the base is a {@code DynaBean}, the {@code propertyResolved}
275      * property of the {@code ELContext} object must be set to {@code true}
276      * by this resolver, before returning. If this property is not
277      * {@code true} after this method is called, the caller should ignore
278      * the return value.</p>
279      *
280      * <p>If this resolver was constructed in read-only mode, this method will
281      * always return {@code true}.</p>
282      *
283      * @param context The context of this evaluation.
284      * @param base The base to analyze. Only bases of type {@code DynaBean}
285      *     are handled by this resolver.
286      * @param property The key to return the read-only status for.
287      *
288      * @return If the {@code propertyResolved} property of {@code ELContext}
289      *     was set to {@code true}, then {@code true} if calling the
290      *     {@code setValue} method will always fail or {@code false} if it
291      *     is possible that such a call may succeed; otherwise undefined.
292      *
293      * @throws NullPointerException if context is {@code null}.
294      * @throws PropertyNotFoundException if the given property does not
295      *     exists.
296      * @throws ELException if an exception was thrown while performing
297      *     the property or variable resolution. The thrown exception
298      *     must be included as the cause property of this exception, if
299      *     available.
300      */
301     @Override
302     public boolean isReadOnly(ELContext context, Object base, Object property) {
303         if (context == null) {
304             throw new NullPointerException();
305         }
306 
307         if (base instanceof DynaBean) {
308             log.trace("Return ready-only status for dynamic property '{}' for DynaBean '{}'",
309                 property, base);
310 
311             final DynaBean dynaBean = (DynaBean) base;
312             final String key = property.toString();
313             final DynaProperty dynaProperty = getDynaProperty(dynaBean, key);
314             if (dynaProperty == null) {
315                 throw new PropertyNotFoundException(key);
316             }
317 
318             context.setPropertyResolved(true);
319             return readOnly;
320         }
321 
322         return false;
323     }
324 
325     /**
326      * Returns information about the set of variables or properties that
327      * can be resolved for the given {@code base} object. One use for
328      * this method is to assist tools in auto-completion.
329      *
330      * <p>If the {@code base} parameter is {@code null}, the resolver
331      * must enumerate the list of top-level variables it can resolve.</p>
332      *
333      * <p>The {@code Iterator} returned must contain zero or more
334      * instances of {@link FeatureDescriptor}, in no guaranteed
335      * order. Each info object contains information about a property
336      * in the {@code DynaBean}, as obtained by calling the
337      * {@link org.apache.commons.beanutils.DynaClass#getDynaProperties()}
338      * method. The {@code FeatureDescriptor} is initialized using the same
339      * fields as are present in the {@code DynaProperty}, with the
340      * additional required named attributes "{@code type}" and
341      * "{@code resolvableAtDesignTime}" set as follows:</p>
342      * <ul>
343      *     <li>{@link ELResolver#TYPE} - The runtime type of the property, from
344      *         {link org.apache.commons.beanutils.DynaProperty#getType()}.</li>
345      *     <li>{@link ELResolver#RESOLVABLE_AT_DESIGN_TIME} - {@code true}.</li>
346      * </ul>
347      *
348      * <p>The caller should be aware that the {@code Iterator}
349      * returned might iterate through a very large or even infinitely large
350      * set of properties. Care should be taken by the caller to not get
351      * stuck in an infinite loop.</p>
352      *
353      * <p>This is a "best-effort" list.  Not all {@code ELResolver}s
354      * will return completely accurate results, but all must be callable
355      * at both design-time and runtime (i.e. whether or not
356      * {@link java.beans.Beans#isDesignTime()} returns {@code true}),
357      * without causing errors.</p>
358      *
359      * <p>The {@code propertyResolved} property of the
360      * {@code ELContext} is not relevant to this method.
361      * The results of all {@code ELResolver}s are concatenated
362      * in the case of composite resolvers.</p>
363      *
364      * @param context The context of this evaluation.
365      * @param base The base object whose set of valid properties is to
366      *     be enumerated, or {@code null} to enumerate the set of
367      *     top-level variables that this resolver can evaluate.
368      * @return An {@code Iterator} containing zero or more (possibly
369      *     infinitely more) {@code FeatureDescriptor} objects, or
370      *     {@code null} if this resolver does not handle the given
371      *     {@code base} object or that the results are too complex to
372      *     represent with this method
373      * @see java.beans.FeatureDescriptor
374      */
375     @Override
376     public Iterator<FeatureDescriptor> getFeatureDescriptors(
377             ELContext context,
378             Object base) {
379 
380         if (base instanceof DynaBean) {
381             log.trace("Get Feature-Descriptors for DynaBean '{}'", base);
382 
383             final DynaBean dynaBean = (DynaBean) base;
384             final DynaProperty[] properties = dynaBean.getDynaClass().getDynaProperties();
385 
386             final int iMax = properties.length;
387             final FeatureDescriptor[] descriptors = new FeatureDescriptor[iMax];
388             for (int i = 0; i < iMax; i++) {
389                 final DynaProperty property = properties[i];
390 
391                 final FeatureDescriptor descriptor = new FeatureDescriptor();
392                 descriptor.setName(property.getName());
393                 descriptor.setDisplayName(property.getName());
394                 descriptor.setExpert(false);
395                 descriptor.setHidden(false);
396                 descriptor.setPreferred(true);
397                 descriptor.setShortDescription(null);
398                 descriptor.setValue(TYPE, property.getType());
399                 descriptor.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE);
400 
401                 descriptors[i] = descriptor;
402             }
403 
404             return Arrays.asList(descriptors).iterator();
405         }
406 
407         return null;
408     }
409 
410     /**
411      * If the base object is a {@code DynaBean}, returns the most
412      * general type that this resolver accepts for the {@code property}
413      * argument. Otherwise, returns {@code null}.
414      *
415      * <p>Assuming the base is a {@code DynaBean}, this method will
416      * always return {@code Object.class}. This is because any object is
417      * accepted as a key and is coerced into a string.</p>
418      *
419      * @param context The context of this evaluation.
420      * @param base The base to analyze. Only bases of type
421      *     {@code DynaBean} are handled by this resolver.
422      *
423      * @return {@code null} if base is not a {@code DynaBean}; otherwise
424      *     {@code Object.class}.
425      */
426     @Override
427     public Class<?> getCommonPropertyType(ELContext context, Object base) {
428         if (base instanceof DynaBean) {
429             log.trace("Get Common-Property-Type for DynaBean '{}'", base);
430 
431             return Object.class;
432         }
433         return null;
434     }
435 
436     /**
437      * Return the {@code DynaProperty} describing the specified property
438      * of the specified {@code DynaBean}, or {@code null} if there is no
439      * such property defined on the underlying {@code DynaClass}.
440      *
441      * @param bean {@code DynaBean} to be checked
442      * @param property The property to be checked
443      */
444     private DynaProperty getDynaProperty(DynaBean bean, String property)
445         throws PropertyNotFoundException {
446 
447         DynaProperty dynaProperty = null;
448         try {
449             dynaProperty = bean.getDynaClass().getDynaProperty(property);
450         } catch (IllegalArgumentException e) {
451             log.trace("Get Dyna-Property '{}'", property, e);
452         }
453 
454         return (dynaProperty);
455     }
456 }