    001    /* $Id: SetNestedPropertiesRule.java 729101 2008-12-23 20:41:52Z rahul $
    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        public void setDigester(Digester digester) {
    198            super.setDigester(digester);
    199            log = digester.getLogger();
    200        }
    201
    202        /**
    203         * When set to true, any text within child elements will have leading
    204         * and trailing whitespace removed before assignment to the target
    205         * object. The default value for this attribute is true.
    206         */
    207        public void setTrimData(boolean trimData) {
    208            this.trimData = trimData;
    209        }
    210
    211        /** See {@link #setTrimData}. */
    212         public boolean getTrimData() {
    213            return trimData;
    214        }
    215
    216        /**
    217         * Determines whether an error is reported when a nested element is
    218         * encountered for which there is no corresponding property-setter
    219         * method.
    220         * <p>
    221         * When set to false, any child element for which there is no
    222         * corresponding object property will cause an error to be reported.
    223         * <p>
    224         * When set to true, any child element for which there is no
    225         * corresponding object property will simply be ignored.
    226         * <p>
    227         * The default value of this attribute is false (unknown child elements
    228         * are not allowed).
    229         */
    230        public void setAllowUnknownChildElements(boolean allowUnknownChildElements) {
    231            this.allowUnknownChildElements = allowUnknownChildElements;
    232        }
    233
    234        /** See {@link #setAllowUnknownChildElements}. */
    235         public boolean getAllowUnknownChildElements() {
    236            return allowUnknownChildElements;
    237        }
    238
    239        /**
    240         * Process the beginning of this element.
    241         *
    242         * @param namespace is the namespace this attribute is in, or null
    243         * @param name is the name of the current xml element
    244         * @param attributes is the attribute list of this element
    245         */
    246        public void begin(String namespace, String name, Attributes attributes)
    247                          throws Exception {
    248            Rules oldRules = digester.getRules();
    249            AnyChildRule anyChildRule = new AnyChildRule();
    250            anyChildRule.setDigester(digester);
    251            AnyChildRules newRules = new AnyChildRules(anyChildRule);
    252            newRules.init(digester.getMatch()+"/", oldRules);
    253            digester.setRules(newRules);
    254        }
    255
    256        /**
    257         * This is only invoked after all child elements have been processed,
    258         * so we can remove the custom Rules object that does the
    259         * child-element-matching.
    260         */
    261        public void body(String bodyText) throws Exception {
    262            AnyChildRules newRules = (AnyChildRules) digester.getRules();
    263            digester.setRules(newRules.getOldRules());
    264        }
    265
    266        /**
    267         * Add an additional custom xml-element -> property mapping.
    268         * <p>
    269         * This is primarily intended to be used from the xml rules module
    270         * (as it is not possible there to pass the necessary parameters to the
    271         * constructor for this class). However it is valid to use this method
    272         * directly if desired.
    273         */
    274        public void addAlias(String elementName, String propertyName) {
    275            elementNames.put(elementName, propertyName);
    276        }
    277
    278        /**
    279         * Render a printable version of this Rule.
    280         */
    281        public String toString() {
    282            StringBuffer sb = new StringBuffer("SetNestedPropertiesRule[");
    283            sb.append("allowUnknownChildElements=");
    284            sb.append(allowUnknownChildElements);
    285            sb.append(", trimData=");
    286            sb.append(trimData);
    287            sb.append(", elementNames=");
    288            sb.append(elementNames);
    289            sb.append("]");
    290            return sb.toString();
    291        }
    292
    293        //----------------------------------------- local classes
    294
    295        /** Private Rules implementation */
    296        private class AnyChildRules implements Rules {
    297            private String matchPrefix = null;
    298            private Rules decoratedRules = null;
    299
    300            private ArrayList<Rule> rules = new ArrayList<Rule>(1);
    301            private AnyChildRule rule;
    302
    303            public AnyChildRules(AnyChildRule rule) {
    304                this.rule = rule;
    305                rules.add(rule);
    306            }
    307
    308            public Digester getDigester() { return null; }
    309            public void setDigester(Digester digester) {}
    310            public String getNamespaceURI() {return null;}
    311            public void setNamespaceURI(String namespaceURI) {}
    312            public void add(String pattern, Rule rule) {}
    313            public void clear() {}
    314
    315            public List<Rule> match(String matchPath) {
    316                return match(null,matchPath);
    317            }
    318
    319            public List<Rule> match(String namespaceURI, String matchPath) {
    320                List<Rule> match = decoratedRules.match(namespaceURI, matchPath);
    321
    322                if ((matchPath.startsWith(matchPrefix)) &&
    323                    (matchPath.indexOf('/', matchPrefix.length()) == -1)) {
    324
    325                    // The current element is a direct child of the element
    326                    // specified in the init method, so we want to ensure that
    327                    // the rule passed to this object's constructor is included
    328                    // in the returned list of matching rules.
    329
    330                    if ((match == null || match.size()==0)) {
    331                        // The "real" rules class doesn't have any matches for
    332                        // the specified path, so we return a list containing
    333                        // just one rule: the one passed to this object's
    334                        // constructor.
    335                        return rules;
    336                    }
    337                    else {
    338                        // The "real" rules class has rules that match the current
    339                        // node, so we return this list *plus* the rule passed to
    340                        // this object's constructor.
    341                        //
    342                        // It might not be safe to modify the returned list,
    343                        // so clone it first.
    344                        LinkedList<Rule> newMatch = new LinkedList<Rule>(match);
    345                        newMatch.addLast(rule);
    346                        return newMatch;
    347                    }
    348                }
    349                else {
    350                    return match;
    351                }
    352            }
    353
    354            public List<Rule> rules() {
    355                // This is not actually expected to be called during normal
    356                // processing.
    357                //
    358                // There is only one known case where this is called; when a rule
    359                // returned from AnyChildRules.match is invoked and throws a
    360                // SAXException then method Digester.endDocument will be called
    361                // without having "uninstalled" the AnyChildRules ionstance. That
    362                // method attempts to invoke the "finish" method for every Rule
    363                // instance - and thus needs to call rules() on its Rules object,
    364                // which is this one. Actually, java 1.5 and 1.6beta2 have a
    365                // bug in their xml implementation such that endDocument is not
    366                // called after a SAXException, but other parsers (eg Aelfred)
    367                // do call endDocument. Here, we therefore need to return the
    368                // rules registered with the underlying Rules object.
    369                log.debug("AnyChildRules.rules invoked.");
    370                return decoratedRules.rules();
    371            }
    372
    373            public void init(String prefix, Rules rules) {
    374                matchPrefix = prefix;
    375                decoratedRules = rules;
    376            }
    377
    378            public Rules getOldRules() {
    379                return decoratedRules;
    380            }
    381        }
    382
    383        private class AnyChildRule extends Rule {
    384            private String currChildNamespaceURI = null;
    385            private String currChildElementName = null;
    386
    387            public void begin(String namespaceURI, String name,
    388                                  Attributes attributes) throws Exception {
    389
    390                currChildNamespaceURI = namespaceURI;
    391                currChildElementName = name;
    392            }
    393
    394            public void body(String value) throws Exception {
    395                String propName = currChildElementName;
    396                if (elementNames.containsKey(currChildElementName)) {
    397                    // overide propName
    398                    propName = (String) elementNames.get(currChildElementName);
    399                    if (propName == null) {
    400                        // user wants us to ignore this element
    401                        return;
    402                    }
    403                }
    404
    405                boolean debug = log.isDebugEnabled();
    406
    407                if (debug) {
    408                    log.debug("[SetNestedPropertiesRule]{" + digester.match +
    409                            "} Setting property '" + propName + "' to '" +
    410                            value + "'");
    411                }
    412
    413                // Populate the corresponding properties of the top object
    414                Object top = digester.peek();
    415                if (debug) {
    416                    if (top != null) {
    417                        log.debug("[SetNestedPropertiesRule]{" + digester.match +
    418                                           "} Set " + top.getClass().getName() +
    419                                           " properties");
    420                    } else {
    421                        log.debug("[SetPropertiesRule]{" + digester.match +
    422                                           "} Set NULL properties");
    423                    }
    424                }
    425
    426                if (trimData) {
    427                    value = value.trim();
    428                }
    429
    430                if (!allowUnknownChildElements) {
    431                    // Force an exception if the property does not exist
    432                    // (BeanUtils.setProperty() silently returns in this case)
    433                    if (top instanceof DynaBean) {
    434                        DynaProperty desc =
    435                            ((DynaBean) top).getDynaClass().getDynaProperty(propName);
    436                        if (desc == null) {
    437                            throw new NoSuchMethodException
    438                                ("Bean has no property named " + propName);
    439                        }
    440                    } else /* this is a standard JavaBean */ {
    441                        PropertyDescriptor desc =
    442                            PropertyUtils.getPropertyDescriptor(top, propName);
    443                        if (desc == null) {
    444                            throw new NoSuchMethodException
    445                                ("Bean has no property named " + propName);
    446                        }
    447                    }
    448                }
    449
    450                try
    451                {
    452                BeanUtils.setProperty(top, propName, value);
    453                }
    454                catch(NullPointerException e) {
    455                    log.error("NullPointerException: "
    456                     + "top=" + top + ",propName=" + propName + ",value=" + value + "!");
    457                     throw e;
    458                }
    459            }
    460
    461            public void end(String namespace, String name) throws Exception {
    462                currChildElementName = null;
    463            }
    464        }
    465    }