001 /* $Id: SetNestedPropertiesRule.java 472836 2006-11-09 10:06:56Z skitching $
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 elementNames = new HashMap();
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 rules = new ArrayList(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 match(String matchPath) {
316 return match(null,matchPath);
317 }
318
319 public List match(String namespaceURI, String matchPath) {
320 List 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 newMatch = new LinkedList(match);
345 newMatch.addLast(rule);
346 return newMatch;
347 }
348 }
349 else {
350 return match;
351 }
352 }
353
354 public List 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 }