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