001 package org.apache.commons.digester3;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements. See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership. The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License. You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied. See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022 import static java.lang.String.format;
023 import static org.apache.commons.beanutils.BeanUtils.populate;
024 import static org.apache.commons.beanutils.PropertyUtils.isWriteable;
025
026 import java.util.HashMap;
027 import java.util.Map;
028
029 import org.xml.sax.Attributes;
030
031 /**
032 * <p>
033 * Rule implementation that sets properties on the object at the top of the stack, based on attributes with
034 * corresponding names.
035 * </p>
036 * <p>
037 * This rule supports custom mapping of attribute names to property names. The default mapping for particular attributes
038 * can be overridden by using {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. This allows
039 * attributes to be mapped to properties with different names. Certain attributes can also be marked to be ignored.
040 * </p>
041 */
042 public class SetPropertiesRule
043 extends Rule
044 {
045
046 // ----------------------------------------------------------- Constructors
047
048 /**
049 * Base constructor.
050 */
051 public SetPropertiesRule()
052 {
053 // nothing to set up
054 }
055
056 /**
057 * <p>
058 * Convenience constructor overrides the mapping for just one property.
059 * </p>
060 * <p>
061 * For details about how this works, see {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}
062 * .
063 * </p>
064 *
065 * @param attributeName map this attribute
066 * @param propertyName to a property with this name
067 */
068 public SetPropertiesRule( String attributeName, String propertyName )
069 {
070 aliases.put( attributeName, propertyName );
071 }
072
073 /**
074 * <p>
075 * Constructor allows attribute->property mapping to be overriden.
076 * </p>
077 * <p>
078 * Two arrays are passed in. One contains the attribute names and the other the property names. The attribute name /
079 * property name pairs are match by position In order words, the first string in the attribute name list matches to
080 * the first string in the property name list and so on.
081 * </p>
082 * <p>
083 * If a property name is null or the attribute name has no matching property name, then this indicates that the
084 * attibute should be ignored.
085 * </p>
086 * <h5>Example One</h5>
087 * <p>
088 * The following constructs a rule that maps the <code>alt-city</code> attribute to the <code>city</code> property
089 * and the <code>alt-state</code> to the <code>state</code> property. All other attributes are mapped as usual using
090 * exact name matching. <code><pre>
091 * SetPropertiesRule(
092 * new String[] {"alt-city", "alt-state"},
093 * new String[] {"city", "state"});
094 * </pre></code>
095 * <h5>Example Two</h5>
096 * <p>
097 * The following constructs a rule that maps the <code>class</code> attribute to the <code>className</code>
098 * property. The attribute <code>ignore-me</code> is not mapped. All other attributes are mapped as usual using
099 * 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 }