    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 * <p>
    038 * General purpose class for storing {@code FormSet} objects based
    039 * on their associated {@link Locale}.  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     * {@link Map} of {@code FormSet}s stored under
    094     * a {@link Locale} 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     * {@link Map} 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     * {@link Map} of {@code ValidatorAction}s with
    110     * the name of the {@code ValidatorAction} 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} (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-formed.
    134     * @throws IOException if an I/O error occurs processing the XML files
    135     * @since 1.1
    136     */
    137    public ValidatorResources(final InputStream in) throws IOException, SAXException {
    138        this(new InputStream[]{in});
    139    }
    140
    141    /**
    142     * Create a ValidatorResources object from an InputStream.
    143     *
    144     * @param streams An array of InputStreams to several validation.xml
    145     * configuration files that will be read in order and merged into this object.
    146     * It's the client's responsibility to close these streams.
    147     * @throws SAXException if the validation XML files are not valid or well-formed.
    148     * @throws IOException if an I/O error occurs processing the XML files
    149     * @since 1.1
    150     */
    151    public ValidatorResources(final InputStream[] streams)
    152            throws IOException, SAXException {
    153
    154        final Digester digester = initDigester();
    155        for (int i = 0; i < streams.length; i++) {
    156            if (streams[i] == null) {
    157                throw new IllegalArgumentException("Stream[" + i + "] is null");
    158            }
    159            digester.push(this);
    160            digester.parse(streams[i]);
    161        }
    162
    163        process();
    164    }
    165
    166    /**
    167     * Create a ValidatorResources object from an uri
    168     *
    169     * @param uri The location of a validation.xml configuration file.
    170     * @throws SAXException if the validation XML files are not valid or well-formed.
    171     * @throws IOException if an I/O error occurs processing the XML files
    172     * @since 1.2
    173     */
    174    public ValidatorResources(final String uri) throws IOException, SAXException {
    175        this(new String[] { uri });
    176    }
    177
    178    /**
    179     * Create a ValidatorResources object from several uris
    180     *
    181     * @param uris An array of uris to several validation.xml
    182     * configuration files that will be read in order and merged into this object.
    183     * @throws SAXException if the validation XML files are not valid or well-formed.
    184     * @throws IOException if an I/O error occurs processing the XML files
    185     * @since 1.2
    186     */
    187    public ValidatorResources(final String... uris)
    188            throws IOException, SAXException {
    189
    190        final Digester digester = initDigester();
    191        for (final String element : uris) {
    192            digester.push(this);
    193            digester.parse(element);
    194        }
    195
    196        process();
    197    }
    198
    199    /**
    200     * Create a ValidatorResources object from a URL.
    201     *
    202     * @param url The URL for the validation.xml
    203     * configuration file that will be read into this object.
    204     * @throws SAXException if the validation XML file are not valid or well-formed.
    205     * @throws IOException if an I/O error occurs processing the XML files
    206     * @since 1.3.1
    207     */
    208    public ValidatorResources(final URL url)
    209            throws IOException, SAXException {
    210        this(new URL[]{url});
    211    }
    212
    213    /**
    214     * Create a ValidatorResources object from several URL.
    215     *
    216     * @param urls An array of URL to several validation.xml
    217     * configuration files that will be read in order and merged into this object.
    218     * @throws SAXException if the validation XML files are not valid or well-formed.
    219     * @throws IOException if an I/O error occurs processing the XML files
    220     * @since 1.3.1
    221     */
    222    public ValidatorResources(final URL[] urls)
    223            throws IOException, SAXException {
    224
    225        final Digester digester = initDigester();
    226        for (final URL url : urls) {
    227            digester.push(this);
    228            digester.parse(url);
    229        }
    230
    231        process();
    232    }
    233
    234    /**
    235     * Add a global constant to the resource.
    236     * @param name The constant name.
    237     * @param value The constant value.
    238     */
    239    public void addConstant(final String name, final String value) {
    240        if (getLog().isDebugEnabled()) {
    241            getLog().debug("Adding Global Constant: " + name + "," + value);
    242        }
    243
    244        hConstants.put(name, value);
    245    }
    246
    247    /**
    248     * Add a {@code FormSet} to this {@code ValidatorResources}
    249     * object.  It will be associated with the {@link Locale} of the
    250     * {@code FormSet}.
    251     * @param fs The form set to add.
    252     * @since 1.1
    253     */
    254    public void addFormSet(final FormSet fs) {
    255        final String key = buildKey(fs);
    256        if (key.isEmpty()) { // there can only be one default formset
    257            if (getLog().isWarnEnabled() && defaultFormSet != null) {
    258                // warn the user he might not get the expected results
    259                getLog().warn("Overriding default FormSet definition.");
    260            }
    261            defaultFormSet = fs;
    262        } else {
    263            final FormSet formset = getFormSets().get(key);
    264            if (formset == null) { // it hasn't been included yet
    265                if (getLog().isDebugEnabled()) {
    266                    getLog().debug("Adding FormSet '" + fs + "'.");
    267                }
    268            } else if (getLog().isWarnEnabled()) { // warn the user he might not
    269                                                   // get the expected results
    270                getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
    271            }
    272            getFormSets().put(key, fs);
    273        }
    274    }
    275
    276    /**
    277     * Create a {@code Rule} to handle {@code arg0-arg3}
    278     * elements. This will allow validation.xml files that use the
    279     * versions of the DTD prior to Validator 1.2.0 to continue
    280     * working.
    281     */
    282    private void addOldArgRules(final Digester digester) {
    283        // Create a new rule to process args elements
    284        final Rule rule = new Rule() {
    285            @Override
    286            public void begin(final String namespace, final String name, final Attributes attributes) {
    287                // Create the Arg
    288                final Arg arg = new Arg();
    289                arg.setKey(attributes.getValue("key"));
    290                arg.setName(attributes.getValue("name"));
    291                if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
    292                    arg.setResource(false);
    293                }
    294                try {
    295                    final int length = "arg".length(); // skip the arg prefix
    296                    arg.setPosition(Integer.parseInt(name.substring(length)));
    297                } catch (final Exception ex) {
    298                    getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
    299                }
    300
    301                // Add the arg to the parent field
    302                ((Field) getDigester().peek(0)).addArg(arg);
    303            }
    304        };
    305
    306        // Add the rule for each of the arg elements
    307        digester.addRule(ARGS_PATTERN + "0", rule);
    308        digester.addRule(ARGS_PATTERN + "1", rule);
    309        digester.addRule(ARGS_PATTERN + "2", rule);
    310        digester.addRule(ARGS_PATTERN + "3", rule);
    311
    312    }
    313
    314    /**
    315     * Add a {@code ValidatorAction} to the resource.  It also creates an
    316     * instance of the class based on the {@code ValidatorAction}s
    317     * class name and retrieves the {@code Method} instance and sets them
    318     * in the {@code ValidatorAction}.
    319     * @param va The validator action.
    320     */
    321    public void addValidatorAction(final ValidatorAction va) {
    322        va.init();
    323
    324        getActions().put(va.getName(), va);
    325
    326        if (getLog().isDebugEnabled()) {
    327            getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
    328        }
    329    }
    330
    331    /**
    332     * Builds a key to store the {@code FormSet} under based on its
    333     * language, country, and variant values.
    334     * @param fs The Form Set.
    335     * @return generated key for a formset.
    336     */
    337    protected String buildKey(final FormSet fs) {
    338        return
    339                buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
    340    }
    341
    342    /**
    343     * Assembles a Locale code from the given parts.
    344     */
    345    private String buildLocale(final String lang, final String country, final String variant) {
    346        final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : "");
    347        key.append(country != null && !country.isEmpty() ? "_" + country : "");
    348        key.append(variant != null && !variant.isEmpty() ? "_" + variant : "");
    349        return key.toString();
    350    }
    351
    352    /**
    353     * Returns a Map of String ValidatorAction names to their ValidatorAction.
    354     * @return Map of Validator Actions
    355     * @since 1.2.0
    356     */
    357    @SuppressWarnings("unchecked") // FastHashMap is not generic
    358    protected Map<String, ValidatorAction> getActions() {
    359        return hActions;
    360    }
    361
    362    /**
    363     * Returns a Map of String constant names to their String values.
    364     * @return Map of Constants
    365     * @since 1.2.0
    366     */
    367    @SuppressWarnings("unchecked") // FastHashMap is not generic
    368    protected Map<String, String> getConstants() {
    369        return hConstants;
    370    }
    371
    372    /**
    373     * <p>Gets a {@code Form} based on the name of the form and the
    374     * {@link Locale} that most closely matches the {@link Locale}
    375     * passed in.  The order of {@link Locale} matching is:</p>
    376     * <ol>
    377     *    <li>language + country + variant</li>
    378     *    <li>language + country</li>
    379     *    <li>language</li>
    380     *    <li>default locale</li>
    381     * </ol>
    382     * @param locale The Locale.
    383     * @param formKey The key for the Form.
    384     * @return The validator Form.
    385     * @since 1.1
    386     */
    387    public Form getForm(final Locale locale, final String formKey) {
    388        return this.getForm(locale.getLanguage(), locale.getCountry(), locale
    389                .getVariant(), formKey);
    390    }
    391
    392    /**
    393     * <p>Gets a {@code Form} based on the name of the form and the
    394     * {@link Locale} that most closely matches the {@link Locale}
    395     * passed in.  The order of {@link Locale} matching is:</p>
    396     * <ol>
    397     *    <li>language + country + variant</li>
    398     *    <li>language + country</li>
    399     *    <li>language</li>
    400     *    <li>default locale</li>
    401     * </ol>
    402     * @param language The locale's language.
    403     * @param country The locale's country.
    404     * @param variant The locale's language variant.
    405     * @param formKey The key for the Form.
    406     * @return The validator Form.
    407     * @since 1.1
    408     */
    409    public Form getForm(final String language, final String country, final String variant, final String formKey) {
    410
    411        Form form = null;
    412
    413        // Try language/country/variant
    414        String key = buildLocale(language, country, variant);
    415        if (!key.isEmpty()) {
    416            final FormSet formSet = getFormSets().get(key);
    417            if (formSet != null) {
    418                form = formSet.getForm(formKey);
    419            }
    420        }
    421        final String localeKey = key;
    422
    423        // Try language/country
    424        if (form == null) {
    425            key = buildLocale(language, country, null);
    426            if (!key.isEmpty()) {
    427                final FormSet formSet = getFormSets().get(key);
    428                if (formSet != null) {
    429                    form = formSet.getForm(formKey);
    430                }
    431            }
    432        }
    433
    434        // Try language
    435        if (form == null) {
    436            key = buildLocale(language, null, null);
    437            if (!key.isEmpty()) {
    438                final FormSet formSet = getFormSets().get(key);
    439                if (formSet != null) {
    440                    form = formSet.getForm(formKey);
    441                }
    442            }
    443        }
    444
    445        // Try default formset
    446        if (form == null) {
    447            form = defaultFormSet.getForm(formKey);
    448            key = "default";
    449        }
    450
    451        if (form == null) {
    452            if (getLog().isWarnEnabled()) {
    453                getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
    454            }
    455        } else if (getLog().isDebugEnabled()) {
    456            getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
    457        }
    458
    459        return form;
    460
    461    }
    462
    463    /**
    464     * <p>Gets a {@code FormSet} based on the language, country
    465     *    and variant.</p>
    466     * @param language The locale's language.
    467     * @param country The locale's country.
    468     * @param variant The locale's language variant.
    469     * @return The FormSet for a locale.
    470     * @since 1.2
    471     */
    472    FormSet getFormSet(final String language, final String country, final String variant) {
    473        final String key = buildLocale(language, country, variant);
    474        if (key.isEmpty()) {
    475            return defaultFormSet;
    476        }
    477        return getFormSets().get(key);
    478    }
    479
    480    /**
    481     * Returns a Map of String locale keys to Lists of their FormSets.
    482     * @return Map of Form sets
    483     * @since 1.2.0
    484     */
    485    @SuppressWarnings("unchecked") // FastHashMap is not generic
    486    protected Map<String, FormSet> getFormSets() {
    487        return hFormSets;
    488    }
    489
    490    /**
    491     * Accessor method for Log instance.
    492     *
    493     * The Log instance variable is transient and
    494     * accessing it through this method ensures it
    495     * is re-initialized when this instance is
    496     * de-serialized.
    497     *
    498     * @return The Log instance.
    499     */
    500    private Log getLog() {
    501        if (log == null) {
    502            log = LogFactory.getLog(ValidatorResources.class);
    503        }
    504        return log;
    505    }
    506
    507    /**
    508     * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
    509     * has a direct parent in the formSet with locale en_UK. If it doesn't
    510     * exist, find the formSet with locale en, if no found get the
    511     * defaultFormSet.
    512     *
    513     * @param fs
    514     *            the formSet we want to get the parent from
    515     * @return fs's parent
    516     */
    517    private FormSet getParent(final FormSet fs) {
    518
    519        FormSet parent = null;
    520        if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
    521            parent = defaultFormSet;
    522        } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
    523            parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
    524            if (parent == null) {
    525                parent = defaultFormSet;
    526            }
    527        } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
    528            parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
    529            if (parent == null) {
    530                parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
    531                if (parent == null) {
    532                    parent = defaultFormSet;
    533                }
    534            }
    535        }
    536        return parent;
    537    }
    538
    539    /**
    540     * Gets a {@code ValidatorAction} based on its name.
    541     * @param key The validator action key.
    542     * @return The validator action.
    543     */
    544    public ValidatorAction getValidatorAction(final String key) {
    545        return getActions().get(key);
    546    }
    547
    548    /**
    549     * Gets an unmodifiable {@link Map} of the {@code ValidatorAction}s.
    550     * @return Map of validator actions.
    551     */
    552    public Map<String, ValidatorAction> getValidatorActions() {
    553        return Collections.unmodifiableMap(getActions());
    554    }
    555
    556    /**
    557     *  Initialize the digester.
    558     */
    559    private Digester initDigester() {
    560        URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
    561        if (rulesUrl == null) {
    562            // Fix for Issue# VALIDATOR-195
    563            rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
    564        }
    565        if (getLog().isDebugEnabled()) {
    566            getLog().debug("Loading rules from '" + rulesUrl + "'");
    567        }
    568        final Digester digester = DigesterLoader.createDigester(rulesUrl);
    569        digester.setNamespaceAware(true);
    570        digester.setValidating(true);
    571        digester.setUseContextClassLoader(true);
    572
    573        // Add rules for arg0-arg3 elements
    574        addOldArgRules(digester);
    575
    576        // register DTDs
    577        for (int i = 0; i < REGISTRATIONS.length; i += 2) {
    578            final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
    579            if (url != null) {
    580                digester.register(REGISTRATIONS[i], url.toString());
    581            }
    582        }
    583        return digester;
    584    }
    585
    586    /**
    587     * Process the {@code ValidatorResources} object. Currently, sets the
    588     * {@code FastHashMap} s to the 'fast' mode and call the processes
    589     * all other resources. <strong>Note </strong>: The framework calls this
    590     * automatically when ValidatorResources is created from an XML file. If you
    591     * create an instance of this class by hand you <strong>must </strong> call
    592     * this method when finished.
    593     */
    594    public void process() {
    595        hFormSets.setFast(true);
    596        hConstants.setFast(true);
    597        hActions.setFast(true);
    598
    599        processForms();
    600    }
    601
    602    /**
    603     * <p>Process the {@code Form} objects.  This clones the {@code Field}s
    604     * that don't exist in a {@code FormSet} compared to its parent
    605     * {@code FormSet}.</p>
    606     */
    607    private void processForms() {
    608        if (defaultFormSet == null) { // it isn't mandatory to have a
    609            // default formset
    610            defaultFormSet = new FormSet();
    611        }
    612        defaultFormSet.process(getConstants());
    613        // Loop through FormSets and merge if necessary
    614        for (final String key : getFormSets().keySet()) {
    615            final FormSet fs = getFormSets().get(key);
    616            fs.merge(getParent(fs));
    617        }
    618
    619        // Process Fully Constructed FormSets
    620        for (final FormSet fs : getFormSets().values()) {
    621            if (!fs.isProcessed()) {
    622                fs.process(getConstants());
    623            }
    624        }
    625    }
    626
    627}