1 package org.apache.commons.digester3;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import static java.lang.String.format;
23 import static org.apache.commons.beanutils.BeanUtils.populate;
24 import static org.apache.commons.beanutils.PropertyUtils.isWriteable;
25
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import org.xml.sax.Attributes;
30
31 /**
32 * <p>
33 * Rule implementation that sets properties on the object at the top of the stack, based on attributes with
34 * corresponding names.
35 * </p>
36 * <p>
37 * This rule supports custom mapping of attribute names to property names. The default mapping for particular attributes
38 * can be overridden by using {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. This allows
39 * attributes to be mapped to properties with different names. Certain attributes can also be marked to be ignored.
40 * </p>
41 */
42 public class SetPropertiesRule
43 extends Rule
44 {
45
46 // ----------------------------------------------------------- Constructors
47
48 /**
49 * Base constructor.
50 */
51 public SetPropertiesRule()
52 {
53 // nothing to set up
54 }
55
56 /**
57 * <p>
58 * Convenience constructor overrides the mapping for just one property.
59 * </p>
60 * <p>
61 * For details about how this works, see {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}
62 * .
63 * </p>
64 *
65 * @param attributeName map this attribute
66 * @param propertyName to a property with this name
67 */
68 public SetPropertiesRule( String attributeName, String propertyName )
69 {
70 aliases.put( attributeName, propertyName );
71 }
72
73 /**
74 * <p>
75 * Constructor allows attribute->property mapping to be overriden.
76 * </p>
77 * <p>
78 * Two arrays are passed in. One contains the attribute names and the other the property names. The attribute name /
79 * property name pairs are match by position In order words, the first string in the attribute name list matches to
80 * the first string in the property name list and so on.
81 * </p>
82 * <p>
83 * If a property name is null or the attribute name has no matching property name, then this indicates that the
84 * attibute should be ignored.
85 * </p>
86 * <h5>Example One</h5>
87 * <p>
88 * The following constructs a rule that maps the <code>alt-city</code> attribute to the <code>city</code> property
89 * and the <code>alt-state</code> to the <code>state</code> property. All other attributes are mapped as usual using
90 * exact name matching. <code><pre>
91 * SetPropertiesRule(
92 * new String[] {"alt-city", "alt-state"},
93 * new String[] {"city", "state"});
94 * </pre></code>
95 * <h5>Example Two</h5>
96 * <p>
97 * The following constructs a rule that maps the <code>class</code> attribute to the <code>className</code>
98 * property. The attribute <code>ignore-me</code> is not mapped. All other attributes are mapped as usual using
99 * exact name matching. <code><pre>
100 * SetPropertiesRule(
101 * new String[] {"class", "ignore-me"},
102 * new String[] {"className"});
103 * </pre></code>
104 *
105 * @param attributeNames names of attributes to map
106 * @param propertyNames names of properties mapped to
107 */
108 public SetPropertiesRule( String[] attributeNames, String[] propertyNames )
109 {
110 for ( int i = 0, size = attributeNames.length; i < size; i++ )
111 {
112 String propName = null;
113 if ( i < propertyNames.length )
114 {
115 propName = propertyNames[i];
116 }
117
118 aliases.put( attributeNames[i], propName );
119 }
120 }
121
122 /**
123 * Constructor allows attribute->property mapping to be overriden.
124 *
125 * @param aliases attribute->property mapping
126 * @since 3.0
127 */
128 public SetPropertiesRule( Map<String, String> aliases )
129 {
130 if ( aliases != null && !aliases.isEmpty() )
131 {
132 this.aliases.putAll( aliases );
133 }
134 }
135
136 // ----------------------------------------------------- Instance Variables
137
138 private final Map<String, String> aliases = new HashMap<String, String>();
139
140 /**
141 * Used to determine whether the parsing should fail if an property specified in the XML is missing from the bean.
142 * Default is true for backward compatibility.
143 */
144 private boolean ignoreMissingProperty = true;
145
146 // --------------------------------------------------------- Public Methods
147
148 /**
149 * {@inheritDoc}
150 */
151 @Override
152 public void begin( String namespace, String name, Attributes attributes )
153 throws Exception
154 {
155 // Build a set of attribute names and corresponding values
156 Map<String, String> values = new HashMap<String, String>();
157
158 for ( int i = 0; i < attributes.getLength(); i++ )
159 {
160 String attributeName = attributes.getLocalName( i );
161 if ( "".equals( attributeName ) )
162 {
163 attributeName = attributes.getQName( i );
164 }
165 String value = attributes.getValue( i );
166
167 // alias lookup has complexity O(1)
168 if ( aliases.containsKey( attributeName ) )
169 {
170 attributeName = aliases.get( attributeName );
171 }
172
173 if ( getDigester().getLogger().isDebugEnabled() )
174 {
175 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Setting property '%s' to '%s'",
176 getDigester().getMatch(),
177 attributeName,
178 attributeName ) );
179 }
180
181 if ( ( !ignoreMissingProperty ) && ( attributeName != null ) )
182 {
183 // The BeanUtils.populate method silently ignores items in
184 // the map (ie xml entities) which have no corresponding
185 // setter method, so here we check whether each xml attribute
186 // does have a corresponding property before calling the
187 // BeanUtils.populate method.
188 //
189 // Yes having the test and set as separate steps is ugly and
190 // inefficient. But BeanUtils.populate doesn't provide the
191 // functionality we need here, and changing the algorithm which
192 // determines the appropriate setter method to invoke is
193 // considered too risky.
194 //
195 // Using two different classes (PropertyUtils vs BeanUtils) to
196 // do the test and the set is also ugly; the codepaths
197 // are different which could potentially lead to trouble.
198 // However the BeanUtils/ProperyUtils code has been carefully
199 // compared and the PropertyUtils functionality does appear
200 // compatible so we'll accept the risk here.
201
202 Object top = getDigester().peek();
203 boolean test = isWriteable( top, attributeName );
204 if ( !test )
205 {
206 throw new NoSuchMethodException( "Property " + attributeName + " can't be set" );
207 }
208 }
209
210 if ( attributeName != null )
211 {
212 values.put( attributeName, value );
213 }
214 }
215
216 // Populate the corresponding properties of the top object
217 Object top = getDigester().peek();
218 if ( getDigester().getLogger().isDebugEnabled() )
219 {
220 if ( top != null )
221 {
222 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Set '%s' properties",
223 getDigester().getMatch(),
224 top.getClass().getName() ) );
225 }
226 else
227 {
228 getDigester().getLogger().debug( format( "[SetPropertiesRule]{%s} Set NULL properties",
229 getDigester().getMatch() ) );
230 }
231 }
232 populate( top, values );
233 }
234
235 /**
236 * Add an additional attribute name to property name mapping. This is intended to be used from the xml rules.
237 *
238 * @param attributeName the attribute name has to be mapped
239 * @param propertyName the target property name
240 */
241 public void addAlias( String attributeName, String propertyName )
242 {
243 aliases.put( attributeName, propertyName );
244 }
245
246 /**
247 * {@inheritDoc}
248 */
249 @Override
250 public String toString()
251 {
252 return format( "SetPropertiesRule[aliases=%s, ignoreMissingProperty=%s]", aliases, ignoreMissingProperty );
253 }
254
255 /**
256 * <p>
257 * Are attributes found in the xml without matching properties to be ignored?
258 * </p>
259 * <p>
260 * If false, the parsing will interrupt with an <code>NoSuchMethodException</code> if a property specified in the
261 * XML is not found. The default is true.
262 * </p>
263 *
264 * @return true if skipping the unmatched attributes.
265 */
266 public boolean isIgnoreMissingProperty()
267 {
268 return this.ignoreMissingProperty;
269 }
270
271 /**
272 * Sets whether attributes found in the xml without matching properties should be ignored. If set to false, the
273 * parsing will throw an <code>NoSuchMethodException</code> if an unmatched attribute is found. This allows to trap
274 * misspellings in the XML file.
275 *
276 * @param ignoreMissingProperty false to stop the parsing on unmatched attributes.
277 */
278 public void setIgnoreMissingProperty( boolean ignoreMissingProperty )
279 {
280 this.ignoreMissingProperty = ignoreMissingProperty;
281 }
282
283 }