    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.validator;
    018
    019import java.io.IOException;
    020import java.io.InputStream;
    021import java.io.Serializable;
    022import java.net.URL;
    023import java.util.Collections;
    024import java.util.Locale;
    025import java.util.Map;
    026
    027import org.apache.commons.collections.FastHashMap; // DEPRECATED
    028import org.apache.commons.digester.Digester;
    029import org.apache.commons.digester.Rule;
    030import org.apache.commons.digester.xmlrules.DigesterLoader;
    031import org.apache.commons.logging.Log;
    032import org.apache.commons.logging.LogFactory;
    033import org.xml.sax.Attributes;
    034import org.xml.sax.SAXException;
    035
    036/**
    037 * <p>
    038 * General purpose class for storing <code>FormSet</code> objects based
    039 * on their associated <code>Locale</code>.  Instances of this class are usually
    040 * configured through a validation.xml file that is parsed in a constructor.
    041 * </p>
    042 *
    043 * <p><strong>Note</strong> - Classes that extend this class
    044 * must be Serializable so that instances may be used in distributable
    045 * application server environments.</p>
    046 *
    047 * <p>
    048 * The use of FastHashMap is deprecated and will be replaced in a future
    049 * release.
    050 * </p>
    051 */
    052//TODO mutable non-private fields
    053public class ValidatorResources implements Serializable {
    054
    055    private static final long serialVersionUID = -8203745881446239554L;
    056
    057    /** Name of the digester validator rules file */
    058    private static final String VALIDATOR_RULES = "digester-rules.xml";
    059
    060    /**
    061     * The set of public identifiers, and corresponding resource names, for
    062     * the versions of the configuration file DTDs that we know about.  There
    063     * <strong>MUST</strong> be an even number of Strings in this list!
    064     */
    065    private static final String[] REGISTRATIONS = {
    066        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
    067        "/org/apache/commons/validator/resources/validator_1_0.dtd",
    068        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
    069        "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
    070        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
    071        "/org/apache/commons/validator/resources/validator_1_1.dtd",
    072        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
    073        "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
    074        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
    075        "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
    076        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
    077        "/org/apache/commons/validator/resources/validator_1_3_0.dtd",
    078        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
    079        "/org/apache/commons/validator/resources/validator_1_4_0.dtd"
    080    };
    081
    082    /**
    083     * The default locale on our server.
    084     */
    085    protected static Locale defaultLocale = Locale.getDefault();
    086
    087    private static final String ARGS_PATTERN
    088               = "form-validation/formset/form/field/arg";
    089
    090    private transient Log log = LogFactory.getLog(ValidatorResources.class);
    091
    092    /**
    093     * <code>Map</code> of <code>FormSet</code>s stored under
    094     * a <code>Locale</code> key (expressed as a String).
    095     * @deprecated Subclasses should use getFormSets() instead.
    096     */
    097    @Deprecated
    098    protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet>
    099
    100    /**
    101     * <code>Map</code> of global constant values with
    102     * the name of the constant as the key.
    103     * @deprecated Subclasses should use getConstants() instead.
    104     */
    105    @Deprecated
    106    protected FastHashMap hConstants = new FastHashMap(); // <String, String>
    107
    108    /**
    109     * <code>Map</code> of <code>ValidatorAction</code>s with
    110     * the name of the <code>ValidatorAction</code> as the key.
    111     * @deprecated Subclasses should use getActions() instead.
    112     */
    113    @Deprecated
    114    protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction>
    115
    116    /**
    117     * This is the default <code>FormSet</code> (without locale). (We probably don't need
    118     * the defaultLocale anymore.)
    119     */
    120    protected FormSet defaultFormSet;
    121
    122    /**
    123     * Create an empty ValidatorResources object.
    124     */
    125    public ValidatorResources() {
    126    }
    127
    128    /**
    129     * Create a ValidatorResources object from an InputStream.
    130     *
    131     * @param in InputStream to a validation.xml configuration file.  It's the client's
    132     * responsibility to close this stream.
    133     * @throws SAXException if the validation XML files are not valid or well
    134     * formed.
    135     * @throws IOException if an I/O error occurs processing the XML files
    136     * @since 1.1
    137     */
    138    public ValidatorResources(final InputStream in) throws IOException, SAXException {
    139        this(new InputStream[]{in});
    140    }
    141
    142    /**
    143     * Create a ValidatorResources object from an InputStream.
    144     *
    145     * @param streams An array of InputStreams to several validation.xml
    146     * configuration files that will be read in order and merged into this object.
    147     * It's the client's responsibility to close these streams.
    148     * @throws SAXException if the validation XML files are not valid or well
    149     * formed.
    150     * @throws IOException if an I/O error occurs processing the XML files
    151     * @since 1.1
    152     */
    153    public ValidatorResources(final InputStream[] streams)
    154            throws IOException, SAXException {
    155
    156        final Digester digester = initDigester();
    157        for (int i = 0; i < streams.length; i++) {
    158            if (streams[i] == null) {
    159                throw new IllegalArgumentException("Stream[" + i + "] is null");
    160            }
    161            digester.push(this);
    162            digester.parse(streams[i]);
    163        }
    164
    165        this.process();
    166    }
    167
    168    /**
    169     * Create a ValidatorResources object from an uri
    170     *
    171     * @param uri The location of a validation.xml configuration file.
    172     * @throws SAXException if the validation XML files are not valid or well
    173     * formed.
    174     * @throws IOException if an I/O error occurs processing the XML files
    175     * @since 1.2
    176     */
    177    public ValidatorResources(final String uri) throws IOException, SAXException {
    178        this(new String[] { uri });
    179    }
    180
    181    /**
    182     * Create a ValidatorResources object from several uris
    183     *
    184     * @param uris An array of uris to several validation.xml
    185     * configuration files that will be read in order and merged into this object.
    186     * @throws SAXException if the validation XML files are not valid or well
    187     * formed.
    188     * @throws IOException if an I/O error occurs processing the XML files
    189     * @since 1.2
    190     */
    191    public ValidatorResources(final String... uris)
    192            throws IOException, SAXException {
    193
    194        final Digester digester = initDigester();
    195        for (final String element : uris) {
    196            digester.push(this);
    197            digester.parse(element);
    198        }
    199
    200        this.process();
    201    }
    202
    203    /**
    204     * Create a ValidatorResources object from a URL.
    205     *
    206     * @param url The URL for the validation.xml
    207     * configuration file that will be read into this object.
    208     * @throws SAXException if the validation XML file are not valid or well
    209     * formed.
    210     * @throws IOException if an I/O error occurs processing the XML files
    211     * @since 1.3.1
    212     */
    213    public ValidatorResources(final URL url)
    214            throws IOException, SAXException {
    215        this(new URL[]{url});
    216    }
    217
    218    /**
    219     * Create a ValidatorResources object from several URL.
    220     *
    221     * @param urls An array of URL to several validation.xml
    222     * configuration files that will be read in order and merged into this object.
    223     * @throws SAXException if the validation XML files are not valid or well
    224     * formed.
    225     * @throws IOException if an I/O error occurs processing the XML files
    226     * @since 1.3.1
    227     */
    228    public ValidatorResources(final URL[] urls)
    229            throws IOException, SAXException {
    230
    231        final Digester digester = initDigester();
    232        for (final URL url : urls) {
    233            digester.push(this);
    234            digester.parse(url);
    235        }
    236
    237        this.process();
    238    }
    239
    240    /**
    241     * Add a global constant to the resource.
    242     * @param name The constant name.
    243     * @param value The constant value.
    244     */
    245    public void addConstant(final String name, final String value) {
    246        if (getLog().isDebugEnabled()) {
    247            getLog().debug("Adding Global Constant: " + name + "," + value);
    248        }
    249
    250        this.hConstants.put(name, value);
    251    }
    252
    253    /**
    254     * Add a <code>FormSet</code> to this <code>ValidatorResources</code>
    255     * object.  It will be associated with the <code>Locale</code> of the
    256     * <code>FormSet</code>.
    257     * @param fs The form set to add.
    258     * @since 1.1
    259     */
    260    public void addFormSet(final FormSet fs) {
    261        final String key = this.buildKey(fs);
    262        if (key.isEmpty()) { // there can only be one default formset
    263            if (getLog().isWarnEnabled() && defaultFormSet != null) {
    264                // warn the user he might not get the expected results
    265                getLog().warn("Overriding default FormSet definition.");
    266            }
    267            defaultFormSet = fs;
    268        } else {
    269            final FormSet formset = getFormSets().get(key);
    270            if (formset == null) { // it hasn't been included yet
    271                if (getLog().isDebugEnabled()) {
    272                    getLog().debug("Adding FormSet '" + fs + "'.");
    273                }
    274            } else if (getLog().isWarnEnabled()) { // warn the user he might not
    275                                                   // get the expected results
    276                getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
    277            }
    278            getFormSets().put(key, fs);
    279        }
    280    }
    281
    282    /**
    283     * Create a <code>Rule</code> to handle <code>arg0-arg3</code>
    284     * elements. This will allow validation.xml files that use the
    285     * versions of the DTD prior to Validator 1.2.0 to continue
    286     * working.
    287     */
    288    private void addOldArgRules(final Digester digester) {
    289        // Create a new rule to process args elements
    290        final Rule rule = new Rule() {
    291            @Override
    292            public void begin(final String namespace, final String name, final Attributes attributes) {
    293                // Create the Arg
    294                final Arg arg = new Arg();
    295                arg.setKey(attributes.getValue("key"));
    296                arg.setName(attributes.getValue("name"));
    297                if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
    298                    arg.setResource(false);
    299                }
    300                try {
    301                    final int length = "arg".length(); // skip the arg prefix
    302                    arg.setPosition(Integer.parseInt(name.substring(length)));
    303                } catch (final Exception ex) {
    304                    getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
    305                }
    306
    307                // Add the arg to the parent field
    308                ((Field) getDigester().peek(0)).addArg(arg);
    309            }
    310        };
    311
    312        // Add the rule for each of the arg elements
    313        digester.addRule(ARGS_PATTERN + "0", rule);
    314        digester.addRule(ARGS_PATTERN + "1", rule);
    315        digester.addRule(ARGS_PATTERN + "2", rule);
    316        digester.addRule(ARGS_PATTERN + "3", rule);
    317
    318    }
    319
    320    /**
    321     * Add a <code>ValidatorAction</code> to the resource.  It also creates an
    322     * instance of the class based on the <code>ValidatorAction</code>s
    323     * class name and retrieves the <code>Method</code> instance and sets them
    324     * in the <code>ValidatorAction</code>.
    325     * @param va The validator action.
    326     */
    327    public void addValidatorAction(final ValidatorAction va) {
    328        va.init();
    329
    330        getActions().put(va.getName(), va);
    331
    332        if (getLog().isDebugEnabled()) {
    333            getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
    334        }
    335    }
    336
    337    /**
    338     * Builds a key to store the <code>FormSet</code> under based on it's
    339     * language, country, and variant values.
    340     * @param fs The Form Set.
    341     * @return generated key for a formset.
    342     */
    343    protected String buildKey(final FormSet fs) {
    344        return
    345                this.buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
    346    }
    347
    348    /**
    349     * Assembles a Locale code from the given parts.
    350     */
    351    private String buildLocale(final String lang, final String country, final String variant) {
    352        String key = lang != null && !lang.isEmpty() ? lang : "";
    353        key += country != null && !country.isEmpty() ? "_" + country : "";
    354        key += variant != null && !variant.isEmpty() ? "_" + variant : "";
    355        return key;
    356    }
    357
    358    /**
    359     * Returns a Map of String ValidatorAction names to their ValidatorAction.
    360     * @return Map of Validator Actions
    361     * @since 1.2.0
    362     */
    363    @SuppressWarnings("unchecked") // FastHashMap is not generic
    364    protected Map<String, ValidatorAction> getActions() {
    365        return hActions;
    366    }
    367
    368    /**
    369     * Returns a Map of String constant names to their String values.
    370     * @return Map of Constants
    371     * @since 1.2.0
    372     */
    373    @SuppressWarnings("unchecked") // FastHashMap is not generic
    374    protected Map<String, String> getConstants() {
    375        return hConstants;
    376    }
    377
    378    /**
    379     * <p>Gets a <code>Form</code> based on the name of the form and the
    380     * <code>Locale</code> that most closely matches the <code>Locale</code>
    381     * passed in.  The order of <code>Locale</code> matching is:</p>
    382     * <ol>
    383     *    <li>language + country + variant</li>
    384     *    <li>language + country</li>
    385     *    <li>language</li>
    386     *    <li>default locale</li>
    387     * </ol>
    388     * @param locale The Locale.
    389     * @param formKey The key for the Form.
    390     * @return The validator Form.
    391     * @since 1.1
    392     */
    393    public Form getForm(final Locale locale, final String formKey) {
    394        return this.getForm(locale.getLanguage(), locale.getCountry(), locale
    395                .getVariant(), formKey);
    396    }
    397
    398    /**
    399     * <p>Gets a <code>Form</code> based on the name of the form and the
    400     * <code>Locale</code> that most closely matches the <code>Locale</code>
    401     * passed in.  The order of <code>Locale</code> matching is:</p>
    402     * <ol>
    403     *    <li>language + country + variant</li>
    404     *    <li>language + country</li>
    405     *    <li>language</li>
    406     *    <li>default locale</li>
    407     * </ol>
    408     * @param language The locale's language.
    409     * @param country The locale's country.
    410     * @param variant The locale's language variant.
    411     * @param formKey The key for the Form.
    412     * @return The validator Form.
    413     * @since 1.1
    414     */
    415    public Form getForm(final String language, final String country, final String variant, final String formKey) {
    416
    417        Form form = null;
    418
    419        // Try language/country/variant
    420        String key = this.buildLocale(language, country, variant);
    421        if (!key.isEmpty()) {
    422            final FormSet formSet = getFormSets().get(key);
    423            if (formSet != null) {
    424                form = formSet.getForm(formKey);
    425            }
    426        }
    427        final String localeKey = key;
    428
    429        // Try language/country
    430        if (form == null) {
    431            key = buildLocale(language, country, null);
    432            if (!key.isEmpty()) {
    433                final FormSet formSet = getFormSets().get(key);
    434                if (formSet != null) {
    435                    form = formSet.getForm(formKey);
    436                }
    437            }
    438        }
    439
    440        // Try language
    441        if (form == null) {
    442            key = buildLocale(language, null, null);
    443            if (!key.isEmpty()) {
    444                final FormSet formSet = getFormSets().get(key);
    445                if (formSet != null) {
    446                    form = formSet.getForm(formKey);
    447                }
    448            }
    449        }
    450
    451        // Try default formset
    452        if (form == null) {
    453            form = defaultFormSet.getForm(formKey);
    454            key = "default";
    455        }
    456
    457        if (form == null) {
    458            if (getLog().isWarnEnabled()) {
    459                getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
    460            }
    461        } else if (getLog().isDebugEnabled()) {
    462            getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
    463        }
    464
    465        return form;
    466
    467    }
    468
    469    /**
    470     * <p>Gets a <code>FormSet</code> based on the language, country
    471     *    and variant.</p>
    472     * @param language The locale's language.
    473     * @param country The locale's country.
    474     * @param variant The locale's language variant.
    475     * @return The FormSet for a locale.
    476     * @since 1.2
    477     */
    478    FormSet getFormSet(final String language, final String country, final String variant) {
    479        final String key = buildLocale(language, country, variant);
    480        if (key.isEmpty()) {
    481            return defaultFormSet;
    482        }
    483        return getFormSets().get(key);
    484    }
    485
    486    /**
    487     * Returns a Map of String locale keys to Lists of their FormSets.
    488     * @return Map of Form sets
    489     * @since 1.2.0
    490     */
    491    @SuppressWarnings("unchecked") // FastHashMap is not generic
    492    protected Map<String, FormSet> getFormSets() {
    493        return hFormSets;
    494    }
    495
    496    /**
    497     * Accessor method for Log instance.
    498     *
    499     * The Log instance variable is transient and
    500     * accessing it through this method ensures it
    501     * is re-initialized when this instance is
    502     * de-serialized.
    503     *
    504     * @return The Log instance.
    505     */
    506    private Log getLog() {
    507        if (log == null) {
    508            log = LogFactory.getLog(ValidatorResources.class);
    509        }
    510        return log;
    511    }
    512
    513    /**
    514     * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
    515     * has a direct parent in the formSet with locale en_UK. If it doesn't
    516     * exist, find the formSet with locale en, if no found get the
    517     * defaultFormSet.
    518     *
    519     * @param fs
    520     *            the formSet we want to get the parent from
    521     * @return fs's parent
    522     */
    523    private FormSet getParent(final FormSet fs) {
    524
    525        FormSet parent = null;
    526        if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
    527            parent = defaultFormSet;
    528        } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
    529            parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
    530            if (parent == null) {
    531                parent = defaultFormSet;
    532            }
    533        } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
    534            parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
    535            if (parent == null) {
    536                parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
    537                if (parent == null) {
    538                    parent = defaultFormSet;
    539                }
    540            }
    541        }
    542        return parent;
    543    }
    544
    545    /**
    546     * Gets a <code>ValidatorAction</code> based on it's name.
    547     * @param key The validator action key.
    548     * @return The validator action.
    549     */
    550    public ValidatorAction getValidatorAction(final String key) {
    551        return getActions().get(key);
    552    }
    553
    554    /**
    555     * Gets an unmodifiable <code>Map</code> of the <code>ValidatorAction</code>s.
    556     * @return Map of validator actions.
    557     */
    558    public Map<String, ValidatorAction> getValidatorActions() {
    559        return Collections.unmodifiableMap(getActions());
    560    }
    561
    562    /**
    563     *  Initialize the digester.
    564     */
    565    private Digester initDigester() {
    566        URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
    567        if (rulesUrl == null) {
    568            // Fix for Issue# VALIDATOR-195
    569            rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
    570        }
    571        if (getLog().isDebugEnabled()) {
    572            getLog().debug("Loading rules from '" + rulesUrl + "'");
    573        }
    574        final Digester digester = DigesterLoader.createDigester(rulesUrl);
    575        digester.setNamespaceAware(true);
    576        digester.setValidating(true);
    577        digester.setUseContextClassLoader(true);
    578
    579        // Add rules for arg0-arg3 elements
    580        addOldArgRules(digester);
    581
    582        // register DTDs
    583        for (int i = 0; i < REGISTRATIONS.length; i += 2) {
    584            final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
    585            if (url != null) {
    586                digester.register(REGISTRATIONS[i], url.toString());
    587            }
    588        }
    589        return digester;
    590    }
    591
    592    /**
    593     * Process the <code>ValidatorResources</code> object. Currently sets the
    594     * <code>FastHashMap</code> s to the 'fast' mode and call the processes
    595     * all other resources. <strong>Note </strong>: The framework calls this
    596     * automatically when ValidatorResources is created from an XML file. If you
    597     * create an instance of this class by hand you <strong>must </strong> call
    598     * this method when finished.
    599     */
    600    public void process() {
    601        hFormSets.setFast(true);
    602        hConstants.setFast(true);
    603        hActions.setFast(true);
    604
    605        this.processForms();
    606    }
    607
    608    /**
    609     * <p>Process the <code>Form</code> objects.  This clones the <code>Field</code>s
    610     * that don't exist in a <code>FormSet</code> compared to its parent
    611     * <code>FormSet</code>.</p>
    612     */
    613    private void processForms() {
    614        if (defaultFormSet == null) { // it isn't mandatory to have a
    615            // default formset
    616            defaultFormSet = new FormSet();
    617        }
    618        defaultFormSet.process(getConstants());
    619        // Loop through FormSets and merge if necessary
    620        for (final String key : getFormSets().keySet()) {
    621            final FormSet fs = getFormSets().get(key);
    622            fs.merge(getParent(fs));
    623        }
    624
    625        // Process Fully Constructed FormSets
    626        for (final FormSet fs : getFormSets().values()) {
    627            if (!fs.isProcessed()) {
    628                fs.process(getConstants());
    629            }
    630        }
    631    }
    632
    633}