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    *      https://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} objects based
39   * on their associated {@link Locale}.  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       * {@link Map} of {@code FormSet}s stored under
94       * a {@link Locale} 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      * {@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 }