001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.validator;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.Serializable;
022import java.net.URL;
023import java.util.Collections;
024import java.util.Locale;
025import java.util.Map;
026
027import org.apache.commons.collections.FastHashMap; // DEPRECATED
028import org.apache.commons.digester.Digester;
029import org.apache.commons.digester.Rule;
030import org.apache.commons.digester.xmlrules.DigesterLoader;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.xml.sax.Attributes;
034import org.xml.sax.SAXException;
035
036/**
037 * <p>
038 * General purpose class for storing {@code FormSet} objects based
039 * on their associated {@link Locale}.  Instances of this class are usually
040 * configured through a validation.xml file that is parsed in a constructor.
041 * </p>
042 *
043 * <p><strong>Note</strong> - Classes that extend this class
044 * must be Serializable so that instances may be used in distributable
045 * application server environments.</p>
046 *
047 * <p>
048 * The use of FastHashMap is deprecated and will be replaced in a future
049 * release.
050 * </p>
051 */
052//TODO mutable non-private fields
053public class ValidatorResources implements Serializable {
054
055    private static final long serialVersionUID = -8203745881446239554L;
056
057    /** Name of the digester validator rules file */
058    private static final String VALIDATOR_RULES = "digester-rules.xml";
059
060    /**
061     * The set of public identifiers, and corresponding resource names, for
062     * the versions of the configuration file DTDs that we know about.  There
063     * <strong>MUST</strong> be an even number of Strings in this list!
064     */
065    private static final String[] REGISTRATIONS = {
066        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
067        "/org/apache/commons/validator/resources/validator_1_0.dtd",
068        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
069        "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
070        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
071        "/org/apache/commons/validator/resources/validator_1_1.dtd",
072        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
073        "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
074        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
075        "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
076        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
077        "/org/apache/commons/validator/resources/validator_1_3_0.dtd",
078        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
079        "/org/apache/commons/validator/resources/validator_1_4_0.dtd"
080    };
081
082    /**
083     * The default locale on our server.
084     */
085    @Deprecated // ought to be final, but is not used, so could be dropped
086    protected static Locale defaultLocale = Locale.getDefault(); // NOPMD not used
087
088    private static final String ARGS_PATTERN
089               = "form-validation/formset/form/field/arg";
090
091    private transient Log log = LogFactory.getLog(ValidatorResources.class);
092
093    /**
094     * {@link Map} of {@code FormSet}s stored under
095     * a {@link Locale} key (expressed as a String).
096     *
097     * @deprecated Subclasses should use getFormSets() instead.
098     */
099    @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}