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