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    
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        @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    }