001package 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
022import static org.apache.commons.beanutils.BeanUtils.setProperty;
023import static org.apache.commons.beanutils.PropertyUtils.getPropertyDescriptor;
024
025import static java.lang.String.format;
026
027import java.beans.PropertyDescriptor;
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.Map;
033
034import org.apache.commons.beanutils.DynaBean;
035import org.apache.commons.beanutils.DynaProperty;
036import org.apache.commons.logging.Log;
037import 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 */
101public 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}