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      @Deprecated // ought to be final, but is not used, so could be dropped
86      protected static Locale defaultLocale = Locale.getDefault(); // NOPMD not used
87  
88      private static final String ARGS_PATTERN
89                 = "form-validation/formset/form/field/arg";
90  
91      private transient Log log = LogFactory.getLog(ValidatorResources.class);
92  
93      /**
94       * {@link Map} of {@code FormSet}s stored under
95       * a {@link Locale} key (expressed as a String).
96       *
97       * @deprecated Subclasses should use getFormSets() instead.
98       */
99      @Deprecated
100     protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet>
101 
102     /**
103      * {@link Map} of global constant values with
104      * the name of the constant as the key.
105      *
106      * @deprecated Subclasses should use getConstants() instead.
107      */
108     @Deprecated
109     protected FastHashMap hConstants = new FastHashMap(); // <String, String>
110 
111     /**
112      * {@link Map} of {@code ValidatorAction}s with
113      * the name of the {@code ValidatorAction} as the key.
114      *
115      * @deprecated Subclasses should use getActions() instead.
116      */
117     @Deprecated
118     protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction>
119 
120     /**
121      * This is the default {@code FormSet} (without locale). (We probably don't need
122      * the defaultLocale anymore.)
123      */
124     protected FormSet defaultFormSet;
125 
126     /**
127      * Create an empty ValidatorResources object.
128      */
129     public ValidatorResources() {
130     }
131 
132     /**
133      * Create a ValidatorResources object from an InputStream.
134      *
135      * @param in InputStream to a validation.xml configuration file.  It's the client's
136      * responsibility to close this stream.
137      * @throws SAXException if the validation XML files are not valid or well-formed.
138      * @throws IOException if an I/O error occurs processing the XML files
139      * @since 1.1
140      */
141     public ValidatorResources(final InputStream in) throws IOException, SAXException {
142         this(new InputStream[]{in});
143     }
144 
145     /**
146      * Create a ValidatorResources object from an InputStream.
147      *
148      * @param streams An array of InputStreams to several validation.xml
149      * configuration files that will be read in order and merged into this object.
150      * It's the client's responsibility to close these streams.
151      * @throws SAXException if the validation XML files are not valid or well-formed.
152      * @throws IOException if an I/O error occurs processing the XML files
153      * @since 1.1
154      */
155     public ValidatorResources(final InputStream[] streams)
156             throws IOException, SAXException {
157 
158         final Digester digester = initDigester();
159         for (int i = 0; i < streams.length; i++) {
160             if (streams[i] == null) {
161                 throw new IllegalArgumentException("Stream[" + i + "] is null");
162             }
163             digester.push(this);
164             digester.parse(streams[i]);
165         }
166 
167         process();
168     }
169 
170     /**
171      * Create a ValidatorResources object from an uri
172      *
173      * @param uri The location of a validation.xml configuration file.
174      * @throws SAXException if the validation XML files are not valid or well-formed.
175      * @throws IOException if an I/O error occurs processing the XML files
176      * @since 1.2
177      */
178     public ValidatorResources(final String uri) throws IOException, SAXException {
179         this(new String[] { uri });
180     }
181 
182     /**
183      * Create a ValidatorResources object from several uris
184      *
185      * @param uris An array of uris to several validation.xml
186      * configuration files that will be read in order and merged into this object.
187      * @throws SAXException if the validation XML files are not valid or well-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         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-formed.
209      * @throws IOException if an I/O error occurs processing the XML files
210      * @since 1.3.1
211      */
212     public ValidatorResources(final URL url)
213             throws IOException, SAXException {
214         this(new URL[]{url});
215     }
216 
217     /**
218      * Create a ValidatorResources object from several URL.
219      *
220      * @param urls An array of URL to several validation.xml
221      * configuration files that will be read in order and merged into this object.
222      * @throws SAXException if the validation XML files are not valid or well-formed.
223      * @throws IOException if an I/O error occurs processing the XML files
224      * @since 1.3.1
225      */
226     public ValidatorResources(final URL[] urls)
227             throws IOException, SAXException {
228 
229         final Digester digester = initDigester();
230         for (final URL url : urls) {
231             digester.push(this);
232             digester.parse(url);
233         }
234 
235         process();
236     }
237 
238     /**
239      * Add a global constant to the resource.
240      *
241      * @param name The constant name.
242      * @param value The constant value.
243      */
244     public void addConstant(final String name, final String value) {
245         if (getLog().isDebugEnabled()) {
246             getLog().debug("Adding Global Constant: " + name + "," + value);
247         }
248 
249         hConstants.put(name, value);
250     }
251 
252     /**
253      * Add a {@code FormSet} to this {@code ValidatorResources}
254      * object.  It will be associated with the {@link Locale} of the
255      * {@code FormSet}.
256      *
257      * @param fs The form set to add.
258      * @since 1.1
259      */
260     public void addFormSet(final FormSet fs) {
261         final String key = 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} to handle {@code arg0-arg3}
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} to the resource.  It also creates an
322      * instance of the class based on the {@code ValidatorAction}s
323      * class name and retrieves the {@code Method} instance and sets them
324      * in the {@code ValidatorAction}.
325      *
326      * @param va The validator action.
327      */
328     public void addValidatorAction(final ValidatorAction va) {
329         va.init();
330 
331         getActions().put(va.getName(), va);
332 
333         if (getLog().isDebugEnabled()) {
334             getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
335         }
336     }
337 
338     /**
339      * Builds a key to store the {@code FormSet} under based on its
340      * language, country, and variant values.
341      *
342      * @param fs The Form Set.
343      * @return generated key for a formset.
344      */
345     protected String buildKey(final FormSet fs) {
346         return
347                 buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
348     }
349 
350     /**
351      * Assembles a Locale code from the given parts.
352      */
353     private String buildLocale(final String lang, final String country, final String variant) {
354         final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : "");
355         key.append(country != null && !country.isEmpty() ? "_" + country : "");
356         key.append(variant != null && !variant.isEmpty() ? "_" + variant : "");
357         return key.toString();
358     }
359 
360     /**
361      * Returns a Map of String ValidatorAction names to their ValidatorAction.
362      *
363      * @return Map of Validator Actions
364      * @since 1.2.0
365      */
366     @SuppressWarnings("unchecked") // FastHashMap is not generic
367     protected Map<String, ValidatorAction> getActions() {
368         return hActions;
369     }
370 
371     /**
372      * Returns a Map of String constant names to their String values.
373      *
374      * @return Map of Constants
375      * @since 1.2.0
376      */
377     @SuppressWarnings("unchecked") // FastHashMap is not generic
378     protected Map<String, String> getConstants() {
379         return hConstants;
380     }
381 
382     /**
383      * <p>Gets a {@code Form} based on the name of the form and the
384      * {@link Locale} that most closely matches the {@link Locale}
385      * passed in.  The order of {@link Locale} matching is:</p>
386      * <ol>
387      *    <li>language + country + variant</li>
388      *    <li>language + country</li>
389      *    <li>language</li>
390      *    <li>default locale</li>
391      * </ol>
392      *
393      * @param locale The Locale.
394      * @param formKey The key for the Form.
395      * @return The validator Form.
396      * @since 1.1
397      */
398     public Form getForm(final Locale locale, final String formKey) {
399         return this.getForm(locale.getLanguage(), locale.getCountry(), locale
400                 .getVariant(), formKey);
401     }
402 
403     /**
404      * <p>Gets a {@code Form} based on the name of the form and the
405      * {@link Locale} that most closely matches the {@link Locale}
406      * passed in.  The order of {@link Locale} matching is:</p>
407      * <ol>
408      *    <li>language + country + variant</li>
409      *    <li>language + country</li>
410      *    <li>language</li>
411      *    <li>default locale</li>
412      * </ol>
413      *
414      * @param language The locale's language.
415      * @param country The locale's country.
416      * @param variant The locale's language variant.
417      * @param formKey The key for the Form.
418      * @return The validator Form.
419      * @since 1.1
420      */
421     public Form getForm(final String language, final String country, final String variant, final String formKey) {
422 
423         Form form = null;
424 
425         // Try language/country/variant
426         String key = buildLocale(language, country, variant);
427         if (!key.isEmpty()) {
428             final FormSet formSet = getFormSets().get(key);
429             if (formSet != null) {
430                 form = formSet.getForm(formKey);
431             }
432         }
433         final String localeKey = key;
434 
435         // Try language/country
436         if (form == null) {
437             key = buildLocale(language, country, null);
438             if (!key.isEmpty()) {
439                 final FormSet formSet = getFormSets().get(key);
440                 if (formSet != null) {
441                     form = formSet.getForm(formKey);
442                 }
443             }
444         }
445 
446         // Try language
447         if (form == null) {
448             key = buildLocale(language, null, null);
449             if (!key.isEmpty()) {
450                 final FormSet formSet = getFormSets().get(key);
451                 if (formSet != null) {
452                     form = formSet.getForm(formKey);
453                 }
454             }
455         }
456 
457         // Try default formset
458         if (form == null) {
459             form = defaultFormSet.getForm(formKey);
460             key = "default";
461         }
462 
463         if (form == null) {
464             if (getLog().isWarnEnabled()) {
465                 getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
466             }
467         } else if (getLog().isDebugEnabled()) {
468             getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
469         }
470 
471         return form;
472 
473     }
474 
475     /**
476      * <p>Gets a {@code FormSet} based on the language, country
477      *    and variant.</p>
478      *
479      * @param language The locale's language.
480      * @param country The locale's country.
481      * @param variant The locale's language variant.
482      * @return The FormSet for a locale.
483      * @since 1.2
484      */
485     FormSet getFormSet(final String language, final String country, final String variant) {
486         final String key = buildLocale(language, country, variant);
487         if (key.isEmpty()) {
488             return defaultFormSet;
489         }
490         return getFormSets().get(key);
491     }
492 
493     /**
494      * Returns a Map of String locale keys to Lists of their FormSets.
495      *
496      * @return Map of Form sets
497      * @since 1.2.0
498      */
499     @SuppressWarnings("unchecked") // FastHashMap is not generic
500     protected Map<String, FormSet> getFormSets() {
501         return hFormSets;
502     }
503 
504     /**
505      * Accessor method for Log instance.
506      *
507      * The Log instance variable is transient and
508      * accessing it through this method ensures it
509      * is re-initialized when this instance is
510      * de-serialized.
511      *
512      * @return The Log instance.
513      */
514     private Log getLog() {
515         if (log == null) {
516             log = LogFactory.getLog(ValidatorResources.class);
517         }
518         return log;
519     }
520 
521     /**
522      * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
523      * has a direct parent in the formSet with locale en_UK. If it doesn't
524      * exist, find the formSet with locale en, if no found get the
525      * defaultFormSet.
526      *
527      * @param fs
528      *            the formSet we want to get the parent from
529      * @return fs's parent
530      */
531     private FormSet getParent(final FormSet fs) {
532 
533         FormSet parent = null;
534         if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
535             parent = defaultFormSet;
536         } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
537             parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
538             if (parent == null) {
539                 parent = defaultFormSet;
540             }
541         } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
542             parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
543             if (parent == null) {
544                 parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
545                 if (parent == null) {
546                     parent = defaultFormSet;
547                 }
548             }
549         }
550         return parent;
551     }
552 
553     /**
554      * Gets a {@code ValidatorAction} based on its name.
555      *
556      * @param key The validator action key.
557      * @return The validator action.
558      */
559     public ValidatorAction getValidatorAction(final String key) {
560         return getActions().get(key);
561     }
562 
563     /**
564      * Gets an unmodifiable {@link Map} of the {@code ValidatorAction}s.
565      *
566      * @return Map of validator actions.
567      */
568     public Map<String, ValidatorAction> getValidatorActions() {
569         return Collections.unmodifiableMap(getActions());
570     }
571 
572     /**
573      *  Initialize the digester.
574      */
575     private Digester initDigester() {
576         URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
577         if (rulesUrl == null) {
578             // Fix for Issue# VALIDATOR-195
579             rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
580         }
581         if (getLog().isDebugEnabled()) {
582             getLog().debug("Loading rules from '" + rulesUrl + "'");
583         }
584         final Digester digester = DigesterLoader.createDigester(rulesUrl);
585         digester.setNamespaceAware(true);
586         digester.setValidating(true);
587         digester.setUseContextClassLoader(true);
588 
589         // Add rules for arg0-arg3 elements
590         addOldArgRules(digester);
591 
592         // register DTDs
593         for (int i = 0; i < REGISTRATIONS.length; i += 2) {
594             final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
595             if (url != null) {
596                 digester.register(REGISTRATIONS[i], url.toString());
597             }
598         }
599         return digester;
600     }
601 
602     /**
603      * Process the {@code ValidatorResources} object. Currently, sets the
604      * {@code FastHashMap} s to the 'fast' mode and call the processes
605      * all other resources. <strong>Note </strong>: The framework calls this
606      * automatically when ValidatorResources is created from an XML file. If you
607      * create an instance of this class by hand you <strong>must </strong> call
608      * this method when finished.
609      */
610     public void process() {
611         hFormSets.setFast(true);
612         hConstants.setFast(true);
613         hActions.setFast(true);
614 
615         processForms();
616     }
617 
618     /**
619      * <p>Process the {@code Form} objects.  This clones the {@code Field}s
620      * that don't exist in a {@code FormSet} compared to its parent
621      * {@code FormSet}.</p>
622      */
623     private void processForms() {
624         if (defaultFormSet == null) { // it isn't mandatory to have a
625             // default formset
626             defaultFormSet = new FormSet();
627         }
628         defaultFormSet.process(getConstants());
629         // Loop through FormSets and merge if necessary
630         for (final String key : getFormSets().keySet()) {
631             final FormSet fs = getFormSets().get(key);
632             fs.merge(getParent(fs));
633         }
634 
635         // Process Fully Constructed FormSets
636         for (final FormSet fs : getFormSets().values()) {
637             if (!fs.isProcessed()) {
638                 fs.process(getConstants());
639             }
640         }
641     }
642 
643 }