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