001 /* $Id: SetNestedPropertiesRule.java 729101 2008-12-23 20:41:52Z rahul $ 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 public void setDigester(Digester digester) { 198 super.setDigester(digester); 199 log = digester.getLogger(); 200 } 201 202 /** 203 * When set to true, any text within child elements will have leading 204 * and trailing whitespace removed before assignment to the target 205 * object. The default value for this attribute is true. 206 */ 207 public void setTrimData(boolean trimData) { 208 this.trimData = trimData; 209 } 210 211 /** See {@link #setTrimData}. */ 212 public boolean getTrimData() { 213 return trimData; 214 } 215 216 /** 217 * Determines whether an error is reported when a nested element is 218 * encountered for which there is no corresponding property-setter 219 * method. 220 * <p> 221 * When set to false, any child element for which there is no 222 * corresponding object property will cause an error to be reported. 223 * <p> 224 * When set to true, any child element for which there is no 225 * corresponding object property will simply be ignored. 226 * <p> 227 * The default value of this attribute is false (unknown child elements 228 * are not allowed). 229 */ 230 public void setAllowUnknownChildElements(boolean allowUnknownChildElements) { 231 this.allowUnknownChildElements = allowUnknownChildElements; 232 } 233 234 /** See {@link #setAllowUnknownChildElements}. */ 235 public boolean getAllowUnknownChildElements() { 236 return allowUnknownChildElements; 237 } 238 239 /** 240 * Process the beginning of this element. 241 * 242 * @param namespace is the namespace this attribute is in, or null 243 * @param name is the name of the current xml element 244 * @param attributes is the attribute list of this element 245 */ 246 public void begin(String namespace, String name, Attributes attributes) 247 throws Exception { 248 Rules oldRules = digester.getRules(); 249 AnyChildRule anyChildRule = new AnyChildRule(); 250 anyChildRule.setDigester(digester); 251 AnyChildRules newRules = new AnyChildRules(anyChildRule); 252 newRules.init(digester.getMatch()+"/", oldRules); 253 digester.setRules(newRules); 254 } 255 256 /** 257 * This is only invoked after all child elements have been processed, 258 * so we can remove the custom Rules object that does the 259 * child-element-matching. 260 */ 261 public void body(String bodyText) throws Exception { 262 AnyChildRules newRules = (AnyChildRules) digester.getRules(); 263 digester.setRules(newRules.getOldRules()); 264 } 265 266 /** 267 * Add an additional custom xml-element -> property mapping. 268 * <p> 269 * This is primarily intended to be used from the xml rules module 270 * (as it is not possible there to pass the necessary parameters to the 271 * constructor for this class). However it is valid to use this method 272 * directly if desired. 273 */ 274 public void addAlias(String elementName, String propertyName) { 275 elementNames.put(elementName, propertyName); 276 } 277 278 /** 279 * Render a printable version of this Rule. 280 */ 281 public String toString() { 282 StringBuffer sb = new StringBuffer("SetNestedPropertiesRule["); 283 sb.append("allowUnknownChildElements="); 284 sb.append(allowUnknownChildElements); 285 sb.append(", trimData="); 286 sb.append(trimData); 287 sb.append(", elementNames="); 288 sb.append(elementNames); 289 sb.append("]"); 290 return sb.toString(); 291 } 292 293 //----------------------------------------- local classes 294 295 /** Private Rules implementation */ 296 private class AnyChildRules implements Rules { 297 private String matchPrefix = null; 298 private Rules decoratedRules = null; 299 300 private ArrayList<Rule> rules = new ArrayList<Rule>(1); 301 private AnyChildRule rule; 302 303 public AnyChildRules(AnyChildRule rule) { 304 this.rule = rule; 305 rules.add(rule); 306 } 307 308 public Digester getDigester() { return null; } 309 public void setDigester(Digester digester) {} 310 public String getNamespaceURI() {return null;} 311 public void setNamespaceURI(String namespaceURI) {} 312 public void add(String pattern, Rule rule) {} 313 public void clear() {} 314 315 public List<Rule> match(String matchPath) { 316 return match(null,matchPath); 317 } 318 319 public List<Rule> match(String namespaceURI, String matchPath) { 320 List<Rule> match = decoratedRules.match(namespaceURI, matchPath); 321 322 if ((matchPath.startsWith(matchPrefix)) && 323 (matchPath.indexOf('/', matchPrefix.length()) == -1)) { 324 325 // The current element is a direct child of the element 326 // specified in the init method, so we want to ensure that 327 // the rule passed to this object's constructor is included 328 // in the returned list of matching rules. 329 330 if ((match == null || match.size()==0)) { 331 // The "real" rules class doesn't have any matches for 332 // the specified path, so we return a list containing 333 // just one rule: the one passed to this object's 334 // constructor. 335 return rules; 336 } 337 else { 338 // The "real" rules class has rules that match the current 339 // node, so we return this list *plus* the rule passed to 340 // this object's constructor. 341 // 342 // It might not be safe to modify the returned list, 343 // so clone it first. 344 LinkedList<Rule> newMatch = new LinkedList<Rule>(match); 345 newMatch.addLast(rule); 346 return newMatch; 347 } 348 } 349 else { 350 return match; 351 } 352 } 353 354 public List<Rule> rules() { 355 // This is not actually expected to be called during normal 356 // processing. 357 // 358 // There is only one known case where this is called; when a rule 359 // returned from AnyChildRules.match is invoked and throws a 360 // SAXException then method Digester.endDocument will be called 361 // without having "uninstalled" the AnyChildRules ionstance. That 362 // method attempts to invoke the "finish" method for every Rule 363 // instance - and thus needs to call rules() on its Rules object, 364 // which is this one. Actually, java 1.5 and 1.6beta2 have a 365 // bug in their xml implementation such that endDocument is not 366 // called after a SAXException, but other parsers (eg Aelfred) 367 // do call endDocument. Here, we therefore need to return the 368 // rules registered with the underlying Rules object. 369 log.debug("AnyChildRules.rules invoked."); 370 return decoratedRules.rules(); 371 } 372 373 public void init(String prefix, Rules rules) { 374 matchPrefix = prefix; 375 decoratedRules = rules; 376 } 377 378 public Rules getOldRules() { 379 return decoratedRules; 380 } 381 } 382 383 private class AnyChildRule extends Rule { 384 private String currChildNamespaceURI = null; 385 private String currChildElementName = null; 386 387 public void begin(String namespaceURI, String name, 388 Attributes attributes) throws Exception { 389 390 currChildNamespaceURI = namespaceURI; 391 currChildElementName = name; 392 } 393 394 public void body(String value) throws Exception { 395 String propName = currChildElementName; 396 if (elementNames.containsKey(currChildElementName)) { 397 // overide propName 398 propName = (String) elementNames.get(currChildElementName); 399 if (propName == null) { 400 // user wants us to ignore this element 401 return; 402 } 403 } 404 405 boolean debug = log.isDebugEnabled(); 406 407 if (debug) { 408 log.debug("[SetNestedPropertiesRule]{" + digester.match + 409 "} Setting property '" + propName + "' to '" + 410 value + "'"); 411 } 412 413 // Populate the corresponding properties of the top object 414 Object top = digester.peek(); 415 if (debug) { 416 if (top != null) { 417 log.debug("[SetNestedPropertiesRule]{" + digester.match + 418 "} Set " + top.getClass().getName() + 419 " properties"); 420 } else { 421 log.debug("[SetPropertiesRule]{" + digester.match + 422 "} Set NULL properties"); 423 } 424 } 425 426 if (trimData) { 427 value = value.trim(); 428 } 429 430 if (!allowUnknownChildElements) { 431 // Force an exception if the property does not exist 432 // (BeanUtils.setProperty() silently returns in this case) 433 if (top instanceof DynaBean) { 434 DynaProperty desc = 435 ((DynaBean) top).getDynaClass().getDynaProperty(propName); 436 if (desc == null) { 437 throw new NoSuchMethodException 438 ("Bean has no property named " + propName); 439 } 440 } else /* this is a standard JavaBean */ { 441 PropertyDescriptor desc = 442 PropertyUtils.getPropertyDescriptor(top, propName); 443 if (desc == null) { 444 throw new NoSuchMethodException 445 ("Bean has no property named " + propName); 446 } 447 } 448 } 449 450 try 451 { 452 BeanUtils.setProperty(top, propName, value); 453 } 454 catch(NullPointerException e) { 455 log.error("NullPointerException: " 456 + "top=" + top + ",propName=" + propName + ",value=" + value + "!"); 457 throw e; 458 } 459 } 460 461 public void end(String namespace, String name) throws Exception { 462 currChildElementName = null; 463 } 464 } 465 }