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    }