001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.validator; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.Serializable; 022import java.net.URL; 023import java.util.Collections; 024import java.util.Iterator; 025import java.util.Locale; 026import java.util.Map; 027 028import org.apache.commons.collections.FastHashMap; // DEPRECATED 029import org.apache.commons.digester.Digester; 030import org.apache.commons.digester.Rule; 031import org.apache.commons.digester.xmlrules.DigesterLoader; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.xml.sax.SAXException; 035import org.xml.sax.Attributes; 036 037/** 038 * <p> 039 * General purpose class for storing <code>FormSet</code> objects based 040 * on their associated <code>Locale</code>. Instances of this class are usually 041 * configured through a validation.xml file that is parsed in a constructor. 042 * </p> 043 * 044 * <p><strong>Note</strong> - Classes that extend this class 045 * must be Serializable so that instances may be used in distributable 046 * application server environments.</p> 047 * 048 * <p> 049 * The use of FastHashMap is deprecated and will be replaced in a future 050 * release. 051 * </p> 052 * 053 * @version $Revision$ 054 */ 055//TODO mutable non-private fields 056public class ValidatorResources implements Serializable { 057 058 private static final long serialVersionUID = -8203745881446239554L; 059 060 /** Name of the digester validator rules file */ 061 private static final String VALIDATOR_RULES = "digester-rules.xml"; 062 063 /** 064 * The set of public identifiers, and corresponding resource names, for 065 * the versions of the configuration file DTDs that we know about. There 066 * <strong>MUST</strong> be an even number of Strings in this list! 067 */ 068 private static final String REGISTRATIONS[] = { 069 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0//EN", 070 "/org/apache/commons/validator/resources/validator_1_0.dtd", 071 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.0.1//EN", 072 "/org/apache/commons/validator/resources/validator_1_0_1.dtd", 073 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN", 074 "/org/apache/commons/validator/resources/validator_1_1.dtd", 075 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN", 076 "/org/apache/commons/validator/resources/validator_1_1_3.dtd", 077 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.2.0//EN", 078 "/org/apache/commons/validator/resources/validator_1_2_0.dtd", 079 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.3.0//EN", 080 "/org/apache/commons/validator/resources/validator_1_3_0.dtd", 081 "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN", 082 "/org/apache/commons/validator/resources/validator_1_4_0.dtd" 083 }; 084 085 private transient Log log = LogFactory.getLog(ValidatorResources.class); 086 087 /** 088 * <code>Map</code> of <code>FormSet</code>s stored under 089 * a <code>Locale</code> key (expressed as a String). 090 * @deprecated Subclasses should use getFormSets() instead. 091 */ 092 @Deprecated 093 protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet> 094 095 /** 096 * <code>Map</code> of global constant values with 097 * the name of the constant as the key. 098 * @deprecated Subclasses should use getConstants() instead. 099 */ 100 @Deprecated 101 protected FastHashMap hConstants = new FastHashMap(); // <String, String> 102 103 /** 104 * <code>Map</code> of <code>ValidatorAction</code>s with 105 * the name of the <code>ValidatorAction</code> as the key. 106 * @deprecated Subclasses should use getActions() instead. 107 */ 108 @Deprecated 109 protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction> 110 111 /** 112 * The default locale on our server. 113 */ 114 protected static Locale defaultLocale = Locale.getDefault(); 115 116 /** 117 * Create an empty ValidatorResources object. 118 */ 119 public ValidatorResources() { 120 super(); 121 } 122 123 /** 124 * This is the default <code>FormSet</code> (without locale). (We probably don't need 125 * the defaultLocale anymore.) 126 */ 127 protected FormSet defaultFormSet; 128 129 /** 130 * Create a ValidatorResources object from an InputStream. 131 * 132 * @param in InputStream to a validation.xml configuration file. It's the client's 133 * responsibility to close this stream. 134 * @throws SAXException if the validation XML files are not valid or well 135 * formed. 136 * @throws IOException if an I/O error occurs processing the XML files 137 * @since Validator 1.1 138 */ 139 public ValidatorResources(InputStream in) throws IOException, SAXException { 140 this(new InputStream[]{in}); 141 } 142 143 /** 144 * Create a ValidatorResources object from an InputStream. 145 * 146 * @param streams An array of InputStreams to several validation.xml 147 * configuration files that will be read in order and merged into this object. 148 * It's the client's responsibility to close these streams. 149 * @throws SAXException if the validation XML files are not valid or well 150 * formed. 151 * @throws IOException if an I/O error occurs processing the XML files 152 * @since Validator 1.1 153 */ 154 public ValidatorResources(InputStream[] streams) 155 throws IOException, SAXException { 156 157 super(); 158 159 Digester digester = initDigester(); 160 for (int i = 0; i < streams.length; i++) { 161 if (streams[i] == null) { 162 throw new IllegalArgumentException("Stream[" + i + "] is null"); 163 } 164 digester.push(this); 165 digester.parse(streams[i]); 166 } 167 168 this.process(); 169 } 170 171 /** 172 * Create a ValidatorResources object from an uri 173 * 174 * @param uri The location of a validation.xml configuration file. 175 * @throws SAXException if the validation XML files are not valid or well 176 * formed. 177 * @throws IOException if an I/O error occurs processing the XML files 178 * @since Validator 1.2 179 */ 180 public ValidatorResources(String uri) throws IOException, SAXException { 181 this(new String[]{uri}); 182 } 183 184 /** 185 * Create a ValidatorResources object from several uris 186 * 187 * @param uris An array of uris to several validation.xml 188 * configuration files that will be read in order and merged into this object. 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 SAXException if the validation XML file are not valid or well 214 * formed. 215 * @throws IOException if an I/O error occurs processing the XML files 216 * @since Validator 1.3.1 217 */ 218 public ValidatorResources(URL url) 219 throws IOException, SAXException { 220 this(new URL[]{url}); 221 } 222 223 /** 224 * Create a ValidatorResources object from several URL. 225 * 226 * @param urls An array of URL to several validation.xml 227 * configuration files that will be read in order and merged into this object. 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 @Override 291 public void begin(String namespace, String name, 292 Attributes attributes) throws Exception { 293 // Create the Arg 294 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 (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 = getFormSets().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 getFormSets().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 getActions().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 getActions().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<String, ValidatorAction> getValidatorActions() { 396 return Collections.unmodifiableMap(getActions()); 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 = getFormSets().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 = getFormSets().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 = getFormSets().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(getConstants()); 544 // Loop through FormSets and merge if necessary 545 for (Iterator<String> i = getFormSets().keySet().iterator(); i.hasNext();) { 546 String key = i.next(); 547 FormSet fs = getFormSets().get(key); 548 fs.merge(getParent(fs)); 549 } 550 551 // Process Fully Constructed FormSets 552 for (Iterator<FormSet> i = getFormSets().values().iterator(); i.hasNext();) { 553 FormSet fs = i.next(); 554 if (!fs.isProcessed()) { 555 fs.process(getConstants()); 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 = getFormSets().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 = getFormSets().get(buildLocale(fs.getLanguage(), fs 583 .getCountry(), null)); 584 if (parent == null) { 585 parent = getFormSets().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 getFormSets().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 @SuppressWarnings("unchecked") // FastHashMap is not generic 621 protected Map<String, FormSet> getFormSets() { 622 return hFormSets; 623 } 624 625 /** 626 * Returns a Map of String constant names to their String values. 627 * @return Map of Constants 628 * @since Validator 1.2.0 629 */ 630 @SuppressWarnings("unchecked") // FastHashMap is not generic 631 protected Map<String, String> getConstants() { 632 return hConstants; 633 } 634 635 /** 636 * Returns a Map of String ValidatorAction names to their ValidatorAction. 637 * @return Map of Validator Actions 638 * @since Validator 1.2.0 639 */ 640 @SuppressWarnings("unchecked") // FastHashMap is not generic 641 protected Map<String, ValidatorAction> getActions() { 642 return hActions; 643 } 644 645 /** 646 * Accessor method for Log instance. 647 * 648 * The Log instance variable is transient and 649 * accessing it through this method ensures it 650 * is re-initialized when this instance is 651 * de-serialized. 652 * 653 * @return The Log instance. 654 */ 655 private Log getLog() { 656 if (log == null) { 657 log = LogFactory.getLog(ValidatorResources.class); 658 } 659 return log; 660 } 661 662}