    001    /* $Id: SetPropertiesRule.java 729103 2008-12-23 20:42:59Z 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.HashMap;
    024
    025    import org.apache.commons.beanutils.BeanUtils;
    026    import org.apache.commons.beanutils.PropertyUtils;
    027    import org.xml.sax.Attributes;
    028
    029
    030    /**
    031     * <p>Rule implementation that sets properties on the object at the top of the
    032     * stack, based on attributes with corresponding names.</p>
    033     *
    034     * <p>This rule supports custom mapping of attribute names to property names.
    035     * The default mapping for particular attributes can be overridden by using
    036     * {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}.
    037     * This allows attributes to be mapped to properties with different names.
    038     * Certain attributes can also be marked to be ignored.</p>
    039     */
    040
    041    public class SetPropertiesRule extends Rule {
    042
    043
    044        // ----------------------------------------------------------- Constructors
    045
    046
    047        /**
    048         * Default constructor sets only the the associated Digester.
    049         *
    050         * @param digester The digester with which this rule is associated
    051         *
    052         * @deprecated The digester instance is now set in the {@link Digester#addRule} method.
    053         * Use {@link #SetPropertiesRule()} instead.
    054         */
    055        public SetPropertiesRule(Digester digester) {
    056
    057            this();
    058
    059        }
    060
    061
    062        /**
    063         * Base constructor.
    064         */
    065        public SetPropertiesRule() {
    066
    067            // nothing to set up
    068
    069        }
    070
    071        /**
    072         * <p>Convenience constructor overrides the mapping for just one property.</p>
    073         *
    074         * <p>For details about how this works, see
    075         * {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}.</p>
    076         *
    077         * @param attributeName map this attribute
    078         * @param propertyName to a property with this name
    079         */
    080        public SetPropertiesRule(String attributeName, String propertyName) {
    081
    082            attributeNames = new String[1];
    083            attributeNames[0] = attributeName;
    084            propertyNames = new String[1];
    085            propertyNames[0] = propertyName;
    086        }
    087
    088        /**
    089         * <p>Constructor allows attribute->property mapping to be overriden.</p>
    090         *
    091         * <p>Two arrays are passed in.
    092         * One contains the attribute names and the other the property names.
    093         * The attribute name / property name pairs are match by position
    094         * In order words, the first string in the attribute name list matches
    095         * to the first string in the property name list and so on.</p>
    096         *
    097         * <p>If a property name is null or the attribute name has no matching
    098         * property name, then this indicates that the attibute should be ignored.</p>
    099         *
    100         * <h5>Example One</h5>
    101         * <p> The following constructs a rule that maps the <code>alt-city</code>
    102         * attribute to the <code>city</code> property and the <code>alt-state</code>
    103         * to the <code>state</code> property.
    104         * All other attributes are mapped as usual using exact name matching.
    105         * <code><pre>
    106         *      SetPropertiesRule(
    107         *                new String[] {"alt-city", "alt-state"},
    108         *                new String[] {"city", "state"});
    109         * </pre></code>
    110         *
    111         * <h5>Example Two</h5>
    112         * <p> The following constructs a rule that maps the <code>class</code>
    113         * attribute to the <code>className</code> property.
    114         * The attribute <code>ignore-me</code> is not mapped.
    115         * All other attributes are mapped as usual using exact name matching.
    116         * <code><pre>
    117         *      SetPropertiesRule(
    118         *                new String[] {"class", "ignore-me"},
    119         *                new String[] {"className"});
    120         * </pre></code>
    121         *
    122         * @param attributeNames names of attributes to map
    123         * @param propertyNames names of properties mapped to
    124         */
    125        public SetPropertiesRule(String[] attributeNames, String[] propertyNames) {
    126            // create local copies
    127            this.attributeNames = new String[attributeNames.length];
    128            for (int i=0, size=attributeNames.length; i<size; i++) {
    129                this.attributeNames[i] = attributeNames[i];
    130            }
    131
    132            this.propertyNames = new String[propertyNames.length];
    133            for (int i=0, size=propertyNames.length; i<size; i++) {
    134                this.propertyNames[i] = propertyNames[i];
    135            }
    136        }
    137
    138        // ----------------------------------------------------- Instance Variables
    139
    140        /**
    141         * Attribute names used to override natural attribute->property mapping
    142         */
    143        private String [] attributeNames;
    144        /**
    145         * Property names used to override natural attribute->property mapping
    146         */
    147        private String [] propertyNames;
    148
    149        /**
    150         * Used to determine whether the parsing should fail if an property specified
    151         * in the XML is missing from the bean. Default is true for backward compatibility.
    152         */
    153        private boolean ignoreMissingProperty = true;
    154
    155
    156        // --------------------------------------------------------- Public Methods
    157
    158
    159        /**
    160         * Process the beginning of this element.
    161         *
    162         * @param attributes The attribute list of this element
    163         */
    164        public void begin(Attributes attributes) throws Exception {
    165
    166            // Build a set of attribute names and corresponding values
    167            HashMap<String, String> values = new HashMap<String, String>();
    168
    169            // set up variables for custom names mappings
    170            int attNamesLength = 0;
    171            if (attributeNames != null) {
    172                attNamesLength = attributeNames.length;
    173            }
    174            int propNamesLength = 0;
    175            if (propertyNames != null) {
    176                propNamesLength = propertyNames.length;
    177            }
    178
    179
    180            for (int i = 0; i < attributes.getLength(); i++) {
    181                String name = attributes.getLocalName(i);
    182                if ("".equals(name)) {
    183                    name = attributes.getQName(i);
    184                }
    185                String value = attributes.getValue(i);
    186
    187                // we'll now check for custom mappings
    188                for (int n = 0; n<attNamesLength; n++) {
    189                    if (name.equals(attributeNames[n])) {
    190                        if (n < propNamesLength) {
    191                            // set this to value from list
    192                            name = propertyNames[n];
    193
    194                        } else {
    195                            // set name to null
    196                            // we'll check for this later
    197                            name = null;
    198                        }
    199                        break;
    200                    }
    201                }
    202
    203                if (digester.log.isDebugEnabled()) {
    204                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
    205                            "} Setting property '" + name + "' to '" +
    206                            value + "'");
    207                }
    208
    209                if ((!ignoreMissingProperty) && (name != null)) {
    210                    // The BeanUtils.populate method silently ignores items in
    211                    // the map (ie xml entities) which have no corresponding
    212                    // setter method, so here we check whether each xml attribute
    213                    // does have a corresponding property before calling the
    214                    // BeanUtils.populate method.
    215                    //
    216                    // Yes having the test and set as separate steps is ugly and
    217                    // inefficient. But BeanUtils.populate doesn't provide the
    218                    // functionality we need here, and changing the algorithm which
    219                    // determines the appropriate setter method to invoke is
    220                    // considered too risky.
    221                    //
    222                    // Using two different classes (PropertyUtils vs BeanUtils) to
    223                    // do the test and the set is also ugly; the codepaths
    224                    // are different which could potentially lead to trouble.
    225                    // However the BeanUtils/ProperyUtils code has been carefully
    226                    // compared and the PropertyUtils functionality does appear
    227                    // compatible so we'll accept the risk here.
    228
    229                    Object top = digester.peek();
    230                    boolean test =  PropertyUtils.isWriteable(top, name);
    231                    if (!test)
    232                        throw new NoSuchMethodException("Property " + name + " can't be set");
    233                }
    234
    235                if (name != null) {
    236                    values.put(name, value);
    237                }
    238            }
    239
    240            // Populate the corresponding properties of the top object
    241            Object top = digester.peek();
    242            if (digester.log.isDebugEnabled()) {
    243                if (top != null) {
    244                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
    245                                       "} Set " + top.getClass().getName() +
    246                                       " properties");
    247                } else {
    248                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
    249                                       "} Set NULL properties");
    250                }
    251            }
    252            BeanUtils.populate(top, values);
    253
    254
    255        }
    256
    257
    258        /**
    259         * <p>Add an additional attribute name to property name mapping.
    260         * This is intended to be used from the xml rules.
    261         */
    262        public void addAlias(String attributeName, String propertyName) {
    263
    264            // this is a bit tricky.
    265            // we'll need to resize the array.
    266            // probably should be synchronized but digester's not thread safe anyway
    267            if (attributeNames == null) {
    268
    269                attributeNames = new String[1];
    270                attributeNames[0] = attributeName;
    271                propertyNames = new String[1];
    272                propertyNames[0] = propertyName;
    273
    274            } else {
    275                int length = attributeNames.length;
    276                String [] tempAttributes = new String[length + 1];
    277                for (int i=0; i<length; i++) {
    278                    tempAttributes[i] = attributeNames[i];
    279                }
    280                tempAttributes[length] = attributeName;
    281
    282                String [] tempProperties = new String[length + 1];
    283                for (int i=0; i<length && i< propertyNames.length; i++) {
    284                    tempProperties[i] = propertyNames[i];
    285                }
    286                tempProperties[length] = propertyName;
    287
    288                propertyNames = tempProperties;
    289                attributeNames = tempAttributes;
    290            }
    291        }
    292
    293
    294        /**
    295         * Render a printable version of this Rule.
    296         */
    297        public String toString() {
    298
    299            StringBuffer sb = new StringBuffer("SetPropertiesRule[");
    300            sb.append("]");
    301            return (sb.toString());
    302
    303        }
    304
    305        /**
    306         * <p>Are attributes found in the xml without matching properties to be ignored?
    307         * </p><p>
    308         * If false, the parsing will interrupt with an <code>NoSuchMethodException</code>
    309         * if a property specified in the XML is not found. The default is true.
    310         * </p>
    311         * @return true if skipping the unmatched attributes.
    312         */
    313        public boolean isIgnoreMissingProperty() {
    314
    315            return this.ignoreMissingProperty;
    316        }
    317
    318        /**
    319         * Sets whether attributes found in the xml without matching properties
    320         * should be ignored.
    321         * If set to false, the parsing will throw an <code>NoSuchMethodException</code>
    322         * if an unmatched
    323         * attribute is found. This allows to trap misspellings in the XML file.
    324         * @param ignoreMissingProperty false to stop the parsing on unmatched attributes.
    325         */
    326        public void setIgnoreMissingProperty(boolean ignoreMissingProperty) {
    327
    328            this.ignoreMissingProperty = ignoreMissingProperty;
    329        }
    330
    331
    332    }