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