001 package org.apache.commons.digester3; 002 003 /* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022 import static org.apache.commons.beanutils.BeanUtils.setProperty; 023 import static org.apache.commons.beanutils.PropertyUtils.getPropertyDescriptor; 024 025 import static java.lang.String.format; 026 027 import java.beans.PropertyDescriptor; 028 import java.util.ArrayList; 029 import java.util.HashMap; 030 import java.util.LinkedList; 031 import java.util.List; 032 import java.util.Map; 033 034 import org.apache.commons.beanutils.DynaBean; 035 import org.apache.commons.beanutils.DynaProperty; 036 import org.apache.commons.logging.Log; 037 import org.xml.sax.Attributes; 038 039 /** 040 * <p> 041 * Rule implementation that sets properties on the object at the top of the stack, based on child elements with names 042 * matching properties on that object. 043 * </p> 044 * <p> 045 * Example input that can be processed by this rule: 046 * </p> 047 * 048 * <pre> 049 * [widget] 050 * [height]7[/height] 051 * [width]8[/width] 052 * [label]Hello, world[/label] 053 * [/widget] 054 * </pre> 055 * <p> 056 * For each child element of [widget], a corresponding setter method is located on the object on the top of the digester 057 * stack, the body text of the child element is converted to the type specified for the (sole) parameter to the setter 058 * method, then the setter method is invoked. 059 * </p> 060 * <p> 061 * This rule supports custom mapping of xml element names to property names. The default mapping for particular elements 062 * can be overridden by using {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}. This 063 * allows child elements to be mapped to properties with different names. Certain elements can also be marked to be 064 * ignored. 065 * </p> 066 * <p> 067 * A very similar effect can be achieved using a combination of the <code>BeanPropertySetterRule</code> and the 068 * <code>ExtendedBaseRules</code> rules manager; this <code>Rule</code>, however, works fine with the default 069 * <code>RulesBase</code> rules manager. 070 * </p> 071 * <p> 072 * Note that this rule is designed to be used to set only "primitive" bean properties, eg String, int, boolean. If some 073 * of the child xml elements match ObjectCreateRule rules (ie cause objects to be created) then you must use one of the 074 * more complex constructors to this rule to explicitly skip processing of that xml element, and define a SetNextRule 075 * (or equivalent) to handle assigning the child object to the appropriate property instead. 076 * </p> 077 * <p> 078 * <b>Implementation Notes</b> 079 * </p> 080 * <p> 081 * This class works by creating its own simple Rules implementation. When begin is invoked on this rule, the digester's 082 * current rules object is replaced by a custom one. When end is invoked for this rule, the original rules object is 083 * restored. The digester rules objects therefore behave in a stack-like manner. 084 * </p> 085 * <p> 086 * For each child element encountered, the custom Rules implementation ensures that a special AnyChildRule instance is 087 * included in the matches returned to the digester, and it is this rule instance that is responsible for setting the 088 * appropriate property on the target object (if such a property exists). The effect is therefore like a 089 * "trailing wildcard pattern". The custom Rules implementation also returns the matches provided by the underlying 090 * Rules implementation for the same pattern, so other rules are not "disabled" during processing of a 091 * SetNestedPropertiesRule. 092 * </p> 093 * <p> 094 * TODO: Optimise this class. Currently, each time begin is called, new AnyChildRules and AnyChildRule objects are 095 * created. It should be possible to cache these in normal use (though watch out for when a rule instance is invoked 096 * re-entrantly!). 097 * </p> 098 * 099 * @since 1.6 100 */ 101 public class SetNestedPropertiesRule 102 extends Rule 103 { 104 105 private Log log = null; 106 107 private boolean trimData = true; 108 109 private boolean allowUnknownChildElements = false; 110 111 private HashMap<String, String> elementNames = new HashMap<String, String>(); 112 113 // ----------------------------------------------------------- Constructors 114 115 /** 116 * Base constructor, which maps every child element into a bean property with the same name as the xml element. 117 * <p> 118 * It is an error if a child xml element exists but the target java bean has no such property (unless 119 * {@link #setAllowUnknownChildElements(boolean)} has been set to true). 120 * </p> 121 */ 122 public SetNestedPropertiesRule() 123 { 124 // nothing to set up 125 } 126 127 /** 128 * <p> 129 * Convenience constructor which overrides the default mappings for just one property. 130 * </p> 131 * <p> 132 * For details about how this works, see 133 * {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}. 134 * </p> 135 * 136 * @param elementName is the child xml element to match 137 * @param propertyName is the java bean property to be assigned the value of the specified xml element. This may be 138 * null, in which case the specified xml element will be ignored. 139 */ 140 public SetNestedPropertiesRule( String elementName, String propertyName ) 141 { 142 elementNames.put( elementName, propertyName ); 143 } 144 145 /** 146 * <p> 147 * Constructor which allows element->property mapping to be overridden. 148 * </p> 149 * <p> 150 * Two arrays are passed in. One contains xml element names and the other java bean property names. The element name 151 * / property name pairs are matched by position; in order words, the first string in the element name array 152 * corresponds to the first string in the property name array and so on. 153 * </p> 154 * <p> 155 * If a property name is null or the xml element name has no matching property name due to the arrays being of 156 * different lengths then this indicates that the xml element should be ignored. 157 * </p> 158 * <h5>Example One</h5> 159 * <p> 160 * The following constructs a rule that maps the <code>alt-city</code> element to the <code>city</code> property and 161 * the <code>alt-state</code> to the <code>state</code> property. All other child elements are mapped as usual using 162 * exact name matching. <code><pre> 163 * SetNestedPropertiesRule( 164 * new String[] {"alt-city", "alt-state"}, 165 * new String[] {"city", "state"}); 166 * </pre></code> 167 * </p> 168 * <h5>Example Two</h5> 169 * <p> 170 * The following constructs a rule that maps the <code>class</code> xml element to the <code>className</code> 171 * property. The xml element <code>ignore-me</code> is not mapped, ie is ignored. All other elements are mapped as 172 * usual using exact name matching. <code><pre> 173 * SetPropertiesRule( 174 * new String[] {"class", "ignore-me"}, 175 * new String[] {"className"}); 176 * </pre></code> 177 * </p> 178 * 179 * @param elementNames names of elements to map 180 * @param propertyNames names of properties mapped to 181 */ 182 public SetNestedPropertiesRule( String[] elementNames, String[] propertyNames ) 183 { 184 for ( int i = 0, size = elementNames.length; i < size; i++ ) 185 { 186 String propName = null; 187 if ( i < propertyNames.length ) 188 { 189 propName = propertyNames[i]; 190 } 191 192 this.elementNames.put( elementNames[i], propName ); 193 } 194 } 195 196 /** 197 * Constructor which allows element->property mapping to be overridden. 198 * 199 * @param elementNames names of elements->properties to map 200 * @since 3.0 201 */ 202 public SetNestedPropertiesRule( Map<String, String> elementNames ) 203 { 204 if ( elementNames != null && !elementNames.isEmpty() ) 205 { 206 this.elementNames.putAll( elementNames ); 207 } 208 } 209 210 // --------------------------------------------------------- Public Methods 211 212 /** 213 * {@inheritDoc} 214 */ 215 @Override 216 public void setDigester( Digester digester ) 217 { 218 super.setDigester( digester ); 219 log = digester.getLogger(); 220 } 221 222 /** 223 * When set to true, any text within child elements will have leading and trailing whitespace removed before 224 * assignment to the target object. The default value for this attribute is true. 225 * 226 * @param trimData flag to have leading and trailing whitespace removed 227 */ 228 public void setTrimData( boolean trimData ) 229 { 230 this.trimData = trimData; 231 } 232 233 /** 234 * Return the flag to have leading and trailing whitespace removed. 235 * 236 * @see #setTrimData(boolean) 237 * @return flag to have leading and trailing whitespace removed 238 */ 239 public boolean getTrimData() 240 { 241 return trimData; 242 } 243 244 /** 245 * Determines whether an error is reported when a nested element is encountered for which there is no corresponding 246 * property-setter method. 247 * <p> 248 * When set to false, any child element for which there is no corresponding object property will cause an error to 249 * be reported. 250 * <p> 251 * When set to true, any child element for which there is no corresponding object property will simply be ignored. 252 * <p> 253 * The default value of this attribute is false (unknown child elements are not allowed). 254 * 255 * @param allowUnknownChildElements flag to ignore any child element for which there is no corresponding 256 * object property 257 */ 258 public void setAllowUnknownChildElements( boolean allowUnknownChildElements ) 259 { 260 this.allowUnknownChildElements = allowUnknownChildElements; 261 } 262 263 /** 264 * Return the flag to ignore any child element for which there is no corresponding object property 265 * 266 * @return flag to ignore any child element for which there is no corresponding object property 267 * @see #setAllowUnknownChildElements(boolean) 268 */ 269 public boolean getAllowUnknownChildElements() 270 { 271 return allowUnknownChildElements; 272 } 273 274 /** 275 * {@inheritDoc} 276 */ 277 @Override 278 public void begin( String namespace, String name, Attributes attributes ) 279 throws Exception 280 { 281 Rules oldRules = getDigester().getRules(); 282 AnyChildRule anyChildRule = new AnyChildRule(); 283 anyChildRule.setDigester( getDigester() ); 284 AnyChildRules newRules = new AnyChildRules( anyChildRule ); 285 newRules.init( getDigester().getMatch() + "/", oldRules ); 286 getDigester().setRules( newRules ); 287 } 288 289 /** 290 * {@inheritDoc} 291 */ 292 @Override 293 public void body( String namespace, String name, String text ) 294 throws Exception 295 { 296 AnyChildRules newRules = (AnyChildRules) getDigester().getRules(); 297 getDigester().setRules( newRules.getOldRules() ); 298 } 299 300 /** 301 * Add an additional custom xml-element -> property mapping. 302 * <p> 303 * This is primarily intended to be used from the xml rules module (as it is not possible there to pass the 304 * necessary parameters to the constructor for this class). However it is valid to use this method directly if 305 * desired. 306 * 307 * @param elementName the xml-element has to be mapped 308 * @param propertyName the property name target 309 */ 310 public void addAlias( String elementName, String propertyName ) 311 { 312 elementNames.put( elementName, propertyName ); 313 } 314 315 /** 316 * {@inheritDoc} 317 */ 318 @Override 319 public String toString() 320 { 321 return format( "SetNestedPropertiesRule[allowUnknownChildElements=%s, trimData=%s, elementNames=%s]", 322 allowUnknownChildElements, 323 trimData, 324 elementNames ); 325 } 326 327 // ----------------------------------------- local classes 328 329 /** Private Rules implementation */ 330 private class AnyChildRules 331 implements Rules 332 { 333 private String matchPrefix = null; 334 335 private Rules decoratedRules = null; 336 337 private ArrayList<Rule> rules = new ArrayList<Rule>( 1 ); 338 339 private AnyChildRule rule; 340 341 public AnyChildRules( AnyChildRule rule ) 342 { 343 this.rule = rule; 344 rules.add( rule ); 345 } 346 347 public Digester getDigester() 348 { 349 return null; 350 } 351 352 public void setDigester( Digester digester ) 353 { 354 } 355 356 public String getNamespaceURI() 357 { 358 return null; 359 } 360 361 public void setNamespaceURI( String namespaceURI ) 362 { 363 } 364 365 public void add( String pattern, Rule rule ) 366 { 367 } 368 369 public void clear() 370 { 371 } 372 373 public List<Rule> match( String namespaceURI, String matchPath, String name, Attributes attributes ) 374 { 375 List<Rule> match = decoratedRules.match( namespaceURI, matchPath, name, attributes ); 376 377 if ( ( matchPath.startsWith( matchPrefix ) ) && ( matchPath.indexOf( '/', matchPrefix.length() ) == -1 ) ) 378 { 379 380 // The current element is a direct child of the element 381 // specified in the init method, so we want to ensure that 382 // the rule passed to this object's constructor is included 383 // in the returned list of matching rules. 384 385 if ( ( match == null || match.size() == 0 ) ) 386 { 387 // The "real" rules class doesn't have any matches for 388 // the specified path, so we return a list containing 389 // just one rule: the one passed to this object's 390 // constructor. 391 return rules; 392 } 393 // The "real" rules class has rules that match the current 394 // node, so we return this list *plus* the rule passed to 395 // this object's constructor. 396 // 397 // It might not be safe to modify the returned list, 398 // so clone it first. 399 LinkedList<Rule> newMatch = new LinkedList<Rule>( match ); 400 newMatch.addLast( rule ); 401 return newMatch; 402 } 403 return match; 404 } 405 406 public List<Rule> rules() 407 { 408 // This is not actually expected to be called during normal 409 // processing. 410 // 411 // There is only one known case where this is called; when a rule 412 // returned from AnyChildRules.match is invoked and throws a 413 // SAXException then method Digester.endDocument will be called 414 // without having "uninstalled" the AnyChildRules ionstance. That 415 // method attempts to invoke the "finish" method for every Rule 416 // instance - and thus needs to call rules() on its Rules object, 417 // which is this one. Actually, java 1.5 and 1.6beta2 have a 418 // bug in their xml implementation such that endDocument is not 419 // called after a SAXException, but other parsers (eg Aelfred) 420 // do call endDocument. Here, we therefore need to return the 421 // rules registered with the underlying Rules object. 422 log.debug( "AnyChildRules.rules invoked." ); 423 return decoratedRules.rules(); 424 } 425 426 public void init( String prefix, Rules rules ) 427 { 428 matchPrefix = prefix; 429 decoratedRules = rules; 430 } 431 432 public Rules getOldRules() 433 { 434 return decoratedRules; 435 } 436 } 437 438 private class AnyChildRule 439 extends Rule 440 { 441 442 private String currChildElementName = null; 443 444 @Override 445 public void begin( String namespaceURI, String name, Attributes attributes ) 446 throws Exception 447 { 448 currChildElementName = name; 449 } 450 451 @Override 452 public void body( String namespace, String name, String text ) 453 throws Exception 454 { 455 String propName = currChildElementName; 456 if ( elementNames.containsKey( currChildElementName ) ) 457 { 458 // overide propName 459 propName = elementNames.get( currChildElementName ); 460 if ( propName == null ) 461 { 462 // user wants us to ignore this element 463 return; 464 } 465 } 466 467 boolean debug = log.isDebugEnabled(); 468 469 if ( debug ) 470 { 471 log.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Setting property '" + propName 472 + "' to '" + text + "'" ); 473 } 474 475 // Populate the corresponding properties of the top object 476 Object top = getDigester().peek(); 477 if ( debug ) 478 { 479 if ( top != null ) 480 { 481 log.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Set " 482 + top.getClass().getName() + " properties" ); 483 } 484 else 485 { 486 log.debug( "[SetPropertiesRule]{" + getDigester().getMatch() + "} Set NULL properties" ); 487 } 488 } 489 490 if ( trimData ) 491 { 492 text = text.trim(); 493 } 494 495 if ( !allowUnknownChildElements ) 496 { 497 // Force an exception if the property does not exist 498 // (BeanUtils.setProperty() silently returns in this case) 499 if ( top instanceof DynaBean ) 500 { 501 DynaProperty desc = ( (DynaBean) top ).getDynaClass().getDynaProperty( propName ); 502 if ( desc == null ) 503 { 504 throw new NoSuchMethodException( "Bean has no property named " + propName ); 505 } 506 } 507 else 508 /* this is a standard JavaBean */ 509 { 510 PropertyDescriptor desc = getPropertyDescriptor( top, propName ); 511 if ( desc == null ) 512 { 513 throw new NoSuchMethodException( "Bean has no property named " + propName ); 514 } 515 } 516 } 517 518 try 519 { 520 setProperty( top, propName, text ); 521 } 522 catch ( NullPointerException e ) 523 { 524 log.error( "NullPointerException: " + "top=" + top + ",propName=" + propName + ",value=" + text + "!" ); 525 throw e; 526 } 527 } 528 529 @Override 530 public void end( String namespace, String name ) 531 throws Exception 532 { 533 currChildElementName = null; 534 } 535 } 536 537 }