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