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 }