001package 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
022import static java.lang.String.format;
023import static org.apache.commons.beanutils.BeanUtils.populate;
024import static org.apache.commons.beanutils.PropertyUtils.isWriteable;
025
026import java.util.HashMap;
027import java.util.Map;
028
029import 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 */
042public 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}