org.apache.commons.digester3.SetPropertiesRule.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.commons.digester3.SetPropertiesRule.java

Source

package org.apache.commons.digester3;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import static java.lang.String.format;
import static org.apache.commons.beanutils.BeanUtils.populate;
import static org.apache.commons.beanutils.PropertyUtils.isWriteable;

import java.util.HashMap;
import java.util.Map;

import org.xml.sax.Attributes;

/**
 * <p>
 * Rule implementation that sets properties on the object at the top of the stack, based on attributes with
 * corresponding names.
 * </p>
 * <p>
 * This rule supports custom mapping of attribute names to property names. The default mapping for particular attributes
 * can be overridden by using {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}. This allows
 * attributes to be mapped to properties with different names. Certain attributes can also be marked to be ignored.
 * </p>
 */
public class SetPropertiesRule extends Rule {

    // ----------------------------------------------------------- Constructors

    /**
     * Base constructor.
     */
    public SetPropertiesRule() {
        // nothing to set up
    }

    /**
     * <p>
     * Convenience constructor overrides the mapping for just one property.
     * </p>
     * <p>
     * For details about how this works, see {@link #SetPropertiesRule(String[] attributeNames, String[] propertyNames)}
     * .
     * </p>
     *
     * @param attributeName map this attribute
     * @param propertyName to a property with this name
     */
    public SetPropertiesRule(String attributeName, String propertyName) {
        aliases.put(attributeName, propertyName);
    }

    /**
     * <p>
     * Constructor allows attribute->property mapping to be overriden.
     * </p>
     * <p>
     * Two arrays are passed in. One contains the attribute names and the other the property names. The attribute name /
     * property name pairs are match by position In order words, the first string in the attribute name list matches to
     * the first string in the property name list and so on.
     * </p>
     * <p>
     * If a property name is null or the attribute name has no matching property name, then this indicates that the
     * attibute should be ignored.
     * </p>
     * <h5>Example One</h5>
     * <p>
     * The following constructs a rule that maps the <code>alt-city</code> attribute to the <code>city</code> property
     * and the <code>alt-state</code> to the <code>state</code> property. All other attributes are mapped as usual using
     * exact name matching. <code><pre>
     *      SetPropertiesRule(
     *                new String[] {"alt-city", "alt-state"},
     *                new String[] {"city", "state"});
     * </pre></code>
     * <h5>Example Two</h5>
     * <p>
     * The following constructs a rule that maps the <code>class</code> attribute to the <code>className</code>
     * property. The attribute <code>ignore-me</code> is not mapped. All other attributes are mapped as usual using
     * exact name matching. <code><pre>
     *      SetPropertiesRule(
     *                new String[] {"class", "ignore-me"},
     *                new String[] {"className"});
     * </pre></code>
     *
     * @param attributeNames names of attributes to map
     * @param propertyNames names of properties mapped to
     */
    public SetPropertiesRule(String[] attributeNames, String[] propertyNames) {
        for (int i = 0, size = attributeNames.length; i < size; i++) {
            String propName = null;
            if (i < propertyNames.length) {
                propName = propertyNames[i];
            }

            aliases.put(attributeNames[i], propName);
        }
    }

    /**
     * Constructor allows attribute->property mapping to be overriden.
     *
     * @param aliases attribute->property mapping
     * @since 3.0
     */
    public SetPropertiesRule(Map<String, String> aliases) {
        if (aliases != null && !aliases.isEmpty()) {
            this.aliases.putAll(aliases);
        }
    }

    // ----------------------------------------------------- Instance Variables

    private final Map<String, String> aliases = new HashMap<String, String>();

    /**
     * Used to determine whether the parsing should fail if an property specified in the XML is missing from the bean.
     * Default is true for backward compatibility.
     */
    private boolean ignoreMissingProperty = true;

    // --------------------------------------------------------- Public Methods

    /**
     * {@inheritDoc}
     */
    @Override
    public void begin(String namespace, String name, Attributes attributes) throws Exception {
        // Build a set of attribute names and corresponding values
        Map<String, String> values = new HashMap<String, String>();

        for (int i = 0; i < attributes.getLength(); i++) {
            String attributeName = attributes.getLocalName(i);
            if ("".equals(attributeName)) {
                attributeName = attributes.getQName(i);
            }
            String value = attributes.getValue(i);

            // alias lookup has complexity O(1)
            if (aliases.containsKey(attributeName)) {
                attributeName = aliases.get(attributeName);
            }

            if (getDigester().getLogger().isDebugEnabled()) {
                getDigester().getLogger().debug(format("[SetPropertiesRule]{%s} Setting property '%s' to '%s'",
                        getDigester().getMatch(), attributeName, attributeName));
            }

            if ((!ignoreMissingProperty) && (attributeName != null)) {
                // The BeanUtils.populate method silently ignores items in
                // the map (ie xml entities) which have no corresponding
                // setter method, so here we check whether each xml attribute
                // does have a corresponding property before calling the
                // BeanUtils.populate method.
                //
                // Yes having the test and set as separate steps is ugly and
                // inefficient. But BeanUtils.populate doesn't provide the
                // functionality we need here, and changing the algorithm which
                // determines the appropriate setter method to invoke is
                // considered too risky.
                //
                // Using two different classes (PropertyUtils vs BeanUtils) to
                // do the test and the set is also ugly; the codepaths
                // are different which could potentially lead to trouble.
                // However the BeanUtils/ProperyUtils code has been carefully
                // compared and the PropertyUtils functionality does appear
                // compatible so we'll accept the risk here.

                Object top = getDigester().peek();
                boolean test = isWriteable(top, attributeName);
                if (!test) {
                    throw new NoSuchMethodException("Property " + attributeName + " can't be set");
                }
            }

            if (attributeName != null) {
                values.put(attributeName, value);
            }
        }

        // Populate the corresponding properties of the top object
        Object top = getDigester().peek();
        if (getDigester().getLogger().isDebugEnabled()) {
            if (top != null) {
                getDigester().getLogger().debug(format("[SetPropertiesRule]{%s} Set '%s' properties",
                        getDigester().getMatch(), top.getClass().getName()));
            } else {
                getDigester().getLogger()
                        .debug(format("[SetPropertiesRule]{%s} Set NULL properties", getDigester().getMatch()));
            }
        }
        populate(top, values);
    }

    /**
     * Add an additional attribute name to property name mapping. This is intended to be used from the xml rules.
     *
     * @param attributeName the attribute name has to be mapped
     * @param propertyName the target property name
     */
    public void addAlias(String attributeName, String propertyName) {
        aliases.put(attributeName, propertyName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return format("SetPropertiesRule[aliases=%s, ignoreMissingProperty=%s]", aliases, ignoreMissingProperty);
    }

    /**
     * <p>
     * Are attributes found in the xml without matching properties to be ignored?
     * </p>
     * <p>
     * If false, the parsing will interrupt with an <code>NoSuchMethodException</code> if a property specified in the
     * XML is not found. The default is true.
     * </p>
     *
     * @return true if skipping the unmatched attributes.
     */
    public boolean isIgnoreMissingProperty() {
        return this.ignoreMissingProperty;
    }

    /**
     * Sets whether attributes found in the xml without matching properties should be ignored. If set to false, the
     * parsing will throw an <code>NoSuchMethodException</code> if an unmatched attribute is found. This allows to trap
     * misspellings in the XML file.
     *
     * @param ignoreMissingProperty false to stop the parsing on unmatched attributes.
     */
    public void setIgnoreMissingProperty(boolean ignoreMissingProperty) {
        this.ignoreMissingProperty = ignoreMissingProperty;
    }

}