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    protected static Locale defaultLocale = Locale.getDefault();
086
087    private static final String ARGS_PATTERN
088               = "form-validation/formset/form/field/arg";
089
090    private transient Log log = LogFactory.getLog(ValidatorResources.class);
091
092    /**
093     * {@link Map} of {@code FormSet}s stored under
094     * a {@link Locale} key (expressed as a String).
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     * @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}