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