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 * https://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} objects based 039 * on their associated {@link Locale}. 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 @Deprecated // ought to be final, but is not used, so could be dropped 086 protected static Locale defaultLocale = Locale.getDefault(); // NOPMD not used 087 088 private static final String ARGS_PATTERN 089 = "form-validation/formset/form/field/arg"; 090 091 private transient Log log = LogFactory.getLog(ValidatorResources.class); 092 093 /** 094 * {@link Map} of {@code FormSet}s stored under 095 * a {@link Locale} key (expressed as a String). 096 * 097 * @deprecated Subclasses should use getFormSets() instead. 098 */ 099 @Deprecated 100 protected FastHashMap hFormSets = new FastHashMap(); // <String, FormSet> 101 102 /** 103 * {@link Map} of global constant values with 104 * the name of the constant as the key. 105 * 106 * @deprecated Subclasses should use getConstants() instead. 107 */ 108 @Deprecated 109 protected FastHashMap hConstants = new FastHashMap(); // <String, String> 110 111 /** 112 * {@link Map} of {@code ValidatorAction}s with 113 * the name of the {@code ValidatorAction} as the key. 114 * 115 * @deprecated Subclasses should use getActions() instead. 116 */ 117 @Deprecated 118 protected FastHashMap hActions = new FastHashMap(); // <String, ValidatorAction> 119 120 /** 121 * This is the default {@code FormSet} (without locale). (We probably don't need 122 * the defaultLocale anymore.) 123 */ 124 protected FormSet defaultFormSet; 125 126 /** 127 * Create an empty ValidatorResources object. 128 */ 129 public ValidatorResources() { 130 } 131 132 /** 133 * Create a ValidatorResources object from an InputStream. 134 * 135 * @param in InputStream to a validation.xml configuration file. It's the client's 136 * responsibility to close this stream. 137 * @throws SAXException if the validation XML files are not valid or well-formed. 138 * @throws IOException if an I/O error occurs processing the XML files 139 * @since 1.1 140 */ 141 public ValidatorResources(final InputStream in) throws IOException, SAXException { 142 this(new InputStream[]{in}); 143 } 144 145 /** 146 * Create a ValidatorResources object from an InputStream. 147 * 148 * @param streams An array of InputStreams to several validation.xml 149 * configuration files that will be read in order and merged into this object. 150 * It's the client's responsibility to close these streams. 151 * @throws SAXException if the validation XML files are not valid or well-formed. 152 * @throws IOException if an I/O error occurs processing the XML files 153 * @since 1.1 154 */ 155 public ValidatorResources(final InputStream[] streams) 156 throws IOException, SAXException { 157 158 final Digester digester = initDigester(); 159 for (int i = 0; i < streams.length; i++) { 160 if (streams[i] == null) { 161 throw new IllegalArgumentException("Stream[" + i + "] is null"); 162 } 163 digester.push(this); 164 digester.parse(streams[i]); 165 } 166 167 process(); 168 } 169 170 /** 171 * Create a ValidatorResources object from an uri 172 * 173 * @param uri The location of a validation.xml configuration file. 174 * @throws SAXException if the validation XML files are not valid or well-formed. 175 * @throws IOException if an I/O error occurs processing the XML files 176 * @since 1.2 177 */ 178 public ValidatorResources(final String uri) throws IOException, SAXException { 179 this(new String[] { uri }); 180 } 181 182 /** 183 * Create a ValidatorResources object from several uris 184 * 185 * @param uris An array of uris to several validation.xml 186 * configuration files that will be read in order and merged into this object. 187 * @throws SAXException if the validation XML files are not valid or well-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 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-formed. 209 * @throws IOException if an I/O error occurs processing the XML files 210 * @since 1.3.1 211 */ 212 public ValidatorResources(final URL url) 213 throws IOException, SAXException { 214 this(new URL[]{url}); 215 } 216 217 /** 218 * Create a ValidatorResources object from several URL. 219 * 220 * @param urls An array of URL to several validation.xml 221 * configuration files that will be read in order and merged into this object. 222 * @throws SAXException if the validation XML files are not valid or well-formed. 223 * @throws IOException if an I/O error occurs processing the XML files 224 * @since 1.3.1 225 */ 226 public ValidatorResources(final URL[] urls) 227 throws IOException, SAXException { 228 229 final Digester digester = initDigester(); 230 for (final URL url : urls) { 231 digester.push(this); 232 digester.parse(url); 233 } 234 235 process(); 236 } 237 238 /** 239 * Add a global constant to the resource. 240 * 241 * @param name The constant name. 242 * @param value The constant value. 243 */ 244 public void addConstant(final String name, final String value) { 245 if (getLog().isDebugEnabled()) { 246 getLog().debug("Adding Global Constant: " + name + "," + value); 247 } 248 249 hConstants.put(name, value); 250 } 251 252 /** 253 * Add a {@code FormSet} to this {@code ValidatorResources} 254 * object. It will be associated with the {@link Locale} of the 255 * {@code FormSet}. 256 * 257 * @param fs The form set to add. 258 * @since 1.1 259 */ 260 public void addFormSet(final FormSet fs) { 261 final String key = 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} to handle {@code arg0-arg3} 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} to the resource. It also creates an 322 * instance of the class based on the {@code ValidatorAction}s 323 * class name and retrieves the {@code Method} instance and sets them 324 * in the {@code ValidatorAction}. 325 * 326 * @param va The validator action. 327 */ 328 public void addValidatorAction(final ValidatorAction va) { 329 va.init(); 330 331 getActions().put(va.getName(), va); 332 333 if (getLog().isDebugEnabled()) { 334 getLog().debug("Add ValidatorAction: " + va.getName() + "," + va.getClassname()); 335 } 336 } 337 338 /** 339 * Builds a key to store the {@code FormSet} under based on its 340 * language, country, and variant values. 341 * 342 * @param fs The Form Set. 343 * @return generated key for a formset. 344 */ 345 protected String buildKey(final FormSet fs) { 346 return 347 buildLocale(fs.getLanguage(), fs.getCountry(), fs.getVariant()); 348 } 349 350 /** 351 * Assembles a Locale code from the given parts. 352 */ 353 private String buildLocale(final String lang, final String country, final String variant) { 354 final StringBuilder key = new StringBuilder().append(lang != null && !lang.isEmpty() ? lang : ""); 355 key.append(country != null && !country.isEmpty() ? "_" + country : ""); 356 key.append(variant != null && !variant.isEmpty() ? "_" + variant : ""); 357 return key.toString(); 358 } 359 360 /** 361 * Returns a Map of String ValidatorAction names to their ValidatorAction. 362 * 363 * @return Map of Validator Actions 364 * @since 1.2.0 365 */ 366 @SuppressWarnings("unchecked") // FastHashMap is not generic 367 protected Map<String, ValidatorAction> getActions() { 368 return hActions; 369 } 370 371 /** 372 * Returns a Map of String constant names to their String values. 373 * 374 * @return Map of Constants 375 * @since 1.2.0 376 */ 377 @SuppressWarnings("unchecked") // FastHashMap is not generic 378 protected Map<String, String> getConstants() { 379 return hConstants; 380 } 381 382 /** 383 * <p>Gets a {@code Form} based on the name of the form and the 384 * {@link Locale} that most closely matches the {@link Locale} 385 * passed in. The order of {@link Locale} matching is:</p> 386 * <ol> 387 * <li>language + country + variant</li> 388 * <li>language + country</li> 389 * <li>language</li> 390 * <li>default locale</li> 391 * </ol> 392 * 393 * @param locale The Locale. 394 * @param formKey The key for the Form. 395 * @return The validator Form. 396 * @since 1.1 397 */ 398 public Form getForm(final Locale locale, final String formKey) { 399 return this.getForm(locale.getLanguage(), locale.getCountry(), locale 400 .getVariant(), formKey); 401 } 402 403 /** 404 * <p>Gets a {@code Form} based on the name of the form and the 405 * {@link Locale} that most closely matches the {@link Locale} 406 * passed in. The order of {@link Locale} matching is:</p> 407 * <ol> 408 * <li>language + country + variant</li> 409 * <li>language + country</li> 410 * <li>language</li> 411 * <li>default locale</li> 412 * </ol> 413 * 414 * @param language The locale's language. 415 * @param country The locale's country. 416 * @param variant The locale's language variant. 417 * @param formKey The key for the Form. 418 * @return The validator Form. 419 * @since 1.1 420 */ 421 public Form getForm(final String language, final String country, final String variant, final String formKey) { 422 423 Form form = null; 424 425 // Try language/country/variant 426 String key = buildLocale(language, country, variant); 427 if (!key.isEmpty()) { 428 final FormSet formSet = getFormSets().get(key); 429 if (formSet != null) { 430 form = formSet.getForm(formKey); 431 } 432 } 433 final String localeKey = key; 434 435 // Try language/country 436 if (form == null) { 437 key = buildLocale(language, country, null); 438 if (!key.isEmpty()) { 439 final FormSet formSet = getFormSets().get(key); 440 if (formSet != null) { 441 form = formSet.getForm(formKey); 442 } 443 } 444 } 445 446 // Try language 447 if (form == null) { 448 key = buildLocale(language, null, null); 449 if (!key.isEmpty()) { 450 final FormSet formSet = getFormSets().get(key); 451 if (formSet != null) { 452 form = formSet.getForm(formKey); 453 } 454 } 455 } 456 457 // Try default formset 458 if (form == null) { 459 form = defaultFormSet.getForm(formKey); 460 key = "default"; 461 } 462 463 if (form == null) { 464 if (getLog().isWarnEnabled()) { 465 getLog().warn("Form '" + formKey + "' not found for locale '" + localeKey + "'"); 466 } 467 } else if (getLog().isDebugEnabled()) { 468 getLog().debug("Form '" + formKey + "' found in formset '" + key + "' for locale '" + localeKey + "'"); 469 } 470 471 return form; 472 473 } 474 475 /** 476 * <p>Gets a {@code FormSet} based on the language, country 477 * and variant.</p> 478 * 479 * @param language The locale's language. 480 * @param country The locale's country. 481 * @param variant The locale's language variant. 482 * @return The FormSet for a locale. 483 * @since 1.2 484 */ 485 FormSet getFormSet(final String language, final String country, final String variant) { 486 final String key = buildLocale(language, country, variant); 487 if (key.isEmpty()) { 488 return defaultFormSet; 489 } 490 return getFormSets().get(key); 491 } 492 493 /** 494 * Returns a Map of String locale keys to Lists of their FormSets. 495 * 496 * @return Map of Form sets 497 * @since 1.2.0 498 */ 499 @SuppressWarnings("unchecked") // FastHashMap is not generic 500 protected Map<String, FormSet> getFormSets() { 501 return hFormSets; 502 } 503 504 /** 505 * Accessor method for Log instance. 506 * 507 * The Log instance variable is transient and 508 * accessing it through this method ensures it 509 * is re-initialized when this instance is 510 * de-serialized. 511 * 512 * @return The Log instance. 513 */ 514 private Log getLog() { 515 if (log == null) { 516 log = LogFactory.getLog(ValidatorResources.class); 517 } 518 return log; 519 } 520 521 /** 522 * Finds the given formSet's parent. ex: A formSet with locale en_UK_TEST1 523 * has a direct parent in the formSet with locale en_UK. If it doesn't 524 * exist, find the formSet with locale en, if no found get the 525 * defaultFormSet. 526 * 527 * @param fs 528 * the formSet we want to get the parent from 529 * @return fs's parent 530 */ 531 private FormSet getParent(final FormSet fs) { 532 533 FormSet parent = null; 534 if (fs.getType() == FormSet.LANGUAGE_FORMSET) { 535 parent = defaultFormSet; 536 } else if (fs.getType() == FormSet.COUNTRY_FORMSET) { 537 parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null)); 538 if (parent == null) { 539 parent = defaultFormSet; 540 } 541 } else if (fs.getType() == FormSet.VARIANT_FORMSET) { 542 parent = getFormSets().get(buildLocale(fs.getLanguage(), fs.getCountry(), null)); 543 if (parent == null) { 544 parent = getFormSets().get(buildLocale(fs.getLanguage(), null, null)); 545 if (parent == null) { 546 parent = defaultFormSet; 547 } 548 } 549 } 550 return parent; 551 } 552 553 /** 554 * Gets a {@code ValidatorAction} based on its name. 555 * 556 * @param key The validator action key. 557 * @return The validator action. 558 */ 559 public ValidatorAction getValidatorAction(final String key) { 560 return getActions().get(key); 561 } 562 563 /** 564 * Gets an unmodifiable {@link Map} of the {@code ValidatorAction}s. 565 * 566 * @return Map of validator actions. 567 */ 568 public Map<String, ValidatorAction> getValidatorActions() { 569 return Collections.unmodifiableMap(getActions()); 570 } 571 572 /** 573 * Initialize the digester. 574 */ 575 private Digester initDigester() { 576 URL rulesUrl = this.getClass().getResource(VALIDATOR_RULES); 577 if (rulesUrl == null) { 578 // Fix for Issue# VALIDATOR-195 579 rulesUrl = ValidatorResources.class.getResource(VALIDATOR_RULES); 580 } 581 if (getLog().isDebugEnabled()) { 582 getLog().debug("Loading rules from '" + rulesUrl + "'"); 583 } 584 final Digester digester = DigesterLoader.createDigester(rulesUrl); 585 digester.setNamespaceAware(true); 586 digester.setValidating(true); 587 digester.setUseContextClassLoader(true); 588 589 // Add rules for arg0-arg3 elements 590 addOldArgRules(digester); 591 592 // register DTDs 593 for (int i = 0; i < REGISTRATIONS.length; i += 2) { 594 final URL url = this.getClass().getResource(REGISTRATIONS[i + 1]); 595 if (url != null) { 596 digester.register(REGISTRATIONS[i], url.toString()); 597 } 598 } 599 return digester; 600 } 601 602 /** 603 * Process the {@code ValidatorResources} object. Currently, sets the 604 * {@code FastHashMap} s to the 'fast' mode and call the processes 605 * all other resources. <strong>Note </strong>: The framework calls this 606 * automatically when ValidatorResources is created from an XML file. If you 607 * create an instance of this class by hand you <strong>must </strong> call 608 * this method when finished. 609 */ 610 public void process() { 611 hFormSets.setFast(true); 612 hConstants.setFast(true); 613 hActions.setFast(true); 614 615 processForms(); 616 } 617 618 /** 619 * <p>Process the {@code Form} objects. This clones the {@code Field}s 620 * that don't exist in a {@code FormSet} compared to its parent 621 * {@code FormSet}.</p> 622 */ 623 private void processForms() { 624 if (defaultFormSet == null) { // it isn't mandatory to have a 625 // default formset 626 defaultFormSet = new FormSet(); 627 } 628 defaultFormSet.process(getConstants()); 629 // Loop through FormSets and merge if necessary 630 for (final String key : getFormSets().keySet()) { 631 final FormSet fs = getFormSets().get(key); 632 fs.merge(getParent(fs)); 633 } 634 635 // Process Fully Constructed FormSets 636 for (final FormSet fs : getFormSets().values()) { 637 if (!fs.isProcessed()) { 638 fs.process(getConstants()); 639 } 640 } 641 } 642 643}