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