001/* $Id: SetPropertiesRule.java 471661 2006-11-06 08:09:25Z skitching $
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    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 values = new HashMap();
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}