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 }