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 * General purpose class for storing {@code FormSet} objects based
038 * on their associated {@link Locale}.  Instances of this class are usually
039 * configured through a validation.xml file that is parsed in a constructor.
040 *
041 * <p><strong>Note</strong> - Classes that extend this class
042 * must be Serializable so that instances may be used in distributable
043 * application server environments.</p>
044 *
045 * <p>
046 * The use of FastHashMap is deprecated and will be replaced in a future
047 * release.
048 * </p>
049 */
050//TODO mutable non-private fields
051public class ValidatorResources implements Serializable {
052
053    private static final long serialVersionUID = -8203745881446239554L;
054
055    /** Name of the digester validator rules file */
056    private static final String VALIDATOR_RULES = "digester-rules.xml";
057
058    /**
059     * The set of public identifiers, and corresponding resource names, for
060     * the versions of the configuration file DTDs that we know about.  There
061     * <strong>MUST</strong> be an even number of Strings in this list!
062     */
063    private static final String[] REGISTRATIONS = {
064        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
065        "/org/apache/commons/validator/resources/validator_1_0.dtd",
066        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
067        "/org/apache/commons/validator/resources/validator_1_0_1.dtd",
068        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
069        "/org/apache/commons/validator/resources/validator_1_1.dtd",
070        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
071        "/org/apache/commons/validator/resources/validator_1_1_3.dtd",
072        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
073        "/org/apache/commons/validator/resources/validator_1_2_0.dtd",
074        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
075        "/org/apache/commons/validator/resources/validator_1_3_0.dtd",
076        "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
077        "/org/apache/commons/validator/resources/validator_1_4_0.dtd"
078    };
079
080    /**
081     * The default locale on our server.
082     */
083    @Deprecated // ought to be final, but is not used, so could be dropped
084    protected static Locale defaultLocale = Locale.getDefault(); // NOPMD not used
085
086    private static final String ARGS_PATTERN
087               = "form-validation/formset/form/field/arg";
088
089    private transient Log log = LogFactory.getLog(ValidatorResources.class);
090
091    /**
092     * {@link Map} of {@code FormSet}s stored under
093     * a {@link Locale} key (expressed as a String).
094     *
095     * @deprecated Subclasses should use getFormSets() instead.
096     */
097    @Deprecated
098    protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet>
099
100    /**
101     * {@link Map} of global constant values with
102     * the name of the constant as the key.
103     *
104     * @deprecated Subclasses should use getConstants() instead.
105     */
106    @Deprecated
107    protected FastHashMap hConstants = new FastHashMap(); // <String, String>
108
109    /**
110     * {@link Map} of {@code ValidatorAction}s with
111     * the name of the {@code ValidatorAction} as the key.
112     *
113     * @deprecated Subclasses should use getActions() instead.
114     */
115    @Deprecated
116    protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction>
117
118    /**
119     * This is the default {@code FormSet} (without locale). (We probably don't need
120     * the defaultLocale anymore.)
121     */
122    protected FormSet defaultFormSet;
123
124    /**
125     * Create an empty ValidatorResources object.
126     */
127    public ValidatorResources() {
128    }
129
130    /**
131     * Create a ValidatorResources object from an InputStream.
132     *
133     * @param in InputStream to a validation.xml configuration file.  It's the client's
134     * responsibility to close this stream.
135     * @throws SAXException if the validation XML files are not valid or well-formed.
136     * @throws IOException if an I/O error occurs processing the XML files
137     * @since 1.1
138     */
139    public ValidatorResources(final InputStream in) throws IOException, SAXException {
140        this(new InputStream[]{in});
141    }
142
143    /**
144     * Create a ValidatorResources object from an InputStream.
145     *
146     * @param streams An array of InputStreams to several validation.xml
147     * configuration files that will be read in order and merged into this object.
148     * It's the client's responsibility to close these streams.
149     * @throws SAXException if the validation XML files are not valid or well-formed.
150     * @throws IOException if an I/O error occurs processing the XML files
151     * @since 1.1
152     */
153    public ValidatorResources(final InputStream[] streams)
154            throws IOException, SAXException {
155
156        final Digester digester = initDigester();
157        for (int i = 0; i < streams.length; i++) {
158            if (streams[i] == null) {
159                throw new IllegalArgumentException("Stream[" + i + "] is null");
160            }
161            digester.push(this);
162            digester.parse(streams[i]);
163        }
164
165        process();
166    }
167
168    /**
169     * Create a ValidatorResources object from an uri
170     *
171     * @param uri The location of a validation.xml configuration file.
172     * @throws SAXException if the validation XML files are not valid or well-formed.
173     * @throws IOException if an I/O error occurs processing the XML files
174     * @since 1.2
175     */
176    public ValidatorResources(final String uri) throws IOException, SAXException {
177        this(new String[] { uri });
178    }
179
180    /**
181     * Create a ValidatorResources object from several uris
182     *
183     * @param uris An array of uris to several validation.xml
184     * configuration files that will be read in order and merged into this object.
185     * @throws SAXException if the validation XML files are not valid or well-formed.
186     * @throws IOException if an I/O error occurs processing the XML files
187     * @since 1.2
188     */
189    public ValidatorResources(final String... uris)
190            throws IOException, SAXException {
191
192        final Digester digester = initDigester();
193        for (final String element : uris) {
194            digester.push(this);
195            digester.parse(element);
196        }
197
198        process();
199    }
200
201    /**
202     * Create a ValidatorResources object from a URL.
203     *
204     * @param url The URL for the validation.xml
205     * configuration file that will be read into this object.
206     * @throws SAXException if the validation XML file are not valid or well-formed.
207     * @throws IOException if an I/O error occurs processing the XML files
208     * @since 1.3.1
209     */
210    public ValidatorResources(final URL url)
211            throws IOException, SAXException {
212        this(new URL[]{url});
213    }
214
215    /**
216     * Create a ValidatorResources object from several URL.
217     *
218     * @param urls An array of URL to several validation.xml
219     * configuration files that will be read in order and merged into this object.
220     * @throws SAXException if the validation XML files are not valid or well-formed.
221     * @throws IOException if an I/O error occurs processing the XML files
222     * @since 1.3.1
223     */
224    public ValidatorResources(final URL[] urls)
225            throws IOException, SAXException {
226
227        final Digester digester = initDigester();
228        for (final URL url : urls) {
229            digester.push(this);
230            digester.parse(url);
231        }
232
233        process();
234    }
235
236    /**
237     * Add a global constant to the resource.
238     *
239     * @param name The constant name.
240     * @param value The constant value.
241     */
242    public void addConstant(final String name, final String value) {
243        if (getLog().isDebugEnabled()) {
244            getLog().debug("Adding Global Constant: " + name + "," + value);
245        }
246
247        hConstants.put(name, value);
248    }
249
250    /**
251     * Add a {@code FormSet} to this {@code ValidatorResources}
252     * object.  It will be associated with the {@link Locale} of the
253     * {@code FormSet}.
254     *
255     * @param fs The form set to add.
256     * @since 1.1
257     */
258    public void addFormSet(final FormSet fs) {
259        final String key = buildKey(fs);
260        if (key.isEmpty()) { // there can only be one default formset
261            if (getLog().isWarnEnabled() && defaultFormSet != null) {
262                // warn the user he might not get the expected results
263                getLog().warn("Overriding default FormSet definition.");
264            }
265            defaultFormSet = fs;
266        } else {
267            final FormSet formset = getFormSets().get(key);
268            if (formset == null) { // it hasn't been included yet
269                if (getLog().isDebugEnabled()) {
270                    getLog().debug("Adding FormSet '" + fs + "'.");
271                }
272            } else if (getLog().isWarnEnabled()) { // warn the user he might not
273                                                   // get the expected results
274                getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
275            }
276            getFormSets().put(key, fs);
277        }
278    }
279
280    /**
281     * Create a {@code Rule} to handle {@code arg0-arg3}
282     * elements. This will allow validation.xml files that use the
283     * versions of the DTD prior to Validator 1.2.0 to continue
284     * working.
285     */
286    private void addOldArgRules(final Digester digester) {
287        // Create a new rule to process args elements
288        final Rule rule = new Rule() {
289            @Override
290            public void begin(final String namespace, final String name, final Attributes attributes) {
291                // Create the Arg
292                final Arg arg = new Arg();
293                arg.setKey(attributes.getValue("key"));
294                arg.setName(attributes.getValue("name"));
295                if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
296                    arg.setResource(false);
297                }
298                try {
299                    final int length = "arg".length(); // skip the arg prefix
300                    arg.setPosition(Integer.parseInt(name.substring(length)));
301                } catch (final Exception ex) {
302                    getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
303                }
304
305                // Add the arg to the parent field
306                ((Field) getDigester().peek(0)).addArg(arg);
307            }
308        };
309
310        // Add the rule for each of the arg elements
311        digester.addRule(ARGS_PATTERN + "0", rule);
312        digester.addRule(ARGS_PATTERN + "1", rule);
313        digester.addRule(ARGS_PATTERN + "2", rule);
314        digester.addRule(ARGS_PATTERN + "3", rule);
315
316    }
317
318    /**
319     * Add a {@code ValidatorAction} to the resource.  It also creates an
320     * instance of the class based on the {@code ValidatorAction}s
321     * class name and retrieves the {@code Method} instance and sets them
322     * in the {@code ValidatorAction}.
323     *
324     * @param va The validator action.
325     */
326    public void addValidatorAction(final ValidatorAction va) {
327        va.init();
328
329        getActions().put(va.getName(), va);
330
331        if (getLog().isDebugEnabled()) {
332            getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
333        }
334    }
335
336    /**
337     * Builds a key to store the {@code FormSet} under based on its
338     * language, country, and variant values.
339     *
340     * @param fs The Form Set.
341     * @return generated key for a formset.
342     */
343    protected String buildKey(final FormSet fs) {
344        return
345                buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
346    }
347
348    /**
349     * Assembles a Locale code from the given parts.
350     */
351    private String buildLocale(final String lang, final String country, final String variant) {
352        final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : "");
353        key.append(country != null && !country.isEmpty() ? "_" + country : "");
354        key.append(variant != null && !variant.isEmpty() ? "_" + variant : "");
355        return key.toString();
356    }
357
358    /**
359     * Returns a Map of String ValidatorAction names to their ValidatorAction.
360     *
361     * @return Map of Validator Actions
362     * @since 1.2.0
363     */
364    @SuppressWarnings("unchecked") // FastHashMap is not generic
365    protected Map<String, ValidatorAction> getActions() {
366        return hActions;
367    }
368
369    /**
370     * Returns a Map of String constant names to their String values.
371     *
372     * @return Map of Constants
373     * @since 1.2.0
374     */
375    @SuppressWarnings("unchecked") // FastHashMap is not generic
376    protected Map<String, String> getConstants() {
377        return hConstants;
378    }
379
380    /**
381     * Gets a {@code Form} based on the name of the form and the
382     * {@link Locale} that most closely matches the {@link Locale}
383     * passed in. The order of {@link Locale} matching is:
384     * <ol>
385     *    <li>language + country + variant</li>
386     *    <li>language + country</li>
387     *    <li>language</li>
388     *    <li>default locale</li>
389     * </ol>
390     *
391     * @param locale The Locale.
392     * @param formKey The key for the Form.
393     * @return The validator Form.
394     * @since 1.1
395     */
396    public Form getForm(final Locale locale, final String formKey) {
397        return this.getForm(locale.getLanguage(), locale.getCountry(), locale
398                .getVariant(), formKey);
399    }
400
401    /**
402     * Gets a {@code Form} based on the name of the form and the
403     * {@link Locale} that most closely matches the {@link Locale}
404     * passed in. The order of {@link Locale} matching is:
405     * <ol>
406     *    <li>language + country + variant</li>
407     *    <li>language + country</li>
408     *    <li>language</li>
409     *    <li>default locale</li>
410     * </ol>
411     *
412     * @param language The locale's language.
413     * @param country The locale's country.
414     * @param variant The locale's language variant.
415     * @param formKey The key for the Form.
416     * @return The validator Form.
417     * @since 1.1
418     */
419    public Form getForm(final String language, final String country, final String variant, final String formKey) {
420
421        Form form = null;
422
423        // Try language/country/variant
424        String key = buildLocale(language, country, variant);
425        if (!key.isEmpty()) {
426            final FormSet formSet = getFormSets().get(key);
427            if (formSet != null) {
428                form = formSet.getForm(formKey);
429            }
430        }
431        final String localeKey = key;
432
433        // Try language/country
434        if (form == null) {
435            key = buildLocale(language, country, null);
436            if (!key.isEmpty()) {
437                final FormSet formSet = getFormSets().get(key);
438                if (formSet != null) {
439                    form = formSet.getForm(formKey);
440                }
441            }
442        }
443
444        // Try language
445        if (form == null) {
446            key = buildLocale(language, null, null);
447            if (!key.isEmpty()) {
448                final FormSet formSet = getFormSets().get(key);
449                if (formSet != null) {
450                    form = formSet.getForm(formKey);
451                }
452            }
453        }
454
455        // Try default formset
456        if (form == null) {
457            form = defaultFormSet.getForm(formKey);
458            key = "default";
459        }
460
461        if (form == null) {
462            if (getLog().isWarnEnabled()) {
463                getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
464            }
465        } else if (getLog().isDebugEnabled()) {
466            getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
467        }
468
469        return form;
470
471    }
472
473    /**
474     * Gets a {@code FormSet} based on the language, country
475     *    and variant.
476     *
477     * @param language The locale's language.
478     * @param country The locale's country.
479     * @param variant The locale's language variant.
480     * @return The FormSet for a locale.
481     * @since 1.2
482     */
483    FormSet getFormSet(final String language, final String country, final String variant) {
484        final String key = buildLocale(language, country, variant);
485        if (key.isEmpty()) {
486            return defaultFormSet;
487        }
488        return getFormSets().get(key);
489    }
490
491    /**
492     * Returns a Map of String locale keys to Lists of their FormSets.
493     *
494     * @return Map of Form sets
495     * @since 1.2.0
496     */
497    @SuppressWarnings("unchecked") // FastHashMap is not generic
498    protected Map<String, FormSet> getFormSets() {
499        return hFormSets;
500    }
501
502    /**
503     * Accessor method for Log instance.
504     *
505     * The Log instance variable is transient and
506     * accessing it through this method ensures it
507     * is re-initialized when this instance is
508     * de-serialized.
509     *
510     * @return The Log instance.
511     */
512    private Log getLog() {
513        if (log == null) {
514            log = LogFactory.getLog(ValidatorResources.class);
515        }
516        return log;
517    }
518
519    /**
520     * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
521     * has a direct parent in the formSet with locale en_UK. If it doesn't
522     * exist, find the formSet with locale en, if no found get the
523     * defaultFormSet.
524     *
525     * @param fs
526     *            the formSet we want to get the parent from
527     * @return fs's parent
528     */
529    private FormSet getParent(final FormSet fs) {
530
531        FormSet parent = null;
532        if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
533            parent = defaultFormSet;
534        } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
535            parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
536            if (parent == null) {
537                parent = defaultFormSet;
538            }
539        } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
540            parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
541            if (parent == null) {
542                parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
543                if (parent == null) {
544                    parent = defaultFormSet;
545                }
546            }
547        }
548        return parent;
549    }
550
551    /**
552     * Gets a {@code ValidatorAction} based on its name.
553     *
554     * @param key The validator action key.
555     * @return The validator action.
556     */
557    public ValidatorAction getValidatorAction(final String key) {
558        return getActions().get(key);
559    }
560
561    /**
562     * Gets an unmodifiable {@link Map} of the {@code ValidatorAction}s.
563     *
564     * @return Map of validator actions.
565     */
566    public Map<String, ValidatorAction> getValidatorActions() {
567        return Collections.unmodifiableMap(getActions());
568    }
569
570    /**
571     *  Initialize the digester.
572     */
573    private Digester initDigester() {
574        URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
575        if (rulesUrl == null) {
576            // Fix for Issue# VALIDATOR-195
577            rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
578        }
579        if (getLog().isDebugEnabled()) {
580            getLog().debug("Loading rules from '" + rulesUrl + "'");
581        }
582        final Digester digester = DigesterLoader.createDigester(rulesUrl);
583        digester.setNamespaceAware(true);
584        digester.setValidating(true);
585        digester.setUseContextClassLoader(true);
586
587        // Add rules for arg0-arg3 elements
588        addOldArgRules(digester);
589
590        // register DTDs
591        for (int i = 0; i < REGISTRATIONS.length; i += 2) {
592            final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
593            if (url != null) {
594                digester.register(REGISTRATIONS[i], url.toString());
595            }
596        }
597        return digester;
598    }
599
600    /**
601     * Process the {@code ValidatorResources} object. Currently, sets the
602     * {@code FastHashMap} s to the 'fast' mode and call the processes
603     * all other resources. <strong>Note </strong>: The framework calls this
604     * automatically when ValidatorResources is created from an XML file. If you
605     * create an instance of this class by hand you <strong>must </strong> call
606     * this method when finished.
607     */
608    public void process() {
609        hFormSets.setFast(true);
610        hConstants.setFast(true);
611        hActions.setFast(true);
612
613        processForms();
614    }
615
616    /**
617     * Process the {@code Form} objects. This clones the {@code Field}s
618     * that don't exist in a {@code FormSet} compared to its parent
619     * {@code FormSet}.
620     */
621    private void processForms() {
622        if (defaultFormSet == null) { // it isn't mandatory to have a
623            // default formset
624            defaultFormSet = new FormSet();
625        }
626        defaultFormSet.process(getConstants());
627        // Loop through FormSets and merge if necessary
628        for (final String key : getFormSets().keySet()) {
629            final FormSet fs = getFormSets().get(key);
630            fs.merge(getParent(fs));
631        }
632
633        // Process Fully Constructed FormSets
634        for (final FormSet fs : getFormSets().values()) {
635            if (!fs.isProcessed()) {
636                fs.process(getConstants());
637            }
638        }
639    }
640
641}