ValidatorResources.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.validator;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.collections.FastHashMap; // DEPRECATED
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* <p>
* General purpose class for storing <code>FormSet</code> objects based
* on their associated <code>Locale</code>. Instances of this class are usually
* configured through a validation.xml file that is parsed in a constructor.
* </p>
*
* <p><strong>Note</strong> - Classes that extend this class
* must be Serializable so that instances may be used in distributable
* application server environments.</p>
*
* <p>
* The use of FastHashMap is deprecated and will be replaced in a future
* release.
* </p>
*/
//TODO mutable non-private fields
public class ValidatorResources implements Serializable {
private static final long serialVersionUID = -8203745881446239554L;
/** Name of the digester validator rules file */
private static final String VALIDATOR_RULES = "digester-rules.xml";
/**
* The set of public identifiers, and corresponding resource names, for
* the versions of the configuration file DTDs that we know about. There
* <strong>MUST</strong> be an even number of Strings in this list!
*/
private static final String[] REGISTRATIONS = {
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN",
"/org/apache/commons/validator/resources/validator_1_0.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN",
"/org/apache/commons/validator/resources/validator_1_0_1.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN",
"/org/apache/commons/validator/resources/validator_1_1.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN",
"/org/apache/commons/validator/resources/validator_1_1_3.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN",
"/org/apache/commons/validator/resources/validator_1_2_0.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN",
"/org/apache/commons/validator/resources/validator_1_3_0.dtd",
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN",
"/org/apache/commons/validator/resources/validator_1_4_0.dtd"
};
/**
* The default locale on our server.
*/
protected static Locale defaultLocale = Locale.getDefault();
private static final String ARGS_PATTERN
= "form-validation/formset/form/field/arg";
private transient Log log = LogFactory.getLog(ValidatorResources.class);
/**
* <code>Map</code> of <code>FormSet</code>s stored under
* a <code>Locale</code> key (expressed as a String).
* @deprecated Subclasses should use getFormSets() instead.
*/
@Deprecated
protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet>
/**
* <code>Map</code> of global constant values with
* the name of the constant as the key.
* @deprecated Subclasses should use getConstants() instead.
*/
@Deprecated
protected FastHashMap hConstants = new FastHashMap(); // <String, String>
/**
* <code>Map</code> of <code>ValidatorAction</code>s with
* the name of the <code>ValidatorAction</code> as the key.
* @deprecated Subclasses should use getActions() instead.
*/
@Deprecated
protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction>
/**
* This is the default <code>FormSet</code> (without locale). (We probably don't need
* the defaultLocale anymore.)
*/
protected FormSet defaultFormSet;
/**
* Create an empty ValidatorResources object.
*/
public ValidatorResources() {
}
/**
* Create a ValidatorResources object from an InputStream.
*
* @param in InputStream to a validation.xml configuration file. It's the client's
* responsibility to close this stream.
* @throws SAXException if the validation XML files are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.1
*/
public ValidatorResources(final InputStream in) throws IOException, SAXException {
this(new InputStream[]{in});
}
/**
* Create a ValidatorResources object from an InputStream.
*
* @param streams An array of InputStreams to several validation.xml
* configuration files that will be read in order and merged into this object.
* It's the client's responsibility to close these streams.
* @throws SAXException if the validation XML files are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.1
*/
public ValidatorResources(final InputStream[] streams)
throws IOException, SAXException {
final Digester digester = initDigester();
for (int i = 0; i < streams.length; i++) {
if (streams[i] == null) {
throw new IllegalArgumentException("Stream[" + i + "] is null");
}
digester.push(this);
digester.parse(streams[i]);
}
this.process();
}
/**
* Create a ValidatorResources object from an uri
*
* @param uri The location of a validation.xml configuration file.
* @throws SAXException if the validation XML files are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.2
*/
public ValidatorResources(final String uri) throws IOException, SAXException {
this(new String[] { uri });
}
/**
* Create a ValidatorResources object from several uris
*
* @param uris An array of uris to several validation.xml
* configuration files that will be read in order and merged into this object.
* @throws SAXException if the validation XML files are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.2
*/
public ValidatorResources(final String... uris)
throws IOException, SAXException {
final Digester digester = initDigester();
for (final String element : uris) {
digester.push(this);
digester.parse(element);
}
this.process();
}
/**
* Create a ValidatorResources object from a URL.
*
* @param url The URL for the validation.xml
* configuration file that will be read into this object.
* @throws SAXException if the validation XML file are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.3.1
*/
public ValidatorResources(final URL url)
throws IOException, SAXException {
this(new URL[]{url});
}
/**
* Create a ValidatorResources object from several URL.
*
* @param urls An array of URL to several validation.xml
* configuration files that will be read in order and merged into this object.
* @throws SAXException if the validation XML files are not valid or well
* formed.
* @throws IOException if an I/O error occurs processing the XML files
* @since 1.3.1
*/
public ValidatorResources(final URL[] urls)
throws IOException, SAXException {
final Digester digester = initDigester();
for (final URL url : urls) {
digester.push(this);
digester.parse(url);
}
this.process();
}
/**
* Add a global constant to the resource.
* @param name The constant name.
* @param value The constant value.
*/
public void addConstant(final String name, final String value) {
if (getLog().isDebugEnabled()) {
getLog().debug("Adding Global Constant: " + name + "," + value);
}
this.hConstants.put(name, value);
}
/**
* Add a <code>FormSet</code> to this <code>ValidatorResources</code>
* object. It will be associated with the <code>Locale</code> of the
* <code>FormSet</code>.
* @param fs The form set to add.
* @since 1.1
*/
public void addFormSet(final FormSet fs) {
final String key = this.buildKey(fs);
if (key.isEmpty()) { // there can only be one default formset
if (getLog().isWarnEnabled() && defaultFormSet != null) {
// warn the user he might not get the expected results
getLog().warn("Overriding default FormSet definition.");
}
defaultFormSet = fs;
} else {
final FormSet formset = getFormSets().get(key);
if (formset == null) { // it hasn't been included yet
if (getLog().isDebugEnabled()) {
getLog().debug("Adding FormSet '" + fs + "'.");
}
} else if (getLog().isWarnEnabled()) { // warn the user he might not
// get the expected results
getLog().warn("Overriding FormSet definition. Duplicate for locale: " + key);
}
getFormSets().put(key, fs);
}
}
/**
* Create a <code>Rule</code> to handle <code>arg0-arg3</code>
* elements. This will allow validation.xml files that use the
* versions of the DTD prior to Validator 1.2.0 to continue
* working.
*/
private void addOldArgRules(final Digester digester) {
// Create a new rule to process args elements
final Rule rule = new Rule() {
@Override
public void begin(final String namespace, final String name, final Attributes attributes) {
// Create the Arg
final Arg arg = new Arg();
arg.setKey(attributes.getValue("key"));
arg.setName(attributes.getValue("name"));
if ("false".equalsIgnoreCase(attributes.getValue("resource"))) {
arg.setResource(false);
}
try {
final int length = "arg".length(); // skip the arg prefix
arg.setPosition(Integer.parseInt(name.substring(length)));
} catch (final Exception ex) {
getLog().error("Error parsing Arg position: " + name + " " + arg + " " + ex);
}
// Add the arg to the parent field
((Field) getDigester().peek(0)).addArg(arg);
}
};
// Add the rule for each of the arg elements
digester.addRule(ARGS_PATTERN + "0", rule);
digester.addRule(ARGS_PATTERN + "1", rule);
digester.addRule(ARGS_PATTERN + "2", rule);
digester.addRule(ARGS_PATTERN + "3", rule);
}
/**
* Add a <code>ValidatorAction</code> to the resource. It also creates an
* instance of the class based on the <code>ValidatorAction</code>s
* class name and retrieves the <code>Method</code> instance and sets them
* in the <code>ValidatorAction</code>.
* @param va The validator action.
*/
public void addValidatorAction(final ValidatorAction va) {
va.init();
getActions().put(va.getName(), va);
if (getLog().isDebugEnabled()) {
getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname());
}
}
/**
* Builds a key to store the <code>FormSet</code> under based on it's
* language, country, and variant values.
* @param fs The Form Set.
* @return generated key for a formset.
*/
protected String buildKey(final FormSet fs) {
return
this.buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant());
}
/**
* Assembles a Locale code from the given parts.
*/
private String buildLocale(final String lang, final String country, final String variant) {
final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : "");
key.append(country != null && !country.isEmpty() ? "_" + country : "");
key.append(variant != null && !variant.isEmpty() ? "_" + variant : "");
return key.toString();
}
/**
* Returns a Map of String ValidatorAction names to their ValidatorAction.
* @return Map of Validator Actions
* @since 1.2.0
*/
@SuppressWarnings("unchecked") // FastHashMap is not generic
protected Map<String, ValidatorAction> getActions() {
return hActions;
}
/**
* Returns a Map of String constant names to their String values.
* @return Map of Constants
* @since 1.2.0
*/
@SuppressWarnings("unchecked") // FastHashMap is not generic
protected Map<String, String> getConstants() {
return hConstants;
}
/**
* <p>Gets a <code>Form</code> based on the name of the form and the
* <code>Locale</code> that most closely matches the <code>Locale</code>
* passed in. The order of <code>Locale</code> matching is:</p>
* <ol>
* <li>language + country + variant</li>
* <li>language + country</li>
* <li>language</li>
* <li>default locale</li>
* </ol>
* @param locale The Locale.
* @param formKey The key for the Form.
* @return The validator Form.
* @since 1.1
*/
public Form getForm(final Locale locale, final String formKey) {
return this.getForm(locale.getLanguage(), locale.getCountry(), locale
.getVariant(), formKey);
}
/**
* <p>Gets a <code>Form</code> based on the name of the form and the
* <code>Locale</code> that most closely matches the <code>Locale</code>
* passed in. The order of <code>Locale</code> matching is:</p>
* <ol>
* <li>language + country + variant</li>
* <li>language + country</li>
* <li>language</li>
* <li>default locale</li>
* </ol>
* @param language The locale's language.
* @param country The locale's country.
* @param variant The locale's language variant.
* @param formKey The key for the Form.
* @return The validator Form.
* @since 1.1
*/
public Form getForm(final String language, final String country, final String variant, final String formKey) {
Form form = null;
// Try language/country/variant
String key = this.buildLocale(language, country, variant);
if (!key.isEmpty()) {
final FormSet formSet = getFormSets().get(key);
if (formSet != null) {
form = formSet.getForm(formKey);
}
}
final String localeKey = key;
// Try language/country
if (form == null) {
key = buildLocale(language, country, null);
if (!key.isEmpty()) {
final FormSet formSet = getFormSets().get(key);
if (formSet != null) {
form = formSet.getForm(formKey);
}
}
}
// Try language
if (form == null) {
key = buildLocale(language, null, null);
if (!key.isEmpty()) {
final FormSet formSet = getFormSets().get(key);
if (formSet != null) {
form = formSet.getForm(formKey);
}
}
}
// Try default formset
if (form == null) {
form = defaultFormSet.getForm(formKey);
key = "default";
}
if (form == null) {
if (getLog().isWarnEnabled()) {
getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'");
}
} else if (getLog().isDebugEnabled()) {
getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'");
}
return form;
}
/**
* <p>Gets a <code>FormSet</code> based on the language, country
* and variant.</p>
* @param language The locale's language.
* @param country The locale's country.
* @param variant The locale's language variant.
* @return The FormSet for a locale.
* @since 1.2
*/
FormSet getFormSet(final String language, final String country, final String variant) {
final String key = buildLocale(language, country, variant);
if (key.isEmpty()) {
return defaultFormSet;
}
return getFormSets().get(key);
}
/**
* Returns a Map of String locale keys to Lists of their FormSets.
* @return Map of Form sets
* @since 1.2.0
*/
@SuppressWarnings("unchecked") // FastHashMap is not generic
protected Map<String, FormSet> getFormSets() {
return hFormSets;
}
/**
* Accessor method for Log instance.
*
* The Log instance variable is transient and
* accessing it through this method ensures it
* is re-initialized when this instance is
* de-serialized.
*
* @return The Log instance.
*/
private Log getLog() {
if (log == null) {
log = LogFactory.getLog(ValidatorResources.class);
}
return log;
}
/**
* Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1
* has a direct parent in the formSet with locale en_UK. If it doesn't
* exist, find the formSet with locale en, if no found get the
* defaultFormSet.
*
* @param fs
* the formSet we want to get the parent from
* @return fs's parent
*/
private FormSet getParent(final FormSet fs) {
FormSet parent = null;
if (fs.getType() == FormSet.LANGUAGE_FORMSET) {
parent = defaultFormSet;
} else if (fs.getType() == FormSet.COUNTRY_FORMSET) {
parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
if (parent == null) {
parent = defaultFormSet;
}
} else if (fs.getType() == FormSet.VARIANT_FORMSET) {
parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null));
if (parent == null) {
parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null));
if (parent == null) {
parent = defaultFormSet;
}
}
}
return parent;
}
/**
* Gets a <code>ValidatorAction</code> based on it's name.
* @param key The validator action key.
* @return The validator action.
*/
public ValidatorAction getValidatorAction(final String key) {
return getActions().get(key);
}
/**
* Gets an unmodifiable <code>Map</code> of the <code>ValidatorAction</code>s.
* @return Map of validator actions.
*/
public Map<String, ValidatorAction> getValidatorActions() {
return Collections.unmodifiableMap(getActions());
}
/**
* Initialize the digester.
*/
private Digester initDigester() {
URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES);
if (rulesUrl == null) {
// Fix for Issue# VALIDATOR-195
rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES);
}
if (getLog().isDebugEnabled()) {
getLog().debug("Loading rules from '" + rulesUrl + "'");
}
final Digester digester = DigesterLoader.createDigester(rulesUrl);
digester.setNamespaceAware(true);
digester.setValidating(true);
digester.setUseContextClassLoader(true);
// Add rules for arg0-arg3 elements
addOldArgRules(digester);
// register DTDs
for (int i = 0; i < REGISTRATIONS.length; i += 2) {
final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]);
if (url != null) {
digester.register(REGISTRATIONS[i], url.toString());
}
}
return digester;
}
/**
* Process the <code>ValidatorResources</code> object. Currently sets the
* <code>FastHashMap</code> s to the 'fast' mode and call the processes
* all other resources. <strong>Note </strong>: The framework calls this
* automatically when ValidatorResources is created from an XML file. If you
* create an instance of this class by hand you <strong>must </strong> call
* this method when finished.
*/
public void process() {
hFormSets.setFast(true);
hConstants.setFast(true);
hActions.setFast(true);
this.processForms();
}
/**
* <p>Process the <code>Form</code> objects. This clones the <code>Field</code>s
* that don't exist in a <code>FormSet</code> compared to its parent
* <code>FormSet</code>.</p>
*/
private void processForms() {
if (defaultFormSet == null) { // it isn't mandatory to have a
// default formset
defaultFormSet = new FormSet();
}
defaultFormSet.process(getConstants());
// Loop through FormSets and merge if necessary
for (final String key : getFormSets().keySet()) {
final FormSet fs = getFormSets().get(key);
fs.merge(getParent(fs));
}
// Process Fully Constructed FormSets
for (final FormSet fs : getFormSets().values()) {
if (!fs.isProcessed()) {
fs.process(getConstants());
}
}
}
}