001    package org.apache.commons.digester3;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import static org.apache.commons.beanutils.BeanUtils.setProperty;
023    import static org.apache.commons.beanutils.PropertyUtils.getPropertyDescriptor;
024    
025    import static java.lang.String.format;
026    
027    import java.beans.PropertyDescriptor;
028    import java.util.ArrayList;
029    import java.util.HashMap;
030    import java.util.LinkedList;
031    import java.util.List;
032    import java.util.Map;
033    
034    import org.apache.commons.beanutils.DynaBean;
035    import org.apache.commons.beanutils.DynaProperty;
036    import org.apache.commons.logging.Log;
037    import org.xml.sax.Attributes;
038    
039    /**
040     * <p>
041     * Rule implementation that sets properties on the object at the top of the stack, based on child elements with names
042     * matching properties on that object.
043     * </p>
044     * <p>
045     * Example input that can be processed by this rule:
046     * </p>
047     * 
048     * <pre>
049     *   [widget]
050     *    [height]7[/height]
051     *    [width]8[/width]
052     *    [label]Hello, world[/label]
053     *   [/widget]
054     * </pre>
055     * <p>
056     * For each child element of [widget], a corresponding setter method is located on the object on the top of the digester
057     * stack, the body text of the child element is converted to the type specified for the (sole) parameter to the setter
058     * method, then the setter method is invoked.
059     * </p>
060     * <p>
061     * This rule supports custom mapping of xml element names to property names. The default mapping for particular elements
062     * can be overridden by using {@link #SetNestedPropertiesRule(String[] elementNames, String[] propertyNames)}. This
063     * allows child elements to be mapped to properties with different names. Certain elements can also be marked to be
064     * ignored.
065     * </p>
066     * <p>
067     * A very similar effect can be achieved using a combination of the <code>BeanPropertySetterRule</code> and the
068     * <code>ExtendedBaseRules</code> rules manager; this <code>Rule</code>, however, works fine with the default
069     * <code>RulesBase</code> rules manager.
070     * </p>
071     * <p>
072     * Note that this rule is designed to be used to set only "primitive" bean properties, eg String, int, boolean. If some
073     * of the child xml elements match ObjectCreateRule rules (ie cause objects to be created) then you must use one of the
074     * more complex constructors to this rule to explicitly skip processing of that xml element, and define a SetNextRule
075     * (or equivalent) to handle assigning the child object to the appropriate property instead.
076     * </p>
077     * <p>
078     * <b>Implementation Notes</b>
079     * </p>
080     * <p>
081     * This class works by creating its own simple Rules implementation. When begin is invoked on this rule, the digester's
082     * current rules object is replaced by a custom one. When end is invoked for this rule, the original rules object is
083     * restored. The digester rules objects therefore behave in a stack-like manner.
084     * </p>
085     * <p>
086     * For each child element encountered, the custom Rules implementation ensures that a special AnyChildRule instance is
087     * included in the matches returned to the digester, and it is this rule instance that is responsible for setting the
088     * appropriate property on the target object (if such a property exists). The effect is therefore like a
089     * "trailing wildcard pattern". The custom Rules implementation also returns the matches provided by the underlying
090     * Rules implementation for the same pattern, so other rules are not "disabled" during processing of a
091     * SetNestedPropertiesRule.
092     * </p>
093     * <p>
094     * TODO: Optimise this class. Currently, each time begin is called, new AnyChildRules and AnyChildRule objects are
095     * created. It should be possible to cache these in normal use (though watch out for when a rule instance is invoked
096     * re-entrantly!).
097     * </p>
098     * 
099     * @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    }