ValidatorResources.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.validator;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.Serializable;
  21. import java.net.URL;
  22. import java.util.Collections;
  23. import java.util.Locale;
  24. import java.util.Map;

  25. import org.apache.commons.collections.FastHashMap; // DEPRECATED
  26. import org.apache.commons.digester.Digester;
  27. import org.apache.commons.digester.Rule;
  28. import org.apache.commons.digester.xmlrules.DigesterLoader;
  29. import org.apache.commons.logging.Log;
  30. import org.apache.commons.logging.LogFactory;
  31. import org.xml.sax.Attributes;
  32. import org.xml.sax.SAXException;

  33. /**
  34.  * <p>
  35.  * General purpose class for storing <code>FormSet</code> objects based
  36.  * on their associated <code>Locale</code>.  Instances of this class are usually
  37.  * configured through a validation.xml file that is parsed in a constructor.
  38.  * </p>
  39.  *
  40.  * <p><strong>Note</strong> - Classes that extend this class
  41.  * must be Serializable so that instances may be used in distributable
  42.  * application server environments.</p>
  43.  *
  44.  * <p>
  45.  * The use of FastHashMap is deprecated and will be replaced in a future
  46.  * release.
  47.  * </p>
  48.  */
  49. //TODO mutable non-private fields
  50. public class ValidatorResources implements Serializable {

  51.     private static final long serialVersionUID = -8203745881446239554L;

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

  54.     /**
  55.      * The set of public identifiers, and corresponding resource names, for
  56.      * the versions of the configuration file DTDs that we know about.  There
  57.      * <strong>MUST</strong> be an even number of Strings in this list!
  58.      */
  59.     private static final String[] REGISTRATIONS = {
  60.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
  61.         "/org/apache/commons/validator/resources/validator_1_0.dtd",
  62.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
  63.         "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
  64.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
  65.         "/org/apache/commons/validator/resources/validator_1_1.dtd",
  66.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
  67.         "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
  68.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
  69.         "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
  70.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
  71.         "/org/apache/commons/validator/resources/validator_1_3_0.dtd",
  72.         "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
  73.         "/org/apache/commons/validator/resources/validator_1_4_0.dtd"
  74.     };

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

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

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

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

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

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

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

  108.     /**
  109.      * Create an empty ValidatorResources object.
  110.      */
  111.     public ValidatorResources() {
  112.     }

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

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

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

  147.         this.process();
  148.     }

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

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

  173.         final Digester digester = initDigester();
  174.         for (final String element : uris) {
  175.             digester.push(this);
  176.             digester.parse(element);
  177.         }

  178.         this.process();
  179.     }

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

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

  206.         final Digester digester = initDigester();
  207.         for (final URL url : urls) {
  208.             digester.push(this);
  209.             digester.parse(url);
  210.         }

  211.         this.process();
  212.     }

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

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

  224.     /**
  225.      * Add a <code>FormSet</code> to this <code>ValidatorResources</code>
  226.      * object.  It will be associated with the <code>Locale</code> of the
  227.      * <code>FormSet</code>.
  228.      * @param fs The form set to add.
  229.      * @since 1.1
  230.      */
  231.     public void addFormSet(final FormSet fs) {
  232.         final String key = this.buildKey(fs);
  233.         if (key.isEmpty()) { // there can only be one default formset
  234.             if (getLog().isWarnEnabled() && defaultFormSet != null) {
  235.                 // warn the user he might not get the expected results
  236.                 getLog().warn("Overriding default FormSet definition.");
  237.             }
  238.             defaultFormSet = fs;
  239.         } else {
  240.             final FormSet formset = getFormSets().get(key);
  241.             if (formset == null) { // it hasn't been included yet
  242.                 if (getLog().isDebugEnabled()) {
  243.                     getLog().debug("Adding FormSet '" + fs + "'.");
  244.                 }
  245.             } else if (getLog().isWarnEnabled()) { // warn the user he might not
  246.                                                    // get the expected results
  247.                 getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
  248.             }
  249.             getFormSets().put(key, fs);
  250.         }
  251.     }

  252.     /**
  253.      * Create a <code>Rule</code> to handle <code>arg0-arg3</code>
  254.      * elements. This will allow validation.xml files that use the
  255.      * versions of the DTD prior to Validator 1.2.0 to continue
  256.      * working.
  257.      */
  258.     private void addOldArgRules(final Digester digester) {
  259.         // Create a new rule to process args elements
  260.         final Rule rule = new Rule() {
  261.             @Override
  262.             public void begin(final String namespace, final String name, final Attributes attributes) {
  263.                 // Create the Arg
  264.                 final Arg arg = new Arg();
  265.                 arg.setKey(attributes.getValue("key"));
  266.                 arg.setName(attributes.getValue("name"));
  267.                 if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
  268.                     arg.setResource(false);
  269.                 }
  270.                 try {
  271.                     final int length = "arg".length(); // skip the arg prefix
  272.                     arg.setPosition(Integer.parseInt(name.substring(length)));
  273.                 } catch (final Exception ex) {
  274.                     getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
  275.                 }

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

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

  285.     }

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

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

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

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

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

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

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

  337.     /**
  338.      * <p>Gets a <code>Form</code> based on the name of the form and the
  339.      * <code>Locale</code> that most closely matches the <code>Locale</code>
  340.      * passed in.  The order of <code>Locale</code> matching is:</p>
  341.      * <ol>
  342.      *    <li>language + country + variant</li>
  343.      *    <li>language + country</li>
  344.      *    <li>language</li>
  345.      *    <li>default locale</li>
  346.      * </ol>
  347.      * @param locale The Locale.
  348.      * @param formKey The key for the Form.
  349.      * @return The validator Form.
  350.      * @since 1.1
  351.      */
  352.     public Form getForm(final Locale locale, final String formKey) {
  353.         return this.getForm(locale.getLanguage(), locale.getCountry(), locale
  354.                 .getVariant(), formKey);
  355.     }

  356.     /**
  357.      * <p>Gets a <code>Form</code> based on the name of the form and the
  358.      * <code>Locale</code> that most closely matches the <code>Locale</code>
  359.      * passed in.  The order of <code>Locale</code> matching is:</p>
  360.      * <ol>
  361.      *    <li>language + country + variant</li>
  362.      *    <li>language + country</li>
  363.      *    <li>language</li>
  364.      *    <li>default locale</li>
  365.      * </ol>
  366.      * @param language The locale's language.
  367.      * @param country The locale's country.
  368.      * @param variant The locale's language variant.
  369.      * @param formKey The key for the Form.
  370.      * @return The validator Form.
  371.      * @since 1.1
  372.      */
  373.     public Form getForm(final String language, final String country, final String variant, final String formKey) {

  374.         Form form = null;

  375.         // Try language/country/variant
  376.         String key = this.buildLocale(language, country, variant);
  377.         if (!key.isEmpty()) {
  378.             final FormSet formSet = getFormSets().get(key);
  379.             if (formSet != null) {
  380.                 form = formSet.getForm(formKey);
  381.             }
  382.         }
  383.         final String localeKey = key;

  384.         // Try language/country
  385.         if (form == null) {
  386.             key = buildLocale(language, country, null);
  387.             if (!key.isEmpty()) {
  388.                 final FormSet formSet = getFormSets().get(key);
  389.                 if (formSet != null) {
  390.                     form = formSet.getForm(formKey);
  391.                 }
  392.             }
  393.         }

  394.         // Try language
  395.         if (form == null) {
  396.             key = buildLocale(language, null, null);
  397.             if (!key.isEmpty()) {
  398.                 final FormSet formSet = getFormSets().get(key);
  399.                 if (formSet != null) {
  400.                     form = formSet.getForm(formKey);
  401.                 }
  402.             }
  403.         }

  404.         // Try default formset
  405.         if (form == null) {
  406.             form = defaultFormSet.getForm(formKey);
  407.             key = "default";
  408.         }

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

  416.         return form;

  417.     }

  418.     /**
  419.      * <p>Gets a <code>FormSet</code> based on the language, country
  420.      *    and variant.</p>
  421.      * @param language The locale's language.
  422.      * @param country The locale's country.
  423.      * @param variant The locale's language variant.
  424.      * @return The FormSet for a locale.
  425.      * @since 1.2
  426.      */
  427.     FormSet getFormSet(final String language, final String country, final String variant) {
  428.         final String key = buildLocale(language, country, variant);
  429.         if (key.isEmpty()) {
  430.             return defaultFormSet;
  431.         }
  432.         return getFormSets().get(key);
  433.     }

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

  443.     /**
  444.      * Accessor method for Log instance.
  445.      *
  446.      * The Log instance variable is transient and
  447.      * accessing it through this method ensures it
  448.      * is re-initialized when this instance is
  449.      * de-serialized.
  450.      *
  451.      * @return The Log instance.
  452.      */
  453.     private Log getLog() {
  454.         if (log == null) {
  455.             log = LogFactory.getLog(ValidatorResources.class);
  456.         }
  457.         return log;
  458.     }

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

  470.         FormSet parent = null;
  471.         if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
  472.             parent = defaultFormSet;
  473.         } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
  474.             parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
  475.             if (parent == null) {
  476.                 parent = defaultFormSet;
  477.             }
  478.         } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
  479.             parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
  480.             if (parent == null) {
  481.                 parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
  482.                 if (parent == null) {
  483.                     parent = defaultFormSet;
  484.                 }
  485.             }
  486.         }
  487.         return parent;
  488.     }

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

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

  504.     /**
  505.      *  Initialize the digester.
  506.      */
  507.     private Digester initDigester() {
  508.         URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
  509.         if (rulesUrl == null) {
  510.             // Fix for Issue# VALIDATOR-195
  511.             rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
  512.         }
  513.         if (getLog().isDebugEnabled()) {
  514.             getLog().debug("Loading rules from '" + rulesUrl + "'");
  515.         }
  516.         final Digester digester = DigesterLoader.createDigester(rulesUrl);
  517.         digester.setNamespaceAware(true);
  518.         digester.setValidating(true);
  519.         digester.setUseContextClassLoader(true);

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

  522.         // register DTDs
  523.         for (int i = 0; i < REGISTRATIONS.length; i += 2) {
  524.             final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
  525.             if (url != null) {
  526.                 digester.register(REGISTRATIONS[i], url.toString());
  527.             }
  528.         }
  529.         return digester;
  530.     }

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

  543.         this.processForms();
  544.     }

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

  561.         // Process Fully Constructed FormSets
  562.         for (final FormSet fs : getFormSets().values()) {
  563.             if (!fs.isProcessed()) {
  564.                 fs.process(getConstants());
  565.             }
  566.         }
  567.     }

  568. }