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 *      http://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</code> objects based
039 * on their associated <code>Locale</code>.  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     * <code>Map</code> of <code>FormSet</code>s stored under
094     * a <code>Locale</code> 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     * <code>Map</code> 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     * <code>Map</code> of <code>ValidatorAction</code>s with
110     * the name of the <code>ValidatorAction</code> 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</code> (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
134     * formed.
135     * @throws IOException if an I/O error occurs processing the XML files
136     * @since 1.1
137     */
138    public ValidatorResources(final InputStream in) throws IOException, SAXException {
139        this(new InputStream[]{in});
140    }
141
142    /**
143     * Create a ValidatorResources object from an InputStream.
144     *
145     * @param streams An array of InputStreams to several validation.xml
146     * configuration files that will be read in order and merged into this object.
147     * It's the client's responsibility to close these streams.
148     * @throws SAXException if the validation XML files are not valid or well
149     * 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        this.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
173     * formed.
174     * @throws IOException if an I/O error occurs processing the XML files
175     * @since 1.2
176     */
177    public ValidatorResources(final String uri) throws IOException, SAXException {
178        this(new String[] { uri });
179    }
180
181    /**
182     * Create a ValidatorResources object from several uris
183     *
184     * @param uris An array of uris to several validation.xml
185     * configuration files that will be read in order and merged into this object.
186     * @throws SAXException if the validation XML files are not valid or well
187     * 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        this.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
209     * formed.
210     * @throws IOException if an I/O error occurs processing the XML files
211     * @since 1.3.1
212     */
213    public ValidatorResources(final URL url)
214            throws IOException, SAXException {
215        this(new URL[]{url});
216    }
217
218    /**
219     * Create a ValidatorResources object from several URL.
220     *
221     * @param urls An array of URL to several validation.xml
222     * configuration files that will be read in order and merged into this object.
223     * @throws SAXException if the validation XML files are not valid or well
224     * formed.
225     * @throws IOException if an I/O error occurs processing the XML files
226     * @since 1.3.1
227     */
228    public ValidatorResources(final URL[] urls)
229            throws IOException, SAXException {
230
231        final Digester digester = initDigester();
232        for (final URL url : urls) {
233            digester.push(this);
234            digester.parse(url);
235        }
236
237        this.process();
238    }
239
240    /**
241     * Add a global constant to the resource.
242     * @param name The constant name.
243     * @param value The constant value.
244     */
245    public void addConstant(final String name, final String value) {
246        if (getLog().isDebugEnabled()) {
247            getLog().debug("Adding Global Constant: " + name + "," + value);
248        }
249
250        this.hConstants.put(name, value);
251    }
252
253    /**
254     * Add a <code>FormSet</code> to this <code>ValidatorResources</code>
255     * object.  It will be associated with the <code>Locale</code> of the
256     * <code>FormSet</code>.
257     * @param fs The form set to add.
258     * @since 1.1
259     */
260    public void addFormSet(final FormSet fs) {
261        final String key = this.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</code> to handle <code>arg0-arg3</code>
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</code> to the resource.  It also creates an
322     * instance of the class based on the <code>ValidatorAction</code>s
323     * class name and retrieves the <code>Method</code> instance and sets them
324     * in the <code>ValidatorAction</code>.
325     * @param va The validator action.
326     */
327    public void addValidatorAction(final ValidatorAction va) {
328        va.init();
329
330        getActions().put(va.getName(), va);
331
332        if (getLog().isDebugEnabled()) {
333            getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
334        }
335    }
336
337    /**
338     * Builds a key to store the <code>FormSet</code> under based on it's
339     * language, country, and variant values.
340     * @param fs The Form Set.
341     * @return generated key for a formset.
342     */
343    protected String buildKey(final FormSet fs) {
344        return
345                this.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     * @return Map of Validator Actions
361     * @since 1.2.0
362     */
363    @SuppressWarnings("unchecked") // FastHashMap is not generic
364    protected Map<String, ValidatorAction> getActions() {
365        return hActions;
366    }
367
368    /**
369     * Returns a Map of String constant names to their String values.
370     * @return Map of Constants
371     * @since 1.2.0
372     */
373    @SuppressWarnings("unchecked") // FastHashMap is not generic
374    protected Map<String, String> getConstants() {
375        return hConstants;
376    }
377
378    /**
379     * <p>Gets a <code>Form</code> based on the name of the form and the
380     * <code>Locale</code> that most closely matches the <code>Locale</code>
381     * passed in.  The order of <code>Locale</code> matching is:</p>
382     * <ol>
383     *    <li>language + country + variant</li>
384     *    <li>language + country</li>
385     *    <li>language</li>
386     *    <li>default locale</li>
387     * </ol>
388     * @param locale The Locale.
389     * @param formKey The key for the Form.
390     * @return The validator Form.
391     * @since 1.1
392     */
393    public Form getForm(final Locale locale, final String formKey) {
394        return this.getForm(locale.getLanguage(), locale.getCountry(), locale
395                .getVariant(), formKey);
396    }
397
398    /**
399     * <p>Gets a <code>Form</code> based on the name of the form and the
400     * <code>Locale</code> that most closely matches the <code>Locale</code>
401     * passed in.  The order of <code>Locale</code> matching is:</p>
402     * <ol>
403     *    <li>language + country + variant</li>
404     *    <li>language + country</li>
405     *    <li>language</li>
406     *    <li>default locale</li>
407     * </ol>
408     * @param language The locale's language.
409     * @param country The locale's country.
410     * @param variant The locale's language variant.
411     * @param formKey The key for the Form.
412     * @return The validator Form.
413     * @since 1.1
414     */
415    public Form getForm(final String language, final String country, final String variant, final String formKey) {
416
417        Form form = null;
418
419        // Try language/country/variant
420        String key = this.buildLocale(language, country, variant);
421        if (!key.isEmpty()) {
422            final FormSet formSet = getFormSets().get(key);
423            if (formSet != null) {
424                form = formSet.getForm(formKey);
425            }
426        }
427        final String localeKey = key;
428
429        // Try language/country
430        if (form == null) {
431            key = buildLocale(language, country, null);
432            if (!key.isEmpty()) {
433                final FormSet formSet = getFormSets().get(key);
434                if (formSet != null) {
435                    form = formSet.getForm(formKey);
436                }
437            }
438        }
439
440        // Try language
441        if (form == null) {
442            key = buildLocale(language, null, null);
443            if (!key.isEmpty()) {
444                final FormSet formSet = getFormSets().get(key);
445                if (formSet != null) {
446                    form = formSet.getForm(formKey);
447                }
448            }
449        }
450
451        // Try default formset
452        if (form == null) {
453            form = defaultFormSet.getForm(formKey);
454            key = "default";
455        }
456
457        if (form == null) {
458            if (getLog().isWarnEnabled()) {
459                getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
460            }
461        } else if (getLog().isDebugEnabled()) {
462            getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
463        }
464
465        return form;
466
467    }
468
469    /**
470     * <p>Gets a <code>FormSet</code> based on the language, country
471     *    and variant.</p>
472     * @param language The locale's language.
473     * @param country The locale's country.
474     * @param variant The locale's language variant.
475     * @return The FormSet for a locale.
476     * @since 1.2
477     */
478    FormSet getFormSet(final String language, final String country, final String variant) {
479        final String key = buildLocale(language, country, variant);
480        if (key.isEmpty()) {
481            return defaultFormSet;
482        }
483        return getFormSets().get(key);
484    }
485
486    /**
487     * Returns a Map of String locale keys to Lists of their FormSets.
488     * @return Map of Form sets
489     * @since 1.2.0
490     */
491    @SuppressWarnings("unchecked") // FastHashMap is not generic
492    protected Map<String, FormSet> getFormSets() {
493        return hFormSets;
494    }
495
496    /**
497     * Accessor method for Log instance.
498     *
499     * The Log instance variable is transient and
500     * accessing it through this method ensures it
501     * is re-initialized when this instance is
502     * de-serialized.
503     *
504     * @return The Log instance.
505     */
506    private Log getLog() {
507        if (log == null) {
508            log = LogFactory.getLog(ValidatorResources.class);
509        }
510        return log;
511    }
512
513    /**
514     * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
515     * has a direct parent in the formSet with locale en_UK. If it doesn't
516     * exist, find the formSet with locale en, if no found get the
517     * defaultFormSet.
518     *
519     * @param fs
520     *            the formSet we want to get the parent from
521     * @return fs's parent
522     */
523    private FormSet getParent(final FormSet fs) {
524
525        FormSet parent = null;
526        if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
527            parent = defaultFormSet;
528        } else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
529            parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
530            if (parent == null) {
531                parent = defaultFormSet;
532            }
533        } else if (fs.getType() == FormSet.VARIANT_FORMSET) {
534            parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
535            if (parent == null) {
536                parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
537                if (parent == null) {
538                    parent = defaultFormSet;
539                }
540            }
541        }
542        return parent;
543    }
544
545    /**
546     * Gets a <code>ValidatorAction</code> based on it's name.
547     * @param key The validator action key.
548     * @return The validator action.
549     */
550    public ValidatorAction getValidatorAction(final String key) {
551        return getActions().get(key);
552    }
553
554    /**
555     * Gets an unmodifiable <code>Map</code> of the <code>ValidatorAction</code>s.
556     * @return Map of validator actions.
557     */
558    public Map<String, ValidatorAction> getValidatorActions() {
559        return Collections.unmodifiableMap(getActions());
560    }
561
562    /**
563     *  Initialize the digester.
564     */
565    private Digester initDigester() {
566        URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
567        if (rulesUrl == null) {
568            // Fix for Issue# VALIDATOR-195
569            rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
570        }
571        if (getLog().isDebugEnabled()) {
572            getLog().debug("Loading rules from '" + rulesUrl + "'");
573        }
574        final Digester digester = DigesterLoader.createDigester(rulesUrl);
575        digester.setNamespaceAware(true);
576        digester.setValidating(true);
577        digester.setUseContextClassLoader(true);
578
579        // Add rules for arg0-arg3 elements
580        addOldArgRules(digester);
581
582        // register DTDs
583        for (int i = 0; i < REGISTRATIONS.length; i += 2) {
584            final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
585            if (url != null) {
586                digester.register(REGISTRATIONS[i], url.toString());
587            }
588        }
589        return digester;
590    }
591
592    /**
593     * Process the <code>ValidatorResources</code> object. Currently sets the
594     * <code>FastHashMap</code> s to the 'fast' mode and call the processes
595     * all other resources. <strong>Note </strong>: The framework calls this
596     * automatically when ValidatorResources is created from an XML file. If you
597     * create an instance of this class by hand you <strong>must </strong> call
598     * this method when finished.
599     */
600    public void process() {
601        hFormSets.setFast(true);
602        hConstants.setFast(true);
603        hActions.setFast(true);
604
605        this.processForms();
606    }
607
608    /**
609     * <p>Process the <code>Form</code> objects.  This clones the <code>Field</code>s
610     * that don't exist in a <code>FormSet</code> compared to its parent
611     * <code>FormSet</code>.</p>
612     */
613    private void processForms() {
614        if (defaultFormSet == null) { // it isn't mandatory to have a
615            // default formset
616            defaultFormSet = new FormSet();
617        }
618        defaultFormSet.process(getConstants());
619        // Loop through FormSets and merge if necessary
620        for (final String key : getFormSets().keySet()) {
621            final FormSet fs = getFormSets().get(key);
622            fs.merge(getParent(fs));
623        }
624
625        // Process Fully Constructed FormSets
626        for (final FormSet fs : getFormSets().values()) {
627            if (!fs.isProcessed()) {
628                fs.process(getConstants());
629            }
630        }
631    }
632
633}