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