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