001 // copied from commons-collections-3.1 002 /* 003 * Copyright 2001-2004 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 */ 017 package gov.lbl.dsd.sea.nio.util; 018 019 import java.io.File; 020 import java.io.FileInputStream; 021 import java.io.IOException; 022 import java.io.InputStream; 023 import java.io.InputStreamReader; 024 import java.io.LineNumberReader; 025 import java.io.OutputStream; 026 import java.io.PrintWriter; 027 import java.io.Reader; 028 import java.io.UnsupportedEncodingException; 029 import java.util.ArrayList; 030 import java.util.Enumeration; 031 import java.util.Hashtable; 032 import java.util.Iterator; 033 import java.util.List; 034 import java.util.NoSuchElementException; 035 import java.util.Properties; 036 import java.util.StringTokenizer; 037 import java.util.Vector; 038 039 /** 040 * This class extends normal Java properties by adding the possibility 041 * to use the same key many times concatenating the value strings 042 * instead of overwriting them. 043 * <p> 044 * <b>Please consider using the <code>PropertiesConfiguration</code> class in 045 * Commons-Configuration as soon as it is released.</b> 046 * <p> 047 * The Extended Properties syntax is explained here: 048 * 049 * <ul> 050 * <li> 051 * Each property has the syntax <code>key = value</code> 052 * </li> 053 * <li> 054 * The <i>key</i> may use any character but the equal sign '='. 055 * </li> 056 * <li> 057 * <i>value</i> may be separated on different lines if a backslash 058 * is placed at the end of the line that continues below. 059 * </li> 060 * <li> 061 * If <i>value</i> is a list of strings, each token is separated 062 * by a comma ','. 063 * </li> 064 * <li> 065 * Commas in each token are escaped placing a backslash right before 066 * the comma. 067 * </li> 068 * <li> 069 * Backslashes are escaped by using two consecutive backslashes i.e. \\ 070 * </li> 071 * <li> 072 * If a <i>key</i> is used more than once, the values are appended 073 * like if they were on the same line separated with commas. 074 * </li> 075 * <li> 076 * Blank lines and lines starting with character '#' are skipped. 077 * </li> 078 * <li> 079 * If a property is named "include" (or whatever is defined by 080 * setInclude() and getInclude() and the value of that property is 081 * the full path to a file on disk, that file will be included into 082 * the ConfigurationsRepository. You can also pull in files relative 083 * to the parent configuration file. So if you have something 084 * like the following: 085 * 086 * include = additional.properties 087 * 088 * Then "additional.properties" is expected to be in the same 089 * directory as the parent configuration file. 090 * 091 * Duplicate name values will be replaced, so be careful. 092 * 093 * </li> 094 * </ul> 095 * 096 * <p>Here is an example of a valid extended properties file: 097 * 098 * <p><pre> 099 * # lines starting with # are comments 100 * 101 * # This is the simplest property 102 * key = value 103 * 104 * # A long property may be separated on multiple lines 105 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ 106 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 107 * 108 * # This is a property with many tokens 109 * tokens_on_a_line = first token, second token 110 * 111 * # This sequence generates exactly the same result 112 * tokens_on_multiple_lines = first token 113 * tokens_on_multiple_lines = second token 114 * 115 * # commas may be escaped in tokens 116 * commas.escaped = Hi\, what'up? 117 * </pre> 118 * 119 * <p><b>NOTE</b>: this class has <b>not</b> been written for 120 * performance nor low memory usage. In fact, it's way slower than it 121 * could be and generates too much memory garbage. But since 122 * performance is not an issue during intialization (and there is not 123 * much time to improve it), I wrote it this way. If you don't like 124 * it, go ahead and tune it up! 125 * 126 * @since Commons Collections 1.0 127 * @version $Revision: 1.1 $ $Date: 2004/07/21 20:54:43 $ 128 * 129 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a> 130 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a> 131 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a> 132 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a> 133 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 134 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a> 135 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a> 136 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 137 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a> 138 * @author Janek Bogucki 139 * @author Mohan Kishore 140 * @author Stephen Colebourne 141 */ 142 public class ExtendedProperties extends Hashtable { 143 144 /** 145 * Default configurations repository. 146 */ 147 private ExtendedProperties defaults; 148 149 /** 150 * The file connected to this repository (holding comments and 151 * such). 152 * 153 * @serial 154 */ 155 protected String file; 156 157 /** 158 * Base path of the configuration file used to create 159 * this ExtendedProperties object. 160 */ 161 protected String basePath; 162 163 /** 164 * File separator. 165 */ 166 protected String fileSeparator = System.getProperty("file.separator"); 167 168 /** 169 * Has this configuration been intialized. 170 */ 171 protected boolean isInitialized = false; 172 173 /** 174 * This is the name of the property that can point to other 175 * properties file for including other properties files. 176 */ 177 protected static String include = "include"; 178 179 /** 180 * These are the keys in the order they listed 181 * in the configuration file. This is useful when 182 * you wish to perform operations with configuration 183 * information in a particular order. 184 */ 185 protected ArrayList keysAsListed = new ArrayList(); 186 187 protected final static String START_TOKEN="${"; 188 protected final static String END_TOKEN="}"; 189 190 191 /** 192 * Interpolate key names to handle ${key} stuff 193 * 194 * @param base string to interpolate 195 * @return returns the key name with the ${key} substituted 196 */ 197 protected String interpolate(String base) { 198 // COPIED from [configuration] 2003-12-29 199 return (interpolateHelper(base, null)); 200 } 201 202 /** 203 * Recursive handler for multiple levels of interpolation. 204 * 205 * When called the first time, priorVariables should be null. 206 * 207 * @param base string with the ${key} variables 208 * @param priorVariables serves two purposes: to allow checking for 209 * loops, and creating a meaningful exception message should a loop 210 * occur. It's 0'th element will be set to the value of base from 211 * the first call. All subsequent interpolated variables are added 212 * afterward. 213 * 214 * @return the string with the interpolation taken care of 215 */ 216 protected String interpolateHelper(String base, List priorVariables) { 217 // COPIED from [configuration] 2003-12-29 218 if (base == null) { 219 return null; 220 } 221 222 // on the first call initialize priorVariables 223 // and add base as the first element 224 if (priorVariables == null) { 225 priorVariables = new ArrayList(); 226 priorVariables.add(base); 227 } 228 229 int begin = -1; 230 int end = -1; 231 int prec = 0 - END_TOKEN.length(); 232 String variable = null; 233 StringBuffer result = new StringBuffer(); 234 235 // FIXME: we should probably allow the escaping of the start token 236 while (((begin = base.indexOf(START_TOKEN, prec + END_TOKEN.length())) > -1) 237 && ((end = base.indexOf(END_TOKEN, begin)) > -1)) { 238 result.append(base.substring(prec + END_TOKEN.length(), begin)); 239 variable = base.substring(begin + START_TOKEN.length(), end); 240 241 // if we've got a loop, create a useful exception message and throw 242 if (priorVariables.contains(variable)) { 243 String initialBase = priorVariables.remove(0).toString(); 244 priorVariables.add(variable); 245 StringBuffer priorVariableSb = new StringBuffer(); 246 247 // create a nice trace of interpolated variables like so: 248 // var1->var2->var3 249 for (Iterator it = priorVariables.iterator(); it.hasNext();) { 250 priorVariableSb.append(it.next()); 251 if (it.hasNext()) { 252 priorVariableSb.append("->"); 253 } 254 } 255 256 throw new IllegalStateException( 257 "infinite loop in property interpolation of " + initialBase + ": " + priorVariableSb.toString()); 258 } 259 // otherwise, add this variable to the interpolation list. 260 else { 261 priorVariables.add(variable); 262 } 263 264 //QUESTION: getProperty or getPropertyDirect 265 Object value = getProperty(variable); 266 if (value != null) { 267 result.append(interpolateHelper(value.toString(), priorVariables)); 268 269 // pop the interpolated variable off the stack 270 // this maintains priorVariables correctness for 271 // properties with multiple interpolations, e.g. 272 // prop.name=${some.other.prop1}/blahblah/${some.other.prop2} 273 priorVariables.remove(priorVariables.size() - 1); 274 } else if (defaults != null && defaults.getString(variable, null) != null) { 275 result.append(defaults.getString(variable)); 276 } else { 277 //variable not defined - so put it back in the value 278 result.append(START_TOKEN).append(variable).append(END_TOKEN); 279 } 280 prec = end; 281 } 282 result.append(base.substring(prec + END_TOKEN.length(), base.length())); 283 284 return result.toString(); 285 } 286 287 /** 288 * Inserts a backslash before every comma and backslash. 289 */ 290 private static String escape(String s) { 291 StringBuffer buf = new StringBuffer(s); 292 for (int i = 0; i < buf.length(); i++) { 293 char c = buf.charAt(i); 294 if (c == ',' || c == '\\') { 295 buf.insert(i, '\\'); 296 i++; 297 } 298 } 299 return buf.toString(); 300 } 301 302 /** 303 * Removes a backslash from every pair of backslashes. 304 */ 305 private static String unescape(String s) { 306 StringBuffer buf = new StringBuffer(s); 307 for (int i = 0; i < buf.length() - 1; i++) { 308 char c1 = buf.charAt(i); 309 char c2 = buf.charAt(i + 1); 310 if (c1 == '\\' && c2 == '\\') { 311 buf.deleteCharAt(i); 312 } 313 } 314 return buf.toString(); 315 } 316 317 /** 318 * Counts the number of successive times 'ch' appears in the 319 * 'line' before the position indicated by the 'index'. 320 */ 321 private static int countPreceding(String line, int index, char ch) { 322 int i; 323 for (i = index - 1; i >= 0; i--) { 324 if (line.charAt(i) != ch) { 325 break; 326 } 327 } 328 return index - 1 - i; 329 } 330 331 /** 332 * Checks if the line ends with odd number of backslashes 333 */ 334 private static boolean endsWithSlash(String line) { 335 if (!line.endsWith("\\")) { 336 return false; 337 } 338 return (countPreceding(line, line.length() - 1, '\\') % 2 == 0); 339 } 340 341 /** 342 * This class is used to read properties lines. These lines do 343 * not terminate with new-line chars but rather when there is no 344 * backslash sign a the end of the line. This is used to 345 * concatenate multiple lines for readability. 346 */ 347 static class PropertiesReader extends LineNumberReader { 348 /** 349 * Constructor. 350 * 351 * @param reader A Reader. 352 */ 353 public PropertiesReader(Reader reader) { 354 super(reader); 355 } 356 357 /** 358 * Read a property. 359 * 360 * @return a String property 361 * @throws IOException if there is difficulty reading the source. 362 */ 363 public String readProperty() throws IOException { 364 StringBuffer buffer = new StringBuffer(); 365 366 try { 367 while (true) { 368 String line = readLine().trim(); 369 if ((line.length() != 0) && (line.charAt(0) != '#')) { 370 if (endsWithSlash(line)) { 371 line = line.substring(0, line.length() - 1); 372 buffer.append(line); 373 } else { 374 buffer.append(line); 375 break; 376 } 377 } 378 } 379 } catch (NullPointerException ex) { 380 return null; 381 } 382 383 return buffer.toString(); 384 } 385 } 386 387 /** 388 * This class divides into tokens a property value. Token 389 * separator is "," but commas into the property value are escaped 390 * using the backslash in front. 391 */ 392 static class PropertiesTokenizer extends StringTokenizer { 393 /** 394 * The property delimiter used while parsing (a comma). 395 */ 396 static final String DELIMITER = ","; 397 398 /** 399 * Constructor. 400 * 401 * @param string A String. 402 */ 403 public PropertiesTokenizer(String string) { 404 super(string, DELIMITER); 405 } 406 407 /** 408 * Check whether the object has more tokens. 409 * 410 * @return True if the object has more tokens. 411 */ 412 public boolean hasMoreTokens() { 413 return super.hasMoreTokens(); 414 } 415 416 /** 417 * Get next token. 418 * 419 * @return A String. 420 */ 421 public String nextToken() { 422 StringBuffer buffer = new StringBuffer(); 423 424 while (hasMoreTokens()) { 425 String token = super.nextToken(); 426 if (endsWithSlash(token)) { 427 buffer.append(token.substring(0, token.length() - 1)); 428 buffer.append(DELIMITER); 429 } else { 430 buffer.append(token); 431 break; 432 } 433 } 434 435 return buffer.toString().trim(); 436 } 437 } 438 439 /** 440 * Creates an empty extended properties object. 441 */ 442 public ExtendedProperties() { 443 super(); 444 } 445 446 /** 447 * Creates and loads the extended properties from the specified file. 448 * 449 * @param file the filename to load 450 * @throws IOException if a file error occurs 451 */ 452 public ExtendedProperties(String file) throws IOException { 453 this(file, null); 454 } 455 456 /** 457 * Creates and loads the extended properties from the specified file. 458 * 459 * @param file the filename to load 460 * @param defaultFile a second filename to load default values from 461 * @throws IOException if a file error occurs 462 */ 463 public ExtendedProperties(String file, String defaultFile) throws IOException { 464 this.file = file; 465 466 basePath = new File(file).getAbsolutePath(); 467 basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1); 468 469 FileInputStream in = null; 470 try { 471 in = new FileInputStream(file); 472 this.load(in); 473 } finally { 474 try { 475 if (in != null) { 476 in.close(); 477 } 478 } catch (IOException ex) {} 479 } 480 481 if (defaultFile != null) { 482 defaults = new ExtendedProperties(defaultFile); 483 } 484 } 485 486 /** 487 * Indicate to client code whether property 488 * resources have been initialized or not. 489 */ 490 public boolean isInitialized() { 491 return isInitialized; 492 } 493 494 /** 495 * Gets the property value for including other properties files. 496 * By default it is "include". 497 * 498 * @return A String. 499 */ 500 public String getInclude() { 501 return include; 502 } 503 504 /** 505 * Sets the property value for including other properties files. 506 * By default it is "include". 507 * 508 * @param inc A String. 509 */ 510 public void setInclude(String inc) { 511 include = inc; 512 } 513 514 /** 515 * Load the properties from the given input stream. 516 * 517 * @param input the InputStream to load from 518 * @throws IOException if an IO error occurs 519 */ 520 public void load(InputStream input) throws IOException { 521 load(input, null); 522 } 523 524 /** 525 * Load the properties from the given input stream 526 * and using the specified encoding. 527 * 528 * @param input the InputStream to load from 529 * @param enc the encoding to use 530 * @throws IOException if an IO error occurs 531 */ 532 public synchronized void load(InputStream input, String enc) throws IOException { 533 PropertiesReader reader = null; 534 if (enc != null) { 535 try { 536 reader = new PropertiesReader(new InputStreamReader(input, enc)); 537 538 } catch (UnsupportedEncodingException ex) { 539 // Another try coming up.... 540 } 541 } 542 543 if (reader == null) { 544 try { 545 reader = new PropertiesReader(new InputStreamReader(input, "8859_1")); 546 547 } catch (UnsupportedEncodingException ex) { 548 // ISO8859-1 support is required on java platforms but.... 549 // If it's not supported, use the system default encoding 550 reader = new PropertiesReader(new InputStreamReader(input)); 551 } 552 } 553 554 try { 555 while (true) { 556 String line = reader.readProperty(); 557 int equalSign = line.indexOf('='); 558 559 if (equalSign > 0) { 560 String key = line.substring(0, equalSign).trim(); 561 String value = line.substring(equalSign + 1).trim(); 562 563 // Configure produces lines like this ... just ignore them 564 if ("".equals(value)) { 565 continue; 566 } 567 568 if (getInclude() != null && key.equalsIgnoreCase(getInclude())) { 569 // Recursively load properties files. 570 File file = null; 571 572 if (value.startsWith(fileSeparator)) { 573 // We have an absolute path so we'll use this 574 file = new File(value); 575 576 } else { 577 // We have a relative path, and we have two 578 // possible forms here. If we have the "./" form 579 // then just strip that off first before continuing. 580 if (value.startsWith("." + fileSeparator)) { 581 value = value.substring(2); 582 } 583 584 file = new File(basePath + value); 585 } 586 587 if (file != null && file.exists() && file.canRead()) { 588 load(new FileInputStream(file)); 589 } 590 } else { 591 addProperty(key, value); 592 } 593 } 594 } 595 } catch (NullPointerException ex) { 596 // Should happen only when EOF is reached. 597 return; 598 } finally { 599 // Loading is initializing 600 isInitialized = true; 601 } 602 } 603 604 /** 605 * Gets a property from the configuration. 606 * 607 * @param key property to retrieve 608 * @return value as object. Will return user value if exists, 609 * if not then default value if exists, otherwise null 610 */ 611 public Object getProperty(String key) { 612 // first, try to get from the 'user value' store 613 Object obj = this.get(key); 614 615 if (obj == null) { 616 // if there isn't a value there, get it from the 617 // defaults if we have them 618 if (defaults != null) { 619 obj = defaults.get(key); 620 } 621 } 622 623 return obj; 624 } 625 626 /** 627 * Add a property to the configuration. If it already 628 * exists then the value stated here will be added 629 * to the configuration entry. For example, if 630 * 631 * <code>resource.loader = file</code> 632 * 633 * is already present in the configuration and you 634 * 635 * <code>addProperty("resource.loader", "classpath")</code> 636 * 637 * Then you will end up with a Vector like the 638 * following: 639 * 640 * <code>["file", "classpath"]</code> 641 * 642 * @param key the key to add 643 * @param value the value to add 644 */ 645 public void addProperty(String key, Object value) { 646 if (value instanceof String) { 647 String str = (String) value; 648 if (str.indexOf(PropertiesTokenizer.DELIMITER) > 0) { 649 // token contains commas, so must be split apart then added 650 PropertiesTokenizer tokenizer = new PropertiesTokenizer(str); 651 while (tokenizer.hasMoreTokens()) { 652 String token = tokenizer.nextToken(); 653 addPropertyInternal(key, unescape(token)); 654 } 655 } else { 656 // token contains no commas, so can be simply added 657 addPropertyInternal(key, unescape(str)); 658 } 659 } else { 660 addPropertyInternal(key, value); 661 } 662 663 // Adding a property connotes initialization 664 isInitialized = true; 665 } 666 667 /** 668 * Adds a key/value pair to the map. This routine does 669 * no magic morphing. It ensures the keylist is maintained 670 * 671 * @param key the key to store at 672 * @param value the decoded object to store 673 */ 674 private void addPropertyDirect(String key, Object value) { 675 // safety check 676 if (!containsKey(key)) { 677 keysAsListed.add(key); 678 } 679 put(key, value); 680 } 681 682 /** 683 * Adds a decoded property to the map w/o checking for commas - used 684 * internally when a property has been broken up into 685 * strings that could contain escaped commas to prevent 686 * the inadvertent vectorization. 687 * <p> 688 * Thanks to Leon Messerschmidt for this one. 689 * 690 * @param key the key to store at 691 * @param value the decoded object to store 692 */ 693 private void addPropertyInternal(String key, Object value) { 694 Object current = this.get(key); 695 696 if (current instanceof String) { 697 // one object already in map - convert it to a vector 698 Vector v = new Vector(2); 699 v.addElement(current); 700 v.addElement(value); 701 put(key, v); 702 703 } else if (current instanceof Vector) { 704 // already a vector - just add the new token 705 ((Vector) current).addElement(value); 706 707 } else { 708 // brand new key - store in keysAsListed to retain order 709 if (!containsKey(key)) { 710 keysAsListed.add(key); 711 } 712 put(key, value); 713 } 714 } 715 716 /** 717 * Set a property, this will replace any previously 718 * set values. Set values is implicitly a call 719 * to clearProperty(key), addProperty(key,value). 720 * 721 * @param key the key to set 722 * @param value the value to set 723 */ 724 public void setProperty(String key, Object value) { 725 clearProperty(key); 726 addProperty(key, value); 727 } 728 729 /** 730 * Save the properties to the given output stream. 731 * <p> 732 * The stream is not closed, but it is flushed. 733 * 734 * @param output an OutputStream, may be null 735 * @param header a textual comment to act as a file header 736 * @throws IOException if an IO error occurs 737 */ 738 public synchronized void save(OutputStream output, String header) { 739 if (output == null) { 740 return; 741 } 742 PrintWriter theWrtr = new PrintWriter(output); 743 if (header != null) { 744 theWrtr.println(header); 745 } 746 747 Enumeration theKeys = keys(); 748 while (theKeys.hasMoreElements()) { 749 String key = (String) theKeys.nextElement(); 750 Object value = get(key); 751 if (value != null) { 752 if (value instanceof String) { 753 StringBuffer currentOutput = new StringBuffer(); 754 currentOutput.append(key); 755 currentOutput.append("="); 756 currentOutput.append(escape((String) value)); 757 theWrtr.println(currentOutput.toString()); 758 759 } else if (value instanceof Vector) { 760 Vector values = (Vector) value; 761 Enumeration valuesEnum = values.elements(); 762 while (valuesEnum.hasMoreElements()) { 763 String currentElement = (String) valuesEnum.nextElement(); 764 StringBuffer currentOutput = new StringBuffer(); 765 currentOutput.append(key); 766 currentOutput.append("="); 767 currentOutput.append(escape(currentElement)); 768 theWrtr.println(currentOutput.toString()); 769 } 770 } 771 } 772 theWrtr.println(); 773 theWrtr.flush(); 774 } 775 } 776 777 /** 778 * Combines an existing Hashtable with this Hashtable. 779 * <p> 780 * Warning: It will overwrite previous entries without warning. 781 * 782 * @param props the properties to combine 783 */ 784 public void combine(ExtendedProperties props) { 785 for (Iterator it = props.getKeys(); it.hasNext();) { 786 String key = (String) it.next(); 787 setProperty(key, props.get(key)); 788 } 789 } 790 791 /** 792 * Clear a property in the configuration. 793 * 794 * @param key the property key to remove along with corresponding value 795 */ 796 public void clearProperty(String key) { 797 if (containsKey(key)) { 798 // we also need to rebuild the keysAsListed or else 799 // things get *very* confusing 800 for (int i = 0; i < keysAsListed.size(); i++) { 801 if (( keysAsListed.get(i)).equals(key)) { 802 keysAsListed.remove(i); 803 break; 804 } 805 } 806 remove(key); 807 } 808 } 809 810 /** 811 * Get the list of the keys contained in the configuration 812 * repository. 813 * 814 * @return an Iterator over the keys 815 */ 816 public Iterator getKeys() { 817 return keysAsListed.iterator(); 818 } 819 820 /** 821 * Get the list of the keys contained in the configuration 822 * repository that match the specified prefix. 823 * 824 * @param prefix the prefix to match 825 * @return an Iterator of keys that match the prefix 826 */ 827 public Iterator getKeys(String prefix) { 828 Iterator keys = getKeys(); 829 ArrayList matchingKeys = new ArrayList(); 830 831 while (keys.hasNext()) { 832 Object key = keys.next(); 833 834 if (key instanceof String && ((String) key).startsWith(prefix)) { 835 matchingKeys.add(key); 836 } 837 } 838 return matchingKeys.iterator(); 839 } 840 841 /** 842 * Create an ExtendedProperties object that is a subset 843 * of this one. Take into account duplicate keys 844 * by using the setProperty() in ExtendedProperties. 845 * 846 * @param prefix the prefix to get a subset for 847 * @return a new independent ExtendedProperties 848 */ 849 public ExtendedProperties subset(String prefix) { 850 ExtendedProperties c = new ExtendedProperties(); 851 Iterator keys = getKeys(); 852 boolean validSubset = false; 853 854 while (keys.hasNext()) { 855 Object key = keys.next(); 856 857 if (key instanceof String && ((String) key).startsWith(prefix)) { 858 if (!validSubset) { 859 validSubset = true; 860 } 861 862 /* 863 * Check to make sure that c.subset(prefix) doesn't 864 * blow up when there is only a single property 865 * with the key prefix. This is not a useful 866 * subset but it is a valid subset. 867 */ 868 String newKey = null; 869 if (((String) key).length() == prefix.length()) { 870 newKey = prefix; 871 } else { 872 newKey = ((String) key).substring(prefix.length() + 1); 873 } 874 875 /* 876 * use addPropertyDirect() - this will plug the data as 877 * is into the Map, but will also do the right thing 878 * re key accounting 879 */ 880 c.addPropertyDirect(newKey, get(key)); 881 } 882 } 883 884 if (validSubset) { 885 return c; 886 } else { 887 return null; 888 } 889 } 890 891 /** 892 * Display the configuration for debugging purposes to System.out. 893 */ 894 public void display() { 895 Iterator i = getKeys(); 896 897 while (i.hasNext()) { 898 String key = (String) i.next(); 899 Object value = get(key); 900 System.out.println(key + " => " + value); 901 } 902 } 903 904 /** 905 * Get a string associated with the given configuration key. 906 * 907 * @param key The configuration key. 908 * @return The associated string. 909 * @throws ClassCastException is thrown if the key maps to an 910 * object that is not a String. 911 */ 912 public String getString(String key) { 913 return getString(key, null); 914 } 915 916 /** 917 * Get a string associated with the given configuration key. 918 * 919 * @param key The configuration key. 920 * @param defaultValue The default value. 921 * @return The associated string if key is found, 922 * default value otherwise. 923 * @throws ClassCastException is thrown if the key maps to an 924 * object that is not a String. 925 */ 926 public String getString(String key, String defaultValue) { 927 Object value = get(key); 928 929 if (value instanceof String) { 930 return interpolate((String) value); 931 932 } else if (value == null) { 933 if (defaults != null) { 934 return interpolate(defaults.getString(key, defaultValue)); 935 } else { 936 return interpolate(defaultValue); 937 } 938 } else if (value instanceof Vector) { 939 return interpolate((String) ((Vector) value).get(0)); 940 } else { 941 throw new ClassCastException('\'' + key + "' doesn't map to a String object"); 942 } 943 } 944 945 /** 946 * Get a list of properties associated with the given 947 * configuration key. 948 * 949 * @param key The configuration key. 950 * @return The associated properties if key is found. 951 * @throws ClassCastException is thrown if the key maps to an 952 * object that is not a String/Vector. 953 * @throws IllegalArgumentException if one of the tokens is 954 * malformed (does not contain an equals sign). 955 */ 956 public Properties getProperties(String key) { 957 return getProperties(key, new Properties()); 958 } 959 960 /** 961 * Get a list of properties associated with the given 962 * configuration key. 963 * 964 * @param key The configuration key. 965 * @return The associated properties if key is found. 966 * @throws ClassCastException is thrown if the key maps to an 967 * object that is not a String/Vector. 968 * @throws IllegalArgumentException if one of the tokens is 969 * malformed (does not contain an equals sign). 970 */ 971 public Properties getProperties(String key, Properties defaults) { 972 /* 973 * Grab an array of the tokens for this key. 974 */ 975 String[] tokens = getStringArray(key); 976 977 // Each token is of the form 'key=value'. 978 Properties props = new Properties(defaults); 979 for (int i = 0; i < tokens.length; i++) { 980 String token = tokens[i]; 981 int equalSign = token.indexOf('='); 982 if (equalSign > 0) { 983 String pkey = token.substring(0, equalSign).trim(); 984 String pvalue = token.substring(equalSign + 1).trim(); 985 props.put(pkey, pvalue); 986 } else { 987 throw new IllegalArgumentException('\'' + token + "' does not contain " + "an equals sign"); 988 } 989 } 990 return props; 991 } 992 993 /** 994 * Get an array of strings associated with the given configuration 995 * key. 996 * 997 * @param key The configuration key. 998 * @return The associated string array if key is found. 999 * @throws ClassCastException is thrown if the key maps to an 1000 * object that is not a String/Vector. 1001 */ 1002 public String[] getStringArray(String key) { 1003 Object value = get(key); 1004 1005 // What's your vector, Victor? 1006 Vector vector; 1007 if (value instanceof String) { 1008 vector = new Vector(1); 1009 vector.addElement(value); 1010 1011 } else if (value instanceof Vector) { 1012 vector = (Vector) value; 1013 1014 } else if (value == null) { 1015 if (defaults != null) { 1016 return defaults.getStringArray(key); 1017 } else { 1018 return new String[0]; 1019 } 1020 } else { 1021 throw new ClassCastException('\'' + key + "' doesn't map to a String/Vector object"); 1022 } 1023 1024 String[] tokens = new String[vector.size()]; 1025 for (int i = 0; i < tokens.length; i++) { 1026 tokens[i] = (String) vector.elementAt(i); 1027 } 1028 1029 return tokens; 1030 } 1031 1032 /** 1033 * Get a Vector of strings associated with the given configuration 1034 * key. 1035 * 1036 * @param key The configuration key. 1037 * @return The associated Vector. 1038 * @throws ClassCastException is thrown if the key maps to an 1039 * object that is not a Vector. 1040 */ 1041 public Vector getVector(String key) { 1042 return getVector(key, null); 1043 } 1044 1045 /** 1046 * Get a Vector of strings associated with the given configuration 1047 * key. 1048 * 1049 * @param key The configuration key. 1050 * @param defaultValue The default value. 1051 * @return The associated Vector. 1052 * @throws ClassCastException is thrown if the key maps to an 1053 * object that is not a Vector. 1054 */ 1055 public Vector getVector(String key, Vector defaultValue) { 1056 Object value = get(key); 1057 1058 if (value instanceof Vector) { 1059 return (Vector) value; 1060 1061 } else if (value instanceof String) { 1062 Vector v = new Vector(1); 1063 v.addElement(value); 1064 put(key, v); 1065 return v; 1066 1067 } else if (value == null) { 1068 if (defaults != null) { 1069 return defaults.getVector(key, defaultValue); 1070 } else { 1071 return ((defaultValue == null) ? new Vector() : defaultValue); 1072 } 1073 } else { 1074 throw new ClassCastException('\'' + key + "' doesn't map to a Vector object"); 1075 } 1076 } 1077 1078 /** 1079 * Get a boolean associated with the given configuration key. 1080 * 1081 * @param key The configuration key. 1082 * @return The associated boolean. 1083 * @throws NoSuchElementException is thrown if the key doesn't 1084 * map to an existing object. 1085 * @throws ClassCastException is thrown if the key maps to an 1086 * object that is not a Boolean. 1087 */ 1088 public boolean getBoolean(String key) { 1089 Boolean b = getBoolean(key, null); 1090 if (b != null) { 1091 return b.booleanValue(); 1092 } else { 1093 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1094 } 1095 } 1096 1097 /** 1098 * Get a boolean associated with the given configuration key. 1099 * 1100 * @param key The configuration key. 1101 * @param defaultValue The default value. 1102 * @return The associated boolean. 1103 * @throws ClassCastException is thrown if the key maps to an 1104 * object that is not a Boolean. 1105 */ 1106 public boolean getBoolean(String key, boolean defaultValue) { 1107 return getBoolean(key, new Boolean(defaultValue)).booleanValue(); 1108 } 1109 1110 /** 1111 * Get a boolean associated with the given configuration key. 1112 * 1113 * @param key The configuration key. 1114 * @param defaultValue The default value. 1115 * @return The associated boolean if key is found and has valid 1116 * format, default value otherwise. 1117 * @throws ClassCastException is thrown if the key maps to an 1118 * object that is not a Boolean. 1119 */ 1120 public Boolean getBoolean(String key, Boolean defaultValue) { 1121 1122 Object value = get(key); 1123 1124 if (value instanceof Boolean) { 1125 return (Boolean) value; 1126 1127 } else if (value instanceof String) { 1128 String s = testBoolean((String) value); 1129 Boolean b = new Boolean(s); 1130 put(key, b); 1131 return b; 1132 1133 } else if (value == null) { 1134 if (defaults != null) { 1135 return defaults.getBoolean(key, defaultValue); 1136 } else { 1137 return defaultValue; 1138 } 1139 } else { 1140 throw new ClassCastException('\'' + key + "' doesn't map to a Boolean object"); 1141 } 1142 } 1143 1144 /** 1145 * Test whether the string represent by value maps to a boolean 1146 * value or not. We will allow <code>true</code>, <code>on</code>, 1147 * and <code>yes</code> for a <code>true</code> boolean value, and 1148 * <code>false</code>, <code>off</code>, and <code>no</code> for 1149 * <code>false</code> boolean values. Case of value to test for 1150 * boolean status is ignored. 1151 * 1152 * @param value the value to test for boolean state 1153 * @return <code>true</code> or <code>false</code> if the supplied 1154 * text maps to a boolean value, or <code>null</code> otherwise. 1155 */ 1156 public String testBoolean(String value) { 1157 String s = value.toLowerCase(); 1158 1159 if (s.equals("true") || s.equals("on") || s.equals("yes")) { 1160 return "true"; 1161 } else if (s.equals("false") || s.equals("off") || s.equals("no")) { 1162 return "false"; 1163 } else { 1164 return null; 1165 } 1166 } 1167 1168 /** 1169 * Get a byte associated with the given configuration key. 1170 * 1171 * @param key The configuration key. 1172 * @return The associated byte. 1173 * @throws NoSuchElementException is thrown if the key doesn't 1174 * map to an existing object. 1175 * @throws ClassCastException is thrown if the key maps to an 1176 * object that is not a Byte. 1177 * @throws NumberFormatException is thrown if the value mapped 1178 * by the key has not a valid number format. 1179 */ 1180 public byte getByte(String key) { 1181 Byte b = getByte(key, null); 1182 if (b != null) { 1183 return b.byteValue(); 1184 } else { 1185 throw new NoSuchElementException('\'' + key + " doesn't map to an existing object"); 1186 } 1187 } 1188 1189 /** 1190 * Get a byte associated with the given configuration key. 1191 * 1192 * @param key The configuration key. 1193 * @param defaultValue The default value. 1194 * @return The associated byte. 1195 * @throws ClassCastException is thrown if the key maps to an 1196 * object that is not a Byte. 1197 * @throws NumberFormatException is thrown if the value mapped 1198 * by the key has not a valid number format. 1199 */ 1200 public byte getByte(String key, byte defaultValue) { 1201 return getByte(key, new Byte(defaultValue)).byteValue(); 1202 } 1203 1204 /** 1205 * Get a byte associated with the given configuration key. 1206 * 1207 * @param key The configuration key. 1208 * @param defaultValue The default value. 1209 * @return The associated byte if key is found and has valid 1210 * format, default value otherwise. 1211 * @throws ClassCastException is thrown if the key maps to an 1212 * object that is not a Byte. 1213 * @throws NumberFormatException is thrown if the value mapped 1214 * by the key has not a valid number format. 1215 */ 1216 public Byte getByte(String key, Byte defaultValue) { 1217 Object value = get(key); 1218 1219 if (value instanceof Byte) { 1220 return (Byte) value; 1221 1222 } else if (value instanceof String) { 1223 Byte b = new Byte((String) value); 1224 put(key, b); 1225 return b; 1226 1227 } else if (value == null) { 1228 if (defaults != null) { 1229 return defaults.getByte(key, defaultValue); 1230 } else { 1231 return defaultValue; 1232 } 1233 } else { 1234 throw new ClassCastException('\'' + key + "' doesn't map to a Byte object"); 1235 } 1236 } 1237 1238 /** 1239 * Get a short associated with the given configuration key. 1240 * 1241 * @param key The configuration key. 1242 * @return The associated short. 1243 * @throws NoSuchElementException is thrown if the key doesn't 1244 * map to an existing object. 1245 * @throws ClassCastException is thrown if the key maps to an 1246 * object that is not a Short. 1247 * @throws NumberFormatException is thrown if the value mapped 1248 * by the key has not a valid number format. 1249 */ 1250 public short getShort(String key) { 1251 Short s = getShort(key, null); 1252 if (s != null) { 1253 return s.shortValue(); 1254 } else { 1255 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1256 } 1257 } 1258 1259 /** 1260 * Get a short associated with the given configuration key. 1261 * 1262 * @param key The configuration key. 1263 * @param defaultValue The default value. 1264 * @return The associated short. 1265 * @throws ClassCastException is thrown if the key maps to an 1266 * object that is not a Short. 1267 * @throws NumberFormatException is thrown if the value mapped 1268 * by the key has not a valid number format. 1269 */ 1270 public short getShort(String key, short defaultValue) { 1271 return getShort(key, new Short(defaultValue)).shortValue(); 1272 } 1273 1274 /** 1275 * Get a short associated with the given configuration key. 1276 * 1277 * @param key The configuration key. 1278 * @param defaultValue The default value. 1279 * @return The associated short if key is found and has valid 1280 * format, default value otherwise. 1281 * @throws ClassCastException is thrown if the key maps to an 1282 * object that is not a Short. 1283 * @throws NumberFormatException is thrown if the value mapped 1284 * by the key has not a valid number format. 1285 */ 1286 public Short getShort(String key, Short defaultValue) { 1287 Object value = get(key); 1288 1289 if (value instanceof Short) { 1290 return (Short) value; 1291 1292 } else if (value instanceof String) { 1293 Short s = new Short((String) value); 1294 put(key, s); 1295 return s; 1296 1297 } else if (value == null) { 1298 if (defaults != null) { 1299 return defaults.getShort(key, defaultValue); 1300 } else { 1301 return defaultValue; 1302 } 1303 } else { 1304 throw new ClassCastException('\'' + key + "' doesn't map to a Short object"); 1305 } 1306 } 1307 1308 /** 1309 * The purpose of this method is to get the configuration resource 1310 * with the given name as an integer. 1311 * 1312 * @param name The resource name. 1313 * @return The value of the resource as an integer. 1314 */ 1315 public int getInt(String name) { 1316 return getInteger(name); 1317 } 1318 1319 /** 1320 * The purpose of this method is to get the configuration resource 1321 * with the given name as an integer, or a default value. 1322 * 1323 * @param name The resource name 1324 * @param def The default value of the resource. 1325 * @return The value of the resource as an integer. 1326 */ 1327 public int getInt(String name, int def) { 1328 return getInteger(name, def); 1329 } 1330 1331 /** 1332 * Get a int associated with the given configuration key. 1333 * 1334 * @param key The configuration key. 1335 * @return The associated int. 1336 * @throws NoSuchElementException is thrown if the key doesn't 1337 * map to an existing object. 1338 * @throws ClassCastException is thrown if the key maps to an 1339 * object that is not a Integer. 1340 * @throws NumberFormatException is thrown if the value mapped 1341 * by the key has not a valid number format. 1342 */ 1343 public int getInteger(String key) { 1344 Integer i = getInteger(key, null); 1345 if (i != null) { 1346 return i.intValue(); 1347 } else { 1348 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1349 } 1350 } 1351 1352 /** 1353 * Get a int associated with the given configuration key. 1354 * 1355 * @param key The configuration key. 1356 * @param defaultValue The default value. 1357 * @return The associated int. 1358 * @throws ClassCastException is thrown if the key maps to an 1359 * object that is not a Integer. 1360 * @throws NumberFormatException is thrown if the value mapped 1361 * by the key has not a valid number format. 1362 */ 1363 public int getInteger(String key, int defaultValue) { 1364 Integer i = getInteger(key, null); 1365 1366 if (i == null) { 1367 return defaultValue; 1368 } 1369 return i.intValue(); 1370 } 1371 1372 /** 1373 * Get a int associated with the given configuration key. 1374 * 1375 * @param key The configuration key. 1376 * @param defaultValue The default value. 1377 * @return The associated int if key is found and has valid 1378 * format, default value otherwise. 1379 * @throws ClassCastException is thrown if the key maps to an 1380 * object that is not a Integer. 1381 * @throws NumberFormatException is thrown if the value mapped 1382 * by the key has not a valid number format. 1383 */ 1384 public Integer getInteger(String key, Integer defaultValue) { 1385 Object value = get(key); 1386 1387 if (value instanceof Integer) { 1388 return (Integer) value; 1389 1390 } else if (value instanceof String) { 1391 Integer i = new Integer((String) value); 1392 put(key, i); 1393 return i; 1394 1395 } else if (value == null) { 1396 if (defaults != null) { 1397 return defaults.getInteger(key, defaultValue); 1398 } else { 1399 return defaultValue; 1400 } 1401 } else { 1402 throw new ClassCastException('\'' + key + "' doesn't map to a Integer object"); 1403 } 1404 } 1405 1406 /** 1407 * Get a long associated with the given configuration key. 1408 * 1409 * @param key The configuration key. 1410 * @return The associated long. 1411 * @throws NoSuchElementException is thrown if the key doesn't 1412 * map to an existing object. 1413 * @throws ClassCastException is thrown if the key maps to an 1414 * object that is not a Long. 1415 * @throws NumberFormatException is thrown if the value mapped 1416 * by the key has not a valid number format. 1417 */ 1418 public long getLong(String key) { 1419 Long l = getLong(key, null); 1420 if (l != null) { 1421 return l.longValue(); 1422 } else { 1423 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1424 } 1425 } 1426 1427 /** 1428 * Get a long associated with the given configuration key. 1429 * 1430 * @param key The configuration key. 1431 * @param defaultValue The default value. 1432 * @return The associated long. 1433 * @throws ClassCastException is thrown if the key maps to an 1434 * object that is not a Long. 1435 * @throws NumberFormatException is thrown if the value mapped 1436 * by the key has not a valid number format. 1437 */ 1438 public long getLong(String key, long defaultValue) { 1439 return getLong(key, new Long(defaultValue)).longValue(); 1440 } 1441 1442 /** 1443 * Get a long associated with the given configuration key. 1444 * 1445 * @param key The configuration key. 1446 * @param defaultValue The default value. 1447 * @return The associated long if key is found and has valid 1448 * format, default value otherwise. 1449 * @throws ClassCastException is thrown if the key maps to an 1450 * object that is not a Long. 1451 * @throws NumberFormatException is thrown if the value mapped 1452 * by the key has not a valid number format. 1453 */ 1454 public Long getLong(String key, Long defaultValue) { 1455 Object value = get(key); 1456 1457 if (value instanceof Long) { 1458 return (Long) value; 1459 1460 } else if (value instanceof String) { 1461 Long l = new Long((String) value); 1462 put(key, l); 1463 return l; 1464 1465 } else if (value == null) { 1466 if (defaults != null) { 1467 return defaults.getLong(key, defaultValue); 1468 } else { 1469 return defaultValue; 1470 } 1471 } else { 1472 throw new ClassCastException('\'' + key + "' doesn't map to a Long object"); 1473 } 1474 } 1475 1476 /** 1477 * Get a float associated with the given configuration key. 1478 * 1479 * @param key The configuration key. 1480 * @return The associated float. 1481 * @throws NoSuchElementException is thrown if the key doesn't 1482 * map to an existing object. 1483 * @throws ClassCastException is thrown if the key maps to an 1484 * object that is not a Float. 1485 * @throws NumberFormatException is thrown if the value mapped 1486 * by the key has not a valid number format. 1487 */ 1488 public float getFloat(String key) { 1489 Float f = getFloat(key, null); 1490 if (f != null) { 1491 return f.floatValue(); 1492 } else { 1493 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1494 } 1495 } 1496 1497 /** 1498 * Get a float associated with the given configuration key. 1499 * 1500 * @param key The configuration key. 1501 * @param defaultValue The default value. 1502 * @return The associated float. 1503 * @throws ClassCastException is thrown if the key maps to an 1504 * object that is not a Float. 1505 * @throws NumberFormatException is thrown if the value mapped 1506 * by the key has not a valid number format. 1507 */ 1508 public float getFloat(String key, float defaultValue) { 1509 return getFloat(key, new Float(defaultValue)).floatValue(); 1510 } 1511 1512 /** 1513 * Get a float associated with the given configuration key. 1514 * 1515 * @param key The configuration key. 1516 * @param defaultValue The default value. 1517 * @return The associated float if key is found and has valid 1518 * format, default value otherwise. 1519 * @throws ClassCastException is thrown if the key maps to an 1520 * object that is not a Float. 1521 * @throws NumberFormatException is thrown if the value mapped 1522 * by the key has not a valid number format. 1523 */ 1524 public Float getFloat(String key, Float defaultValue) { 1525 Object value = get(key); 1526 1527 if (value instanceof Float) { 1528 return (Float) value; 1529 1530 } else if (value instanceof String) { 1531 Float f = new Float((String) value); 1532 put(key, f); 1533 return f; 1534 1535 } else if (value == null) { 1536 if (defaults != null) { 1537 return defaults.getFloat(key, defaultValue); 1538 } else { 1539 return defaultValue; 1540 } 1541 } else { 1542 throw new ClassCastException('\'' + key + "' doesn't map to a Float object"); 1543 } 1544 } 1545 1546 /** 1547 * Get a double associated with the given configuration key. 1548 * 1549 * @param key The configuration key. 1550 * @return The associated double. 1551 * @throws NoSuchElementException is thrown if the key doesn't 1552 * map to an existing object. 1553 * @throws ClassCastException is thrown if the key maps to an 1554 * object that is not a Double. 1555 * @throws NumberFormatException is thrown if the value mapped 1556 * by the key has not a valid number format. 1557 */ 1558 public double getDouble(String key) { 1559 Double d = getDouble(key, null); 1560 if (d != null) { 1561 return d.doubleValue(); 1562 } else { 1563 throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object"); 1564 } 1565 } 1566 1567 /** 1568 * Get a double associated with the given configuration key. 1569 * 1570 * @param key The configuration key. 1571 * @param defaultValue The default value. 1572 * @return The associated double. 1573 * @throws ClassCastException is thrown if the key maps to an 1574 * object that is not a Double. 1575 * @throws NumberFormatException is thrown if the value mapped 1576 * by the key has not a valid number format. 1577 */ 1578 public double getDouble(String key, double defaultValue) { 1579 return getDouble(key, new Double(defaultValue)).doubleValue(); 1580 } 1581 1582 /** 1583 * Get a double associated with the given configuration key. 1584 * 1585 * @param key The configuration key. 1586 * @param defaultValue The default value. 1587 * @return The associated double if key is found and has valid 1588 * format, default value otherwise. 1589 * @throws ClassCastException is thrown if the key maps to an 1590 * object that is not a Double. 1591 * @throws NumberFormatException is thrown if the value mapped 1592 * by the key has not a valid number format. 1593 */ 1594 public Double getDouble(String key, Double defaultValue) { 1595 Object value = get(key); 1596 1597 if (value instanceof Double) { 1598 return (Double) value; 1599 1600 } else if (value instanceof String) { 1601 Double d = new Double((String) value); 1602 put(key, d); 1603 return d; 1604 1605 } else if (value == null) { 1606 if (defaults != null) { 1607 return defaults.getDouble(key, defaultValue); 1608 } else { 1609 return defaultValue; 1610 } 1611 } else { 1612 throw new ClassCastException('\'' + key + "' doesn't map to a Double object"); 1613 } 1614 } 1615 1616 /** 1617 * Convert a standard properties class into a configuration class. 1618 * 1619 * @param props the properties object to convert 1620 * @return new ExtendedProperties created from props 1621 */ 1622 public static ExtendedProperties convertProperties(Properties props) { 1623 ExtendedProperties c = new ExtendedProperties(); 1624 1625 for (Enumeration e = props.keys(); e.hasMoreElements();) { 1626 String s = (String) e.nextElement(); 1627 c.setProperty(s, props.getProperty(s)); 1628 } 1629 1630 return c; 1631 } 1632 1633 }