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: 1227719 $ $Date: 2012-01-05 18:45:51 +0100 (Thu, 05 Jan 2012) $
054     */
055    public 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    }