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 }