View Javadoc

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 }