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 }