View Javadoc

1   package org.apache.commons.digester3;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import static org.apache.commons.beanutils.BeanUtils.setProperty;
23  import static org.apache.commons.beanutils.PropertyUtils.getPropertyDescriptor;
24  
25  import static java.lang.String.format;
26  
27  import java.beans.PropertyDescriptor;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.commons.beanutils.DynaBean;
35  import org.apache.commons.beanutils.DynaProperty;
36  import org.apache.commons.logging.Log;
37  import org.xml.sax.Attributes;
38  
39  /**
40   * <p>
41   * Rule implementation that sets properties on the object at the top of the stack, based on child elements with names
42   * matching properties on that object.
43   * </p>
44   * <p>
45   * Example input that can be processed by this rule:
46   * </p>
47   * 
48   * <pre>
49   *   [widget]
50   *    [height]7[/height]
51   *    [width]8[/width]
52   *    [label]Hello, world[/label]
53   *   [/widget]
54   * </pre>
55   * <p>
56   * For each child element of [widget], a corresponding setter method is located on the object on the top of the digester
57   * stack, the body text of the child element is converted to the type specified for the (sole) parameter to the setter
58   * method, then the setter method is invoked.
59   * </p>
60   * <p>
61   * This rule supports custom mapping of xml element names to property names. The default mapping for particular elements
62   * can be overridden by using {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}. This
63   * allows child elements to be mapped to properties with different names. Certain elements can also be marked to be
64   * ignored.
65   * </p>
66   * <p>
67   * A very similar effect can be achieved using a combination of the <code>BeanPropertySetterRule</code> and the
68   * <code>ExtendedBaseRules</code> rules manager; this <code>Rule</code>, however, works fine with the default
69   * <code>RulesBase</code> rules manager.
70   * </p>
71   * <p>
72   * Note that this rule is designed to be used to set only "primitive" bean properties, eg String, int, boolean. If some
73   * of the child xml elements match ObjectCreateRule rules (ie cause objects to be created) then you must use one of the
74   * more complex constructors to this rule to explicitly skip processing of that xml element, and define a SetNextRule
75   * (or equivalent) to handle assigning the child object to the appropriate property instead.
76   * </p>
77   * <p>
78   * <b>Implementation Notes</b>
79   * </p>
80   * <p>
81   * This class works by creating its own simple Rules implementation. When begin is invoked on this rule, the digester's
82   * current rules object is replaced by a custom one. When end is invoked for this rule, the original rules object is
83   * restored. The digester rules objects therefore behave in a stack-like manner.
84   * </p>
85   * <p>
86   * For each child element encountered, the custom Rules implementation ensures that a special AnyChildRule instance is
87   * included in the matches returned to the digester, and it is this rule instance that is responsible for setting the
88   * appropriate property on the target object (if such a property exists). The effect is therefore like a
89   * "trailing wildcard pattern". The custom Rules implementation also returns the matches provided by the underlying
90   * Rules implementation for the same pattern, so other rules are not "disabled" during processing of a
91   * SetNestedPropertiesRule.
92   * </p>
93   * <p>
94   * TODO: Optimise this class. Currently, each time begin is called, new AnyChildRules and AnyChildRule objects are
95   * created. It should be possible to cache these in normal use (though watch out for when a rule instance is invoked
96   * re-entrantly!).
97   * </p>
98   * 
99   * @since 1.6
100  */
101 public class SetNestedPropertiesRule
102     extends Rule
103 {
104 
105     private Log log = null;
106 
107     private boolean trimData = true;
108 
109     private boolean allowUnknownChildElements = false;
110 
111     private HashMap<String, String> elementNames = new HashMap<String, String>();
112 
113     // ----------------------------------------------------------- Constructors
114 
115     /**
116      * Base constructor, which maps every child element into a bean property with the same name as the xml element.
117      * <p>
118      * It is an error if a child xml element exists but the target java bean has no such property (unless
119      * {@link #setAllowUnknownChildElements(boolean)} has been set to true).
120      * </p>
121      */
122     public SetNestedPropertiesRule()
123     {
124         // nothing to set up
125     }
126 
127     /**
128      * <p>
129      * Convenience constructor which overrides the default mappings for just one property.
130      * </p>
131      * <p>
132      * For details about how this works, see
133      * {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}.
134      * </p>
135      * 
136      * @param elementName is the child xml element to match
137      * @param propertyName is the java bean property to be assigned the value of the specified xml element. This may be
138      *            null, in which case the specified xml element will be ignored.
139      */
140     public SetNestedPropertiesRule( String elementName, String propertyName )
141     {
142         elementNames.put( elementName, propertyName );
143     }
144 
145     /**
146      * <p>
147      * Constructor which allows element->property mapping to be overridden.
148      * </p>
149      * <p>
150      * Two arrays are passed in. One contains xml element names and the other java bean property names. The element name
151      * / property name pairs are matched by position; in order words, the first string in the element name array
152      * corresponds to the first string in the property name array and so on.
153      * </p>
154      * <p>
155      * If a property name is null or the xml element name has no matching property name due to the arrays being of
156      * different lengths then this indicates that the xml element should be ignored.
157      * </p>
158      * <h5>Example One</h5>
159      * <p>
160      * The following constructs a rule that maps the <code>alt-city</code> element to the <code>city</code> property and
161      * the <code>alt-state</code> to the <code>state</code> property. All other child elements are mapped as usual using
162      * exact name matching. <code><pre>
163      *      SetNestedPropertiesRule(
164      *                new String[] {"alt-city", "alt-state"}, 
165      *                new String[] {"city", "state"});
166      * </pre></code>
167      * </p>
168      * <h5>Example Two</h5>
169      * <p>
170      * The following constructs a rule that maps the <code>class</code> xml element to the <code>className</code>
171      * property. The xml element <code>ignore-me</code> is not mapped, ie is ignored. All other elements are mapped as
172      * usual using exact name matching. <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[] elementNames, String[] propertyNames )
183     {
184         for ( int i = 0, size = elementNames.length; i < size; i++ )
185         {
186             String propName = null;
187             if ( i < propertyNames.length )
188             {
189                 propName = propertyNames[i];
190             }
191 
192             this.elementNames.put( elementNames[i], propName );
193         }
194     }
195 
196     /**
197      * Constructor which allows element->property mapping to be overridden.
198      *
199      * @param elementNames names of elements->properties to map
200      * @since 3.0
201      */
202     public SetNestedPropertiesRule( Map<String, String> elementNames )
203     {
204         if ( elementNames != null && !elementNames.isEmpty() )
205         {
206             this.elementNames.putAll( elementNames );
207         }
208     }
209 
210     // --------------------------------------------------------- Public Methods
211 
212     /**
213      * {@inheritDoc}
214      */
215     @Override
216     public void setDigester( Digester digester )
217     {
218         super.setDigester( digester );
219         log = digester.getLogger();
220     }
221 
222     /**
223      * When set to true, any text within child elements will have leading and trailing whitespace removed before
224      * assignment to the target object. The default value for this attribute is true.
225      *
226      * @param trimData flag to have leading and trailing whitespace removed
227      */
228     public void setTrimData( boolean trimData )
229     {
230         this.trimData = trimData;
231     }
232 
233     /**
234      * Return the flag to have leading and trailing whitespace removed.
235      *
236      * @see #setTrimData(boolean)
237      * @return flag to have leading and trailing whitespace removed
238      */
239     public boolean getTrimData()
240     {
241         return trimData;
242     }
243 
244     /**
245      * Determines whether an error is reported when a nested element is encountered for which there is no corresponding
246      * property-setter method.
247      * <p>
248      * When set to false, any child element for which there is no corresponding object property will cause an error to
249      * be reported.
250      * <p>
251      * When set to true, any child element for which there is no corresponding object property will simply be ignored.
252      * <p>
253      * The default value of this attribute is false (unknown child elements are not allowed).
254      *
255      * @param allowUnknownChildElements flag to ignore any child element for which there is no corresponding
256      *        object property
257      */
258     public void setAllowUnknownChildElements( boolean allowUnknownChildElements )
259     {
260         this.allowUnknownChildElements = allowUnknownChildElements;
261     }
262 
263     /**
264      * Return the flag to ignore any child element for which there is no corresponding object property
265      *
266      * @return flag to ignore any child element for which there is no corresponding object property
267      * @see #setAllowUnknownChildElements(boolean)
268      */
269     public boolean getAllowUnknownChildElements()
270     {
271         return allowUnknownChildElements;
272     }
273 
274     /**
275      * {@inheritDoc}
276      */
277     @Override
278     public void begin( String namespace, String name, Attributes attributes )
279         throws Exception
280     {
281         Rules oldRules = getDigester().getRules();
282         AnyChildRule anyChildRule = new AnyChildRule();
283         anyChildRule.setDigester( getDigester() );
284         AnyChildRules newRules = new AnyChildRules( anyChildRule );
285         newRules.init( getDigester().getMatch() + "/", oldRules );
286         getDigester().setRules( newRules );
287     }
288 
289     /**
290      * {@inheritDoc}
291      */
292     @Override
293     public void body( String namespace, String name, String text )
294         throws Exception
295     {
296         AnyChildRules newRules = (AnyChildRules) getDigester().getRules();
297         getDigester().setRules( newRules.getOldRules() );
298     }
299 
300     /**
301      * Add an additional custom xml-element -> property mapping.
302      * <p>
303      * This is primarily intended to be used from the xml rules module (as it is not possible there to pass the
304      * necessary parameters to the constructor for this class). However it is valid to use this method directly if
305      * desired.
306      *
307      * @param elementName the xml-element has to be mapped
308      * @param propertyName the property name target
309      */
310     public void addAlias( String elementName, String propertyName )
311     {
312         elementNames.put( elementName, propertyName );
313     }
314 
315     /**
316      * {@inheritDoc}
317      */
318     @Override
319     public String toString()
320     {
321         return format( "SetNestedPropertiesRule[allowUnknownChildElements=%s, trimData=%s, elementNames=%s]",
322                        allowUnknownChildElements,
323                        trimData,
324                        elementNames );
325     }
326 
327     // ----------------------------------------- local classes
328 
329     /** Private Rules implementation */
330     private class AnyChildRules
331         implements Rules
332     {
333         private String matchPrefix = null;
334 
335         private Rules decoratedRules = null;
336 
337         private ArrayList<Rule> rules = new ArrayList<Rule>( 1 );
338 
339         private AnyChildRule rule;
340 
341         public AnyChildRules( AnyChildRule rule )
342         {
343             this.rule = rule;
344             rules.add( rule );
345         }
346 
347         public Digester getDigester()
348         {
349             return null;
350         }
351 
352         public void setDigester( Digester digester )
353         {
354         }
355 
356         public String getNamespaceURI()
357         {
358             return null;
359         }
360 
361         public void setNamespaceURI( String namespaceURI )
362         {
363         }
364 
365         public void add( String pattern, Rule rule )
366         {
367         }
368 
369         public void clear()
370         {
371         }
372 
373         public List<Rule> match( String namespaceURI, String matchPath, String name, Attributes attributes )
374         {
375             List<Rule> match = decoratedRules.match( namespaceURI, matchPath, name, attributes );
376 
377             if ( ( matchPath.startsWith( matchPrefix ) ) && ( matchPath.indexOf( '/', matchPrefix.length() ) == -1 ) )
378             {
379 
380                 // The current element is a direct child of the element
381                 // specified in the init method, so we want to ensure that
382                 // the rule passed to this object's constructor is included
383                 // in the returned list of matching rules.
384 
385                 if ( ( match == null || match.size() == 0 ) )
386                 {
387                     // The "real" rules class doesn't have any matches for
388                     // the specified path, so we return a list containing
389                     // just one rule: the one passed to this object's
390                     // constructor.
391                     return rules;
392                 }
393                 // The "real" rules class has rules that match the current
394                 // node, so we return this list *plus* the rule passed to
395                 // this object's constructor.
396                 //
397                 // It might not be safe to modify the returned list,
398                 // so clone it first.
399                 LinkedList<Rule> newMatch = new LinkedList<Rule>( match );
400                 newMatch.addLast( rule );
401                 return newMatch;
402             }
403             return match;
404         }
405 
406         public List<Rule> rules()
407         {
408             // This is not actually expected to be called during normal
409             // processing.
410             //
411             // There is only one known case where this is called; when a rule
412             // returned from AnyChildRules.match is invoked and throws a
413             // SAXException then method Digester.endDocument will be called
414             // without having "uninstalled" the AnyChildRules ionstance. That
415             // method attempts to invoke the "finish" method for every Rule
416             // instance - and thus needs to call rules() on its Rules object,
417             // which is this one. Actually, java 1.5 and 1.6beta2 have a
418             // bug in their xml implementation such that endDocument is not
419             // called after a SAXException, but other parsers (eg Aelfred)
420             // do call endDocument. Here, we therefore need to return the
421             // rules registered with the underlying Rules object.
422             log.debug( "AnyChildRules.rules invoked." );
423             return decoratedRules.rules();
424         }
425 
426         public void init( String prefix, Rules rules )
427         {
428             matchPrefix = prefix;
429             decoratedRules = rules;
430         }
431 
432         public Rules getOldRules()
433         {
434             return decoratedRules;
435         }
436     }
437 
438     private class AnyChildRule
439         extends Rule
440     {
441 
442         private String currChildElementName = null;
443 
444         @Override
445         public void begin( String namespaceURI, String name, Attributes attributes )
446             throws Exception
447         {
448             currChildElementName = name;
449         }
450 
451         @Override
452         public void body( String namespace, String name, String text )
453             throws Exception
454         {
455             String propName = currChildElementName;
456             if ( elementNames.containsKey( currChildElementName ) )
457             {
458                 // overide propName
459                 propName = elementNames.get( currChildElementName );
460                 if ( propName == null )
461                 {
462                     // user wants us to ignore this element
463                     return;
464                 }
465             }
466 
467             boolean debug = log.isDebugEnabled();
468 
469             if ( debug )
470             {
471                 log.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Setting property '" + propName
472                     + "' to '" + text + "'" );
473             }
474 
475             // Populate the corresponding properties of the top object
476             Object top = getDigester().peek();
477             if ( debug )
478             {
479                 if ( top != null )
480                 {
481                     log.debug( "[SetNestedPropertiesRule]{" + getDigester().getMatch() + "} Set "
482                         + top.getClass().getName() + " properties" );
483                 }
484                 else
485                 {
486                     log.debug( "[SetPropertiesRule]{" + getDigester().getMatch() + "} Set NULL properties" );
487                 }
488             }
489 
490             if ( trimData )
491             {
492                 text = text.trim();
493             }
494 
495             if ( !allowUnknownChildElements )
496             {
497                 // Force an exception if the property does not exist
498                 // (BeanUtils.setProperty() silently returns in this case)
499                 if ( top instanceof DynaBean )
500                 {
501                     DynaProperty desc = ( (DynaBean) top ).getDynaClass().getDynaProperty( propName );
502                     if ( desc == null )
503                     {
504                         throw new NoSuchMethodException( "Bean has no property named " + propName );
505                     }
506                 }
507                 else
508                 /* this is a standard JavaBean */
509                 {
510                     PropertyDescriptor desc = getPropertyDescriptor( top, propName );
511                     if ( desc == null )
512                     {
513                         throw new NoSuchMethodException( "Bean has no property named " + propName );
514                     }
515                 }
516             }
517 
518             try
519             {
520                 setProperty( top, propName, text );
521             }
522             catch ( NullPointerException e )
523             {
524                 log.error( "NullPointerException: " + "top=" + top + ",propName=" + propName + ",value=" + text + "!" );
525                 throw e;
526             }
527         }
528 
529         @Override
530         public void end( String namespace, String name )
531             throws Exception
532         {
533             currChildElementName = null;
534         }
535     }
536 
537 }