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    }