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