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