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