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 }