ValidatorResources.java

/*
 * 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.
 */
package org.apache.commons.validator;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.collections.FastHashMap; // DEPRECATED
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * <p>
 * General purpose class for storing <code>FormSet</code> objects based
 * on their associated <code>Locale</code>.  Instances of this class are usually
 * configured through a validation.xml file that is parsed in a constructor.
 * </p>
 *
 * <p><strong>Note</strong> - Classes that extend this class
 * must be Serializable so that instances may be used in distributable
 * application server environments.</p>
 *
 * <p>
 * The use of FastHashMap is deprecated and will be replaced in a future
 * release.
 * </p>
 */
//TODO mutable non-private fields
public class ValidatorResources implements Serializable {

    private static final long serialVersionUID = -8203745881446239554L;

    /** Name of the digester validator rules file */
    private static final String VALIDATOR_RULES = "digester-rules.xml";

    /**
     * The set of public identifiers, and corresponding resource names, for
     * the versions of the configuration file DTDs that we know about.  There
     * <strong>MUST</strong> be an even number of Strings in this list!
     */
    private static final String[] REGISTRATIONS = {
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
        "/org/apache/commons/validator/resources/validator_1_0.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
        "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
        "/org/apache/commons/validator/resources/validator_1_1.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
        "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
        "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
        "/org/apache/commons/validator/resources/validator_1_3_0.dtd",
        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
        "/org/apache/commons/validator/resources/validator_1_4_0.dtd"
    };

    /**
     * The default locale on our server.
     */
    protected static Locale defaultLocale = Locale.getDefault();

    private static final String ARGS_PATTERN
               = "form-validation/formset/form/field/arg";

    private transient Log log = LogFactory.getLog(ValidatorResources.class);

    /**
     * <code>Map</code> of <code>FormSet</code>s stored under
     * a <code>Locale</code> key (expressed as a String).
     * @deprecated Subclasses should use getFormSets() instead.
     */
    @Deprecated
    protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet>

    /**
     * <code>Map</code> of global constant values with
     * the name of the constant as the key.
     * @deprecated Subclasses should use getConstants() instead.
     */
    @Deprecated
    protected FastHashMap hConstants = new FastHashMap(); // <String, String>

    /**
     * <code>Map</code> of <code>ValidatorAction</code>s with
     * the name of the <code>ValidatorAction</code> as the key.
     * @deprecated Subclasses should use getActions() instead.
     */
    @Deprecated
    protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction>

    /**
     * This is the default <code>FormSet</code> (without locale). (We probably don't need
     * the defaultLocale anymore.)
     */
    protected FormSet defaultFormSet;

    /**
     * Create an empty ValidatorResources object.
     */
    public ValidatorResources() {
    }

    /**
     * Create a ValidatorResources object from an InputStream.
     *
     * @param in InputStream to a validation.xml configuration file.  It's the client's
     * responsibility to close this stream.
     * @throws SAXException if the validation XML files are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.1
     */
    public ValidatorResources(final InputStream in) throws IOException, SAXException {
        this(new InputStream[]{in});
    }

    /**
     * Create a ValidatorResources object from an InputStream.
     *
     * @param streams An array of InputStreams to several validation.xml
     * configuration files that will be read in order and merged into this object.
     * It's the client's responsibility to close these streams.
     * @throws SAXException if the validation XML files are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.1
     */
    public ValidatorResources(final InputStream[] streams)
            throws IOException, SAXException {

        final Digester digester = initDigester();
        for (int i = 0; i < streams.length; i++) {
            if (streams[i] == null) {
                throw new IllegalArgumentException("Stream[" + i + "] is null");
            }
            digester.push(this);
            digester.parse(streams[i]);
        }

        this.process();
    }

    /**
     * Create a ValidatorResources object from an uri
     *
     * @param uri The location of a validation.xml configuration file.
     * @throws SAXException if the validation XML files are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.2
     */
    public ValidatorResources(final String uri) throws IOException, SAXException {
        this(new String[] { uri });
    }

    /**
     * Create a ValidatorResources object from several uris
     *
     * @param uris An array of uris to several validation.xml
     * configuration files that will be read in order and merged into this object.
     * @throws SAXException if the validation XML files are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.2
     */
    public ValidatorResources(final String... uris)
            throws IOException, SAXException {

        final Digester digester = initDigester();
        for (final String element : uris) {
            digester.push(this);
            digester.parse(element);
        }

        this.process();
    }

    /**
     * Create a ValidatorResources object from a URL.
     *
     * @param url The URL for the validation.xml
     * configuration file that will be read into this object.
     * @throws SAXException if the validation XML file are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.3.1
     */
    public ValidatorResources(final URL url)
            throws IOException, SAXException {
        this(new URL[]{url});
    }

    /**
     * Create a ValidatorResources object from several URL.
     *
     * @param urls An array of URL to several validation.xml
     * configuration files that will be read in order and merged into this object.
     * @throws SAXException if the validation XML files are not valid or well
     * formed.
     * @throws IOException if an I/O error occurs processing the XML files
     * @since 1.3.1
     */
    public ValidatorResources(final URL[] urls)
            throws IOException, SAXException {

        final Digester digester = initDigester();
        for (final URL url : urls) {
            digester.push(this);
            digester.parse(url);
        }

        this.process();
    }

    /**
     * Add a global constant to the resource.
     * @param name The constant name.
     * @param value The constant value.
     */
    public void addConstant(final String name, final String value) {
        if (getLog().isDebugEnabled()) {
            getLog().debug("Adding Global Constant: " + name + "," + value);
        }

        this.hConstants.put(name, value);
    }

    /**
     * Add a <code>FormSet</code> to this <code>ValidatorResources</code>
     * object.  It will be associated with the <code>Locale</code> of the
     * <code>FormSet</code>.
     * @param fs The form set to add.
     * @since 1.1
     */
    public void addFormSet(final FormSet fs) {
        final String key = this.buildKey(fs);
        if (key.isEmpty()) { // there can only be one default formset
            if (getLog().isWarnEnabled() && defaultFormSet != null) {
                // warn the user he might not get the expected results
                getLog().warn("Overriding default FormSet definition.");
            }
            defaultFormSet = fs;
        } else {
            final FormSet formset = getFormSets().get(key);
            if (formset == null) { // it hasn't been included yet
                if (getLog().isDebugEnabled()) {
                    getLog().debug("Adding FormSet '" + fs + "'.");
                }
            } else if (getLog().isWarnEnabled()) { // warn the user he might not
                                                   // get the expected results
                getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
            }
            getFormSets().put(key, fs);
        }
    }

    /**
     * Create a <code>Rule</code> to handle <code>arg0-arg3</code>
     * elements. This will allow validation.xml files that use the
     * versions of the DTD prior to Validator 1.2.0 to continue
     * working.
     */
    private void addOldArgRules(final Digester digester) {
        // Create a new rule to process args elements
        final Rule rule = new Rule() {
            @Override
            public void begin(final String namespace, final String name, final Attributes attributes) {
                // Create the Arg
                final Arg arg = new Arg();
                arg.setKey(attributes.getValue("key"));
                arg.setName(attributes.getValue("name"));
                if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
                    arg.setResource(false);
                }
                try {
                    final int length = "arg".length(); // skip the arg prefix
                    arg.setPosition(Integer.parseInt(name.substring(length)));
                } catch (final Exception ex) {
                    getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
                }

                // Add the arg to the parent field
                ((Field) getDigester().peek(0)).addArg(arg);
            }
        };

        // Add the rule for each of the arg elements
        digester.addRule(ARGS_PATTERN + "0", rule);
        digester.addRule(ARGS_PATTERN + "1", rule);
        digester.addRule(ARGS_PATTERN + "2", rule);
        digester.addRule(ARGS_PATTERN + "3", rule);

    }

    /**
     * Add a <code>ValidatorAction</code> to the resource.  It also creates an
     * instance of the class based on the <code>ValidatorAction</code>s
     * class name and retrieves the <code>Method</code> instance and sets them
     * in the <code>ValidatorAction</code>.
     * @param va The validator action.
     */
    public void addValidatorAction(final ValidatorAction va) {
        va.init();

        getActions().put(va.getName(), va);

        if (getLog().isDebugEnabled()) {
            getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
        }
    }

    /**
     * Builds a key to store the <code>FormSet</code> under based on it's
     * language, country, and variant values.
     * @param fs The Form Set.
     * @return generated key for a formset.
     */
    protected String buildKey(final FormSet fs) {
        return
                this.buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
    }

    /**
     * Assembles a Locale code from the given parts.
     */
    private String buildLocale(final String lang, final String country, final String variant) {
        final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : "");
        key.append(country != null && !country.isEmpty() ? "_" + country : "");
        key.append(variant != null && !variant.isEmpty() ? "_" + variant : "");
        return key.toString();
    }

    /**
     * Returns a Map of String ValidatorAction names to their ValidatorAction.
     * @return Map of Validator Actions
     * @since 1.2.0
     */
    @SuppressWarnings("unchecked") // FastHashMap is not generic
    protected Map<String, ValidatorAction> getActions() {
        return hActions;
    }

    /**
     * Returns a Map of String constant names to their String values.
     * @return Map of Constants
     * @since 1.2.0
     */
    @SuppressWarnings("unchecked") // FastHashMap is not generic
    protected Map<String, String> getConstants() {
        return hConstants;
    }

    /**
     * <p>Gets a <code>Form</code> based on the name of the form and the
     * <code>Locale</code> that most closely matches the <code>Locale</code>
     * passed in.  The order of <code>Locale</code> matching is:</p>
     * <ol>
     *    <li>language + country + variant</li>
     *    <li>language + country</li>
     *    <li>language</li>
     *    <li>default locale</li>
     * </ol>
     * @param locale The Locale.
     * @param formKey The key for the Form.
     * @return The validator Form.
     * @since 1.1
     */
    public Form getForm(final Locale locale, final String formKey) {
        return this.getForm(locale.getLanguage(), locale.getCountry(), locale
                .getVariant(), formKey);
    }

    /**
     * <p>Gets a <code>Form</code> based on the name of the form and the
     * <code>Locale</code> that most closely matches the <code>Locale</code>
     * passed in.  The order of <code>Locale</code> matching is:</p>
     * <ol>
     *    <li>language + country + variant</li>
     *    <li>language + country</li>
     *    <li>language</li>
     *    <li>default locale</li>
     * </ol>
     * @param language The locale's language.
     * @param country The locale's country.
     * @param variant The locale's language variant.
     * @param formKey The key for the Form.
     * @return The validator Form.
     * @since 1.1
     */
    public Form getForm(final String language, final String country, final String variant, final String formKey) {

        Form form = null;

        // Try language/country/variant
        String key = this.buildLocale(language, country, variant);
        if (!key.isEmpty()) {
            final FormSet formSet = getFormSets().get(key);
            if (formSet != null) {
                form = formSet.getForm(formKey);
            }
        }
        final String localeKey = key;

        // Try language/country
        if (form == null) {
            key = buildLocale(language, country, null);
            if (!key.isEmpty()) {
                final FormSet formSet = getFormSets().get(key);
                if (formSet != null) {
                    form = formSet.getForm(formKey);
                }
            }
        }

        // Try language
        if (form == null) {
            key = buildLocale(language, null, null);
            if (!key.isEmpty()) {
                final FormSet formSet = getFormSets().get(key);
                if (formSet != null) {
                    form = formSet.getForm(formKey);
                }
            }
        }

        // Try default formset
        if (form == null) {
            form = defaultFormSet.getForm(formKey);
            key = "default";
        }

        if (form == null) {
            if (getLog().isWarnEnabled()) {
                getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
            }
        } else if (getLog().isDebugEnabled()) {
            getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
        }

        return form;

    }

    /**
     * <p>Gets a <code>FormSet</code> based on the language, country
     *    and variant.</p>
     * @param language The locale's language.
     * @param country The locale's country.
     * @param variant The locale's language variant.
     * @return The FormSet for a locale.
     * @since 1.2
     */
    FormSet getFormSet(final String language, final String country, final String variant) {
        final String key = buildLocale(language, country, variant);
        if (key.isEmpty()) {
            return defaultFormSet;
        }
        return getFormSets().get(key);
    }

    /**
     * Returns a Map of String locale keys to Lists of their FormSets.
     * @return Map of Form sets
     * @since 1.2.0
     */
    @SuppressWarnings("unchecked") // FastHashMap is not generic
    protected Map<String, FormSet> getFormSets() {
        return hFormSets;
    }

    /**
     * Accessor method for Log instance.
     *
     * The Log instance variable is transient and
     * accessing it through this method ensures it
     * is re-initialized when this instance is
     * de-serialized.
     *
     * @return The Log instance.
     */
    private Log getLog() {
        if (log == null) {
            log = LogFactory.getLog(ValidatorResources.class);
        }
        return log;
    }

    /**
     * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
     * has a direct parent in the formSet with locale en_UK. If it doesn't
     * exist, find the formSet with locale en, if no found get the
     * defaultFormSet.
     *
     * @param fs
     *            the formSet we want to get the parent from
     * @return fs's parent
     */
    private FormSet getParent(final FormSet fs) {

        FormSet parent = null;
        if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
            parent = defaultFormSet;
        } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
            parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
            if (parent == null) {
                parent = defaultFormSet;
            }
        } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
            parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
            if (parent == null) {
                parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
                if (parent == null) {
                    parent = defaultFormSet;
                }
            }
        }
        return parent;
    }

    /**
     * Gets a <code>ValidatorAction</code> based on it's name.
     * @param key The validator action key.
     * @return The validator action.
     */
    public ValidatorAction getValidatorAction(final String key) {
        return getActions().get(key);
    }

    /**
     * Gets an unmodifiable <code>Map</code> of the <code>ValidatorAction</code>s.
     * @return Map of validator actions.
     */
    public Map<String, ValidatorAction> getValidatorActions() {
        return Collections.unmodifiableMap(getActions());
    }

    /**
     *  Initialize the digester.
     */
    private Digester initDigester() {
        URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
        if (rulesUrl == null) {
            // Fix for Issue# VALIDATOR-195
            rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
        }
        if (getLog().isDebugEnabled()) {
            getLog().debug("Loading rules from '" + rulesUrl + "'");
        }
        final Digester digester = DigesterLoader.createDigester(rulesUrl);
        digester.setNamespaceAware(true);
        digester.setValidating(true);
        digester.setUseContextClassLoader(true);

        // Add rules for arg0-arg3 elements
        addOldArgRules(digester);

        // register DTDs
        for (int i = 0; i < REGISTRATIONS.length; i += 2) {
            final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
            if (url != null) {
                digester.register(REGISTRATIONS[i], url.toString());
            }
        }
        return digester;
    }

    /**
     * Process the <code>ValidatorResources</code> object. Currently sets the
     * <code>FastHashMap</code> s to the 'fast' mode and call the processes
     * all other resources. <strong>Note </strong>: The framework calls this
     * automatically when ValidatorResources is created from an XML file. If you
     * create an instance of this class by hand you <strong>must </strong> call
     * this method when finished.
     */
    public void process() {
        hFormSets.setFast(true);
        hConstants.setFast(true);
        hActions.setFast(true);

        this.processForms();
    }

    /**
     * <p>Process the <code>Form</code> objects.  This clones the <code>Field</code>s
     * that don't exist in a <code>FormSet</code> compared to its parent
     * <code>FormSet</code>.</p>
     */
    private void processForms() {
        if (defaultFormSet == null) { // it isn't mandatory to have a
            // default formset
            defaultFormSet = new FormSet();
        }
        defaultFormSet.process(getConstants());
        // Loop through FormSets and merge if necessary
        for (final String key : getFormSets().keySet()) {
            final FormSet fs = getFormSets().get(key);
            fs.merge(getParent(fs));
        }

        // Process Fully Constructed FormSets
        for (final FormSet fs : getFormSets().values()) {
            if (!fs.isProcessed()) {
                fs.process(getConstants());
            }
        }
    }

}