KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > commons > digester > SetNestedPropertiesRule


1 /* $Id: SetNestedPropertiesRule.java 179714 2005-06-03 03:53:39Z skitching $
2  *
3  * Copyright 2003-2004 The Apache Software Foundation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17
18
19 package org.apache.commons.digester;
20
21
22 import java.util.List JavaDoc;
23 import java.util.LinkedList JavaDoc;
24 import java.util.ArrayList JavaDoc;
25 import java.util.HashMap JavaDoc;
26 import java.beans.PropertyDescriptor JavaDoc;
27
28 import org.apache.commons.beanutils.BeanUtils;
29 import org.apache.commons.beanutils.DynaBean;
30 import org.apache.commons.beanutils.DynaProperty;
31 import org.apache.commons.beanutils.PropertyUtils;
32
33 import org.xml.sax.Attributes JavaDoc;
34
35 import org.apache.commons.logging.Log;
36
37
38 /**
39  * <p>Rule implementation that sets properties on the object at the top of the
40  * stack, based on child elements with names matching properties on that
41  * object.</p>
42  *
43  * <p>Example input that can be processed by this rule:</p>
44  * <pre>
45  * [widget]
46  * [height]7[/height]
47  * [width]8[/width]
48  * [label]Hello, world[/label]
49  * [/widget]
50  * </pre>
51  *
52  * <p>For each child element of [widget], a corresponding setter method is
53  * located on the object on the top of the digester stack, the body text of
54  * the child element is converted to the type specified for the (sole)
55  * parameter to the setter method, then the setter method is invoked.</p>
56  *
57  * <p>This rule supports custom mapping of xml element names to property names.
58  * The default mapping for particular elements can be overridden by using
59  * {@link #SetNestedPropertiesRule(String[] elementNames,
60  * String[] propertyNames)}.
61  * This allows child elements to be mapped to properties with different names.
62  * Certain elements can also be marked to be ignored.</p>
63  *
64  * <p>A very similar effect can be achieved using a combination of the
65  * <code>BeanPropertySetterRule</code> and the <code>ExtendedBaseRules</code>
66  * rules manager; this <code>Rule</code>, however, works fine with the default
67  * <code>RulesBase</code> rules manager.</p>
68  *
69  * <p>Note that this rule is designed to be used to set only "primitive"
70  * bean properties, eg String, int, boolean. If some of the child xml elements
71  * match ObjectCreateRule rules (ie cause objects to be created) then you must
72  * use one of the more complex constructors to this rule to explicitly skip
73  * processing of that xml element, and define a SetNextRule (or equivalent) to
74  * handle assigning the child object to the appropriate property instead.</p>
75  *
76  * <p><b>Implementation Notes</b></p>
77  *
78  * <p>This class works by creating its own simple Rules implementation. When
79  * begin is invoked on this rule, the digester's current rules object is
80  * replaced by a custom one. When end is invoked for this rule, the original
81  * rules object is restored. The digester rules objects therefore behave in
82  * a stack-like manner.</p>
83  *
84  * <p>For each child element encountered, the custom Rules implementation
85  * ensures that a special AnyChildRule instance is included in the matches
86  * returned to the digester, and it is this rule instance that is responsible
87  * for setting the appropriate property on the target object (if such a property
88  * exists). The effect is therefore like a "trailing wildcard pattern". The
89  * custom Rules implementation also returns the matches provided by the
90  * underlying Rules implementation for the same pattern, so other rules
91  * are not "disabled" during processing of a SetNestedPropertiesRule.</p>
92  *
93  * <p>TODO: Optimise this class. Currently, each time begin is called,
94  * new AnyChildRules and AnyChildRule objects are created. It should be
95  * possible to cache these in normal use (though watch out for when a rule
96  * instance is invoked re-entrantly!).</p>
97  *
98  * @since 1.6
99  */

100
101 public class SetNestedPropertiesRule extends Rule {
102
103     private Log log = null;
104     
105     private boolean trimData = true;
106     private boolean allowUnknownChildElements = false;
107     
108     private HashMap JavaDoc elementNames = new HashMap JavaDoc();
109
110     // ----------------------------------------------------------- Constructors
111

112     /**
113      * Base constructor, which maps every child element into a bean property
114      * with the same name as the xml element.
115      *
116      * <p>It is an error if a child xml element exists but the target java
117      * bean has no such property (unless setAllowUnknownChildElements has been
118      * set to true).</p>
119      */

120     public SetNestedPropertiesRule() {
121         // nothing to set up
122
}
123     
124     /**
125      * <p>Convenience constructor which overrides the default mappings for
126      * just one property.</p>
127      *
128      * <p>For details about how this works, see
129      * {@link #SetNestedPropertiesRule(String[] elementNames,
130      * String[] propertyNames)}.</p>
131      *
132      * @param elementName is the child xml element to match
133      * @param propertyName is the java bean property to be assigned the value
134      * of the specified xml element. This may be null, in which case the
135      * specified xml element will be ignored.
136      */

137     public SetNestedPropertiesRule(String JavaDoc elementName, String JavaDoc propertyName) {
138         elementNames.put(elementName, propertyName);
139     }
140     
141     /**
142      * <p>Constructor which allows element->property mapping to be overridden.
143      * </p>
144      *
145      * <p>Two arrays are passed in. One contains xml element names and the
146      * other java bean property names. The element name / property name pairs
147      * are matched by position; in order words, the first string in the element
148      * name array corresponds to the first string in the property name array
149      * and so on.</p>
150      *
151      * <p>If a property name is null or the xml element name has no matching
152      * property name due to the arrays being of different lengths then this
153      * indicates that the xml element should be ignored.</p>
154      *
155      * <h5>Example One</h5>
156      * <p> The following constructs a rule that maps the <code>alt-city</code>
157      * element to the <code>city</code> property and the <code>alt-state</code>
158      * to the <code>state</code> property. All other child elements are mapped
159      * as usual using exact name matching.
160      * <code><pre>
161      * SetNestedPropertiesRule(
162      * new String[] {"alt-city", "alt-state"},
163      * new String[] {"city", "state"});
164      * </pre></code>
165      * </p>
166      *
167      * <h5>Example Two</h5>
168      * <p> The following constructs a rule that maps the <code>class</code>
169      * xml element to the <code>className</code> property. The xml element
170      * <code>ignore-me</code> is not mapped, ie is ignored. All other elements
171      * are mapped as usual using exact name matching.
172      * <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 JavaDoc[] elementNames, String JavaDoc[] propertyNames) {
183         for (int i=0, size=elementNames.length; i<size; i++) {
184             String JavaDoc 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(boolean allowUnknownChildElements) {
230         this.allowUnknownChildElements = allowUnknownChildElements;
231     }
232     
233     /** See {@link #setAllowUnknownChildElements}. */
234      public boolean getAllowUnknownChildElements() {
235         return allowUnknownChildElements;
236     }
237     
238     /**
239      * Process the beginning of this element.
240      *
241      * @param namespace is the namespace this attribute is in, or null
242      * @param name is the name of the current xml element
243      * @param attributes is the attribute list of this element
244      */

245     public void begin(String JavaDoc namespace, String JavaDoc name, Attributes JavaDoc attributes)
246                       throws Exception JavaDoc {
247         Rules oldRules = digester.getRules();
248         AnyChildRule anyChildRule = new AnyChildRule();
249         anyChildRule.setDigester(digester);
250         AnyChildRules newRules = new AnyChildRules(anyChildRule);
251         newRules.init(digester.getMatch()+"/", oldRules);
252         digester.setRules(newRules);
253     }
254     
255     /**
256      * This is only invoked after all child elements have been processed,
257      * so we can remove the custom Rules object that does the
258      * child-element-matching.
259      */

260     public void body(String JavaDoc bodyText) throws Exception JavaDoc {
261         AnyChildRules newRules = (AnyChildRules) digester.getRules();
262         digester.setRules(newRules.getOldRules());
263     }
264
265     /**
266      * Add an additional custom xml-element -> property mapping.
267      * <p>
268      * This is primarily intended to be used from the xml rules module
269      * (as it is not possible there to pass the necessary parameters to the
270      * constructor for this class). However it is valid to use this method
271      * directly if desired.
272      */

273     public void addAlias(String JavaDoc elementName, String JavaDoc propertyName) {
274         elementNames.put(elementName, propertyName);
275     }
276   
277     /**
278      * Render a printable version of this Rule.
279      */

280     public String JavaDoc toString() {
281         StringBuffer JavaDoc sb = new StringBuffer JavaDoc("SetNestedPropertiesRule[");
282         sb.append("allowUnknownChildElements=");
283         sb.append(allowUnknownChildElements);
284         sb.append(", trimData=");
285         sb.append(trimData);
286         sb.append(", elementNames=");
287         sb.append(elementNames);
288         sb.append("]");
289         return sb.toString();
290     }
291
292     //----------------------------------------- local classes
293

294     /** Private Rules implementation */
295     private class AnyChildRules implements Rules {
296         private String JavaDoc matchPrefix = null;
297         private Rules decoratedRules = null;
298         
299         private ArrayList JavaDoc rules = new ArrayList JavaDoc(1);
300         private AnyChildRule rule;
301         
302         public AnyChildRules(AnyChildRule rule) {
303             this.rule = rule;
304             rules.add(rule);
305         }
306         
307         public Digester getDigester() { return null; }
308         public void setDigester(Digester digester) {}
309         public String JavaDoc getNamespaceURI() {return null;}
310         public void setNamespaceURI(String JavaDoc namespaceURI) {}
311         public void add(String JavaDoc pattern, Rule rule) {}
312         public void clear() {}
313         
314         public List JavaDoc match(String JavaDoc matchPath) {
315             return match(null,matchPath);
316         }
317         
318         public List JavaDoc match(String JavaDoc namespaceURI, String JavaDoc matchPath) {
319             List JavaDoc match = decoratedRules.match(namespaceURI, matchPath);
320             
321             if ((matchPath.startsWith(matchPrefix)) &&
322                 (matchPath.indexOf('/', matchPrefix.length()) == -1)) {
323                     
324                 // The current element is a direct child of the element
325
// specified in the init method, so we want to ensure that
326
// the rule passed to this object's constructor is included
327
// in the returned list of matching rules.
328

329                 if ((match == null || match.size()==0)) {
330                     // The "real" rules class doesn't have any matches for
331
// the specified path, so we return a list containing
332
// just one rule: the one passed to this object's
333
// constructor.
334
return rules;
335                 }
336                 else {
337                     // The "real" rules class has rules that match the current
338
// node, so we return this list *plus* the rule passed to
339
// this object's constructor.
340
//
341
// It might not be safe to modify the returned list,
342
// so clone it first.
343
LinkedList JavaDoc newMatch = new LinkedList JavaDoc(match);
344                     newMatch.addLast(rule);
345                     return newMatch;
346                 }
347             }
348             else {
349                 return match;
350             }
351         }
352         
353         public List JavaDoc rules() {
354             // This is not actually expected to be called.
355
throw new RuntimeException JavaDoc(
356                 "AnyChildRules.rules not implemented.");
357         }
358         
359         public void init(String JavaDoc prefix, Rules rules) {
360             matchPrefix = prefix;
361             decoratedRules = rules;
362         }
363         
364         public Rules getOldRules() {
365             return decoratedRules;
366         }
367     }
368     
369     private class AnyChildRule extends Rule {
370         private String JavaDoc currChildNamespaceURI = null;
371         private String JavaDoc currChildElementName = null;
372         
373         public void begin(String JavaDoc namespaceURI, String JavaDoc name,
374                               Attributes JavaDoc attributes) throws Exception JavaDoc {
375     
376             currChildNamespaceURI = namespaceURI;
377             currChildElementName = name;
378         }
379         
380         public void body(String JavaDoc value) throws Exception JavaDoc {
381             boolean debug = log.isDebugEnabled();
382
383             String JavaDoc propName = currChildElementName;
384             if (elementNames.containsKey(currChildElementName)) {
385                 // overide propName
386
propName = (String JavaDoc) elementNames.get(currChildElementName);
387                 if (propName == null) {
388                     // user wants us to ignore this element
389
return;
390                 }
391             }
392     
393             if (digester.log.isDebugEnabled()) {
394                 digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
395                         "} Setting property '" + propName + "' to '" +
396                         value + "'");
397             }
398     
399             // Populate the corresponding properties of the top object
400
Object JavaDoc top = digester.peek();
401             if (digester.log.isDebugEnabled()) {
402                 if (top != null) {
403                     digester.log.debug("[SetNestedPropertiesRule]{" + digester.match +
404                                        "} Set " + top.getClass().getName() +
405                                        " properties");
406                 } else {
407                     digester.log.debug("[SetPropertiesRule]{" + digester.match +
408                                        "} Set NULL properties");
409                 }
410             }
411  
412             if (trimData) {
413                 value = value.trim();
414             }
415
416             if (!allowUnknownChildElements) {
417                 // Force an exception if the property does not exist
418
// (BeanUtils.setProperty() silently returns in this case)
419
if (top instanceof DynaBean) {
420                     DynaProperty desc =
421                         ((DynaBean) top).getDynaClass().getDynaProperty(propName);
422                     if (desc == null) {
423                         throw new NoSuchMethodException JavaDoc
424                             ("Bean has no property named " + propName);
425                     }
426                 } else /* this is a standard JavaBean */ {
427                     PropertyDescriptor JavaDoc desc =
428                         PropertyUtils.getPropertyDescriptor(top, propName);
429                     if (desc == null) {
430                         throw new NoSuchMethodException JavaDoc
431                             ("Bean has no property named " + propName);
432                     }
433                 }
434             }
435             
436             try
437             {
438             BeanUtils.setProperty(top, propName, value);
439             }
440             catch(NullPointerException JavaDoc e) {
441                 digester.log.error("NullPointerException: "
442                  + "top=" + top + ",propName=" + propName + ",value=" + value + "!");
443                  throw e;
444             }
445         }
446     
447         public void end(String JavaDoc namespace, String JavaDoc name) throws Exception JavaDoc {
448             currChildElementName = null;
449         }
450     }
451 }
452
Popular Tags