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