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