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
020package org.apache.commons.digester;
021
022
023import java.util.List;
024import java.util.LinkedList;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.beans.PropertyDescriptor;
028
029import org.apache.commons.beanutils.BeanUtils;
030import org.apache.commons.beanutils.DynaBean;
031import org.apache.commons.beanutils.DynaProperty;
032import org.apache.commons.beanutils.PropertyUtils;
033
034import org.xml.sax.Attributes;
035
036import 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
102public 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}