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