001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.commons.configuration; 019 020 import java.io.File; 021 import java.io.FilterWriter; 022 import java.io.IOException; 023 import java.io.LineNumberReader; 024 import java.io.Reader; 025 import java.io.Writer; 026 import java.net.URL; 027 import java.util.ArrayList; 028 import java.util.Iterator; 029 import java.util.List; 030 031 import org.apache.commons.lang.ArrayUtils; 032 import org.apache.commons.lang.StringEscapeUtils; 033 import org.apache.commons.lang.StringUtils; 034 035 /** 036 * This is the "classic" Properties loader which loads the values from 037 * a single or multiple files (which can be chained with "include =". 038 * All given path references are either absolute or relative to the 039 * file name supplied in the constructor. 040 * <p> 041 * In this class, empty PropertyConfigurations can be built, properties 042 * added and later saved. include statements are (obviously) not supported 043 * if you don't construct a PropertyConfiguration from a file. 044 * 045 * <p>The properties file syntax is explained here, basically it follows 046 * the syntax of the stream parsed by {@link java.util.Properties#load} and 047 * adds several useful extensions: 048 * 049 * <ul> 050 * <li> 051 * Each property has the syntax <code>key <separator> value</code>. The 052 * separators accepted are <code>'='</code>, <code>':'</code> and any white 053 * space character. Examples: 054 * <pre> 055 * key1 = value1 056 * key2 : value2 057 * key3 value3</pre> 058 * </li> 059 * <li> 060 * The <i>key</i> may use any character, separators must be escaped: 061 * <pre> 062 * key\:foo = bar</pre> 063 * </li> 064 * <li> 065 * <i>value</i> may be separated on different lines if a backslash 066 * is placed at the end of the line that continues below. 067 * </li> 068 * <li> 069 * <i>value</i> can contain <em>value delimiters</em> and will then be interpreted 070 * as a list of tokens. Default value delimiter is the comma ','. So the 071 * following property definition 072 * <pre> 073 * key = This property, has multiple, values 074 * </pre> 075 * will result in a property with three values. You can change the value 076 * delimiter using the <code>{@link AbstractConfiguration#setListDelimiter(char)}</code> 077 * method. Setting the delimiter to 0 will disable value splitting completely. 078 * </li> 079 * <li> 080 * Commas in each token are escaped placing a backslash right before 081 * the comma. 082 * </li> 083 * <li> 084 * If a <i>key</i> is used more than once, the values are appended 085 * like if they were on the same line separated with commas. <em>Note</em>: 086 * When the configuration file is written back to disk the associated 087 * <code>{@link PropertiesConfigurationLayout}</code> object (see below) will 088 * try to preserve as much of the original format as possible, i.e. properties 089 * with multiple values defined on a single line will also be written back on 090 * a single line, and multiple occurrences of a single key will be written on 091 * multiple lines. If the <code>addProperty()</code> method was called 092 * multiple times for adding multiple values to a property, these properties 093 * will per default be written on multiple lines in the output file, too. 094 * Some options of the <code>PropertiesConfigurationLayout</code> class have 095 * influence on that behavior. 096 * </li> 097 * <li> 098 * Blank lines and lines starting with character '#' or '!' are skipped. 099 * </li> 100 * <li> 101 * If a property is named "include" (or whatever is defined by 102 * setInclude() and getInclude() and the value of that property is 103 * the full path to a file on disk, that file will be included into 104 * the configuration. You can also pull in files relative to the parent 105 * configuration file. So if you have something like the following: 106 * 107 * include = additional.properties 108 * 109 * Then "additional.properties" is expected to be in the same 110 * directory as the parent configuration file. 111 * 112 * The properties in the included file are added to the parent configuration, 113 * they do not replace existing properties with the same key. 114 * 115 * </li> 116 * </ul> 117 * 118 * <p>Here is an example of a valid extended properties file: 119 * 120 * <p><pre> 121 * # lines starting with # are comments 122 * 123 * # This is the simplest property 124 * key = value 125 * 126 * # A long property may be separated on multiple lines 127 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ 128 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 129 * 130 * # This is a property with many tokens 131 * tokens_on_a_line = first token, second token 132 * 133 * # This sequence generates exactly the same result 134 * tokens_on_multiple_lines = first token 135 * tokens_on_multiple_lines = second token 136 * 137 * # commas may be escaped in tokens 138 * commas.escaped = Hi\, what'up? 139 * 140 * # properties can reference other properties 141 * base.prop = /base 142 * first.prop = ${base.prop}/first 143 * second.prop = ${first.prop}/second 144 * </pre> 145 * 146 * <p>A <code>PropertiesConfiguration</code> object is associated with an 147 * instance of the <code>{@link PropertiesConfigurationLayout}</code> class, 148 * which is responsible for storing the layout of the parsed properties file 149 * (i.e. empty lines, comments, and such things). The <code>getLayout()</code> 150 * method can be used to obtain this layout object. With <code>setLayout()</code> 151 * a new layout object can be set. This should be done before a properties file 152 * was loaded. 153 * <p><em>Note:</em>Configuration objects of this type can be read concurrently 154 * by multiple threads. However if one of these threads modifies the object, 155 * synchronization has to be performed manually. 156 * 157 * @see java.util.Properties#load 158 * 159 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a> 160 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a> 161 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a> 162 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 163 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a> 164 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a> 165 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 166 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a> 167 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 168 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a> 169 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 170 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 171 * @author Oliver Heger 172 * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a> 173 * @version $Id: PropertiesConfiguration.java 727168 2008-12-16 21:44:29Z oheger $ 174 */ 175 public class PropertiesConfiguration extends AbstractFileConfiguration 176 { 177 /** Constant for the supported comment characters.*/ 178 static final String COMMENT_CHARS = "#!"; 179 180 /** 181 * This is the name of the property that can point to other 182 * properties file for including other properties files. 183 */ 184 private static String include = "include"; 185 186 /** The list of possible key/value separators */ 187 private static final char[] SEPARATORS = new char[] {'=', ':'}; 188 189 /** The white space characters used as key/value separators. */ 190 private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; 191 192 /** 193 * The default encoding (ISO-8859-1 as specified by 194 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) 195 */ 196 private static final String DEFAULT_ENCODING = "ISO-8859-1"; 197 198 /** Constant for the platform specific line separator.*/ 199 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 200 201 /** Constant for the escaping character.*/ 202 private static final String ESCAPE = "\\"; 203 204 /** Constant for the radix of hex numbers.*/ 205 private static final int HEX_RADIX = 16; 206 207 /** Constant for the length of a unicode literal.*/ 208 private static final int UNICODE_LEN = 4; 209 210 /** Stores the layout object.*/ 211 private PropertiesConfigurationLayout layout; 212 213 /** Allow file inclusion or not */ 214 private boolean includesAllowed; 215 216 /** 217 * Creates an empty PropertyConfiguration object which can be 218 * used to synthesize a new Properties file by adding values and 219 * then saving(). 220 */ 221 public PropertiesConfiguration() 222 { 223 layout = createLayout(); 224 setIncludesAllowed(false); 225 } 226 227 /** 228 * Creates and loads the extended properties from the specified file. 229 * The specified file can contain "include = " properties which then 230 * are loaded and merged into the properties. 231 * 232 * @param fileName The name of the properties file to load. 233 * @throws ConfigurationException Error while loading the properties file 234 */ 235 public PropertiesConfiguration(String fileName) throws ConfigurationException 236 { 237 super(fileName); 238 } 239 240 /** 241 * Creates and loads the extended properties from the specified file. 242 * The specified file can contain "include = " properties which then 243 * are loaded and merged into the properties. If the file does not exist, 244 * an empty configuration will be created. Later the <code>save()</code> 245 * method can be called to save the properties to the specified file. 246 * 247 * @param file The properties file to load. 248 * @throws ConfigurationException Error while loading the properties file 249 */ 250 public PropertiesConfiguration(File file) throws ConfigurationException 251 { 252 super(file); 253 254 // If the file does not exist, no layout object was created. We have to 255 // do this manually in this case. 256 getLayout(); 257 } 258 259 /** 260 * Creates and loads the extended properties from the specified URL. 261 * The specified file can contain "include = " properties which then 262 * are loaded and merged into the properties. 263 * 264 * @param url The location of the properties file to load. 265 * @throws ConfigurationException Error while loading the properties file 266 */ 267 public PropertiesConfiguration(URL url) throws ConfigurationException 268 { 269 super(url); 270 } 271 272 /** 273 * Gets the property value for including other properties files. 274 * By default it is "include". 275 * 276 * @return A String. 277 */ 278 public static String getInclude() 279 { 280 return PropertiesConfiguration.include; 281 } 282 283 /** 284 * Sets the property value for including other properties files. 285 * By default it is "include". 286 * 287 * @param inc A String. 288 */ 289 public static void setInclude(String inc) 290 { 291 PropertiesConfiguration.include = inc; 292 } 293 294 /** 295 * Controls whether additional files can be loaded by the include = <xxx> 296 * statement or not. Base rule is, that objects created by the empty 297 * C'tor can not have included files. 298 * 299 * @param includesAllowed includesAllowed True if Includes are allowed. 300 */ 301 protected void setIncludesAllowed(boolean includesAllowed) 302 { 303 this.includesAllowed = includesAllowed; 304 } 305 306 /** 307 * Reports the status of file inclusion. 308 * 309 * @return True if include files are loaded. 310 */ 311 public boolean getIncludesAllowed() 312 { 313 return this.includesAllowed; 314 } 315 316 /** 317 * Return the comment header. 318 * 319 * @return the comment header 320 * @since 1.1 321 */ 322 public String getHeader() 323 { 324 return getLayout().getHeaderComment(); 325 } 326 327 /** 328 * Set the comment header. 329 * 330 * @param header the header to use 331 * @since 1.1 332 */ 333 public void setHeader(String header) 334 { 335 getLayout().setHeaderComment(header); 336 } 337 338 /** 339 * Returns the encoding to be used when loading or storing configuration 340 * data. This implementation ensures that the default encoding will be used 341 * if none has been set explicitly. 342 * 343 * @return the encoding 344 */ 345 public String getEncoding() 346 { 347 String enc = super.getEncoding(); 348 return (enc != null) ? enc : DEFAULT_ENCODING; 349 } 350 351 /** 352 * Returns the associated layout object. 353 * 354 * @return the associated layout object 355 * @since 1.3 356 */ 357 public synchronized PropertiesConfigurationLayout getLayout() 358 { 359 if (layout == null) 360 { 361 layout = createLayout(); 362 } 363 return layout; 364 } 365 366 /** 367 * Sets the associated layout object. 368 * 369 * @param layout the new layout object; can be <b>null</b>, then a new 370 * layout object will be created 371 * @since 1.3 372 */ 373 public synchronized void setLayout(PropertiesConfigurationLayout layout) 374 { 375 // only one layout must exist 376 if (this.layout != null) 377 { 378 removeConfigurationListener(this.layout); 379 } 380 381 if (layout == null) 382 { 383 this.layout = createLayout(); 384 } 385 else 386 { 387 this.layout = layout; 388 } 389 } 390 391 /** 392 * Creates the associated layout object. This method is invoked when the 393 * layout object is accessed and has not been created yet. Derived classes 394 * can override this method to hook in a different layout implementation. 395 * 396 * @return the layout object to use 397 * @since 1.3 398 */ 399 protected PropertiesConfigurationLayout createLayout() 400 { 401 return new PropertiesConfigurationLayout(this); 402 } 403 404 /** 405 * Load the properties from the given reader. 406 * Note that the <code>clear()</code> method is not called, so 407 * the properties contained in the loaded file will be added to the 408 * actual set of properties. 409 * 410 * @param in An InputStream. 411 * 412 * @throws ConfigurationException if an error occurs 413 */ 414 public synchronized void load(Reader in) throws ConfigurationException 415 { 416 boolean oldAutoSave = isAutoSave(); 417 setAutoSave(false); 418 419 try 420 { 421 getLayout().load(in); 422 } 423 finally 424 { 425 setAutoSave(oldAutoSave); 426 } 427 } 428 429 /** 430 * Save the configuration to the specified stream. 431 * 432 * @param writer the output stream used to save the configuration 433 * @throws ConfigurationException if an error occurs 434 */ 435 public void save(Writer writer) throws ConfigurationException 436 { 437 enterNoReload(); 438 try 439 { 440 getLayout().save(writer); 441 } 442 finally 443 { 444 exitNoReload(); 445 } 446 } 447 448 /** 449 * Extend the setBasePath method to turn includes 450 * on and off based on the existence of a base path. 451 * 452 * @param basePath The new basePath to set. 453 */ 454 public void setBasePath(String basePath) 455 { 456 super.setBasePath(basePath); 457 setIncludesAllowed(StringUtils.isNotEmpty(basePath)); 458 } 459 460 /** 461 * Creates a copy of this object. 462 * 463 * @return the copy 464 */ 465 public Object clone() 466 { 467 PropertiesConfiguration copy = (PropertiesConfiguration) super.clone(); 468 if (layout != null) 469 { 470 copy.setLayout(new PropertiesConfigurationLayout(copy, layout)); 471 } 472 return copy; 473 } 474 475 /** 476 * This method is invoked by the associated 477 * <code>{@link PropertiesConfigurationLayout}</code> object for each 478 * property definition detected in the parsed properties file. Its task is 479 * to check whether this is a special property definition (e.g. the 480 * <code>include</code> property). If not, the property must be added to 481 * this configuration. The return value indicates whether the property 482 * should be treated as a normal property. If it is <b>false</b>, the 483 * layout object will ignore this property. 484 * 485 * @param key the property key 486 * @param value the property value 487 * @return a flag whether this is a normal property 488 * @throws ConfigurationException if an error occurs 489 * @since 1.3 490 */ 491 boolean propertyLoaded(String key, String value) 492 throws ConfigurationException 493 { 494 boolean result; 495 496 if (StringUtils.isNotEmpty(getInclude()) 497 && key.equalsIgnoreCase(getInclude())) 498 { 499 if (getIncludesAllowed()) 500 { 501 String[] files; 502 if (!isDelimiterParsingDisabled()) 503 { 504 files = StringUtils.split(value, getListDelimiter()); 505 } 506 else 507 { 508 files = new String[]{value}; 509 } 510 for (int i = 0; i < files.length; i++) 511 { 512 loadIncludeFile(interpolate(files[i].trim())); 513 } 514 } 515 result = false; 516 } 517 518 else 519 { 520 addProperty(key, value); 521 result = true; 522 } 523 524 return result; 525 } 526 527 /** 528 * Tests whether a line is a comment, i.e. whether it starts with a comment 529 * character. 530 * 531 * @param line the line 532 * @return a flag if this is a comment line 533 * @since 1.3 534 */ 535 static boolean isCommentLine(String line) 536 { 537 String s = line.trim(); 538 // blanc lines are also treated as comment lines 539 return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; 540 } 541 542 /** 543 * This class is used to read properties lines. These lines do 544 * not terminate with new-line chars but rather when there is no 545 * backslash sign a the end of the line. This is used to 546 * concatenate multiple lines for readability. 547 */ 548 public static class PropertiesReader extends LineNumberReader 549 { 550 /** Stores the comment lines for the currently processed property.*/ 551 private List commentLines; 552 553 /** Stores the name of the last read property.*/ 554 private String propertyName; 555 556 /** Stores the value of the last read property.*/ 557 private String propertyValue; 558 559 /** Stores the list delimiter character.*/ 560 private char delimiter; 561 562 /** 563 * Constructor. 564 * 565 * @param reader A Reader. 566 */ 567 public PropertiesReader(Reader reader) 568 { 569 this(reader, AbstractConfiguration.getDefaultListDelimiter()); 570 } 571 572 /** 573 * Creates a new instance of <code>PropertiesReader</code> and sets 574 * the underlaying reader and the list delimiter. 575 * 576 * @param reader the reader 577 * @param listDelimiter the list delimiter character 578 * @since 1.3 579 */ 580 public PropertiesReader(Reader reader, char listDelimiter) 581 { 582 super(reader); 583 commentLines = new ArrayList(); 584 delimiter = listDelimiter; 585 } 586 587 /** 588 * Reads a property line. Returns null if Stream is 589 * at EOF. Concatenates lines ending with "\". 590 * Skips lines beginning with "#" or "!" and empty lines. 591 * The return value is a property definition (<code><name></code> 592 * = <code><value></code>) 593 * 594 * @return A string containing a property value or null 595 * 596 * @throws IOException in case of an I/O error 597 */ 598 public String readProperty() throws IOException 599 { 600 commentLines.clear(); 601 StringBuffer buffer = new StringBuffer(); 602 603 while (true) 604 { 605 String line = readLine(); 606 if (line == null) 607 { 608 // EOF 609 return null; 610 } 611 612 if (isCommentLine(line)) 613 { 614 commentLines.add(line); 615 continue; 616 } 617 618 line = line.trim(); 619 620 if (checkCombineLines(line)) 621 { 622 line = line.substring(0, line.length() - 1); 623 buffer.append(line); 624 } 625 else 626 { 627 buffer.append(line); 628 break; 629 } 630 } 631 return buffer.toString(); 632 } 633 634 /** 635 * Parses the next property from the input stream and stores the found 636 * name and value in internal fields. These fields can be obtained using 637 * the provided getter methods. The return value indicates whether EOF 638 * was reached (<b>false</b>) or whether further properties are 639 * available (<b>true</b>). 640 * 641 * @return a flag if further properties are available 642 * @throws IOException if an error occurs 643 * @since 1.3 644 */ 645 public boolean nextProperty() throws IOException 646 { 647 String line = readProperty(); 648 649 if (line == null) 650 { 651 return false; // EOF 652 } 653 654 // parse the line 655 String[] property = parseProperty(line); 656 propertyName = StringEscapeUtils.unescapeJava(property[0]); 657 propertyValue = unescapeJava(property[1], delimiter); 658 return true; 659 } 660 661 /** 662 * Returns the comment lines that have been read for the last property. 663 * 664 * @return the comment lines for the last property returned by 665 * <code>readProperty()</code> 666 * @since 1.3 667 */ 668 public List getCommentLines() 669 { 670 return commentLines; 671 } 672 673 /** 674 * Returns the name of the last read property. This method can be called 675 * after <code>{@link #nextProperty()}</code> was invoked and its 676 * return value was <b>true</b>. 677 * 678 * @return the name of the last read property 679 * @since 1.3 680 */ 681 public String getPropertyName() 682 { 683 return propertyName; 684 } 685 686 /** 687 * Returns the value of the last read property. This method can be 688 * called after <code>{@link #nextProperty()}</code> was invoked and 689 * its return value was <b>true</b>. 690 * 691 * @return the value of the last read property 692 * @since 1.3 693 */ 694 public String getPropertyValue() 695 { 696 return propertyValue; 697 } 698 699 /** 700 * Checks if the passed in line should be combined with the following. 701 * This is true, if the line ends with an odd number of backslashes. 702 * 703 * @param line the line 704 * @return a flag if the lines should be combined 705 */ 706 private static boolean checkCombineLines(String line) 707 { 708 int bsCount = 0; 709 for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) 710 { 711 bsCount++; 712 } 713 714 return bsCount % 2 != 0; 715 } 716 717 /** 718 * Parse a property line and return the key and the value in an array. 719 * 720 * @param line the line to parse 721 * @return an array with the property's key and value 722 * @since 1.2 723 */ 724 private static String[] parseProperty(String line) 725 { 726 // sorry for this spaghetti code, please replace it as soon as 727 // possible with a regexp when the Java 1.3 requirement is dropped 728 729 String[] result = new String[2]; 730 StringBuffer key = new StringBuffer(); 731 StringBuffer value = new StringBuffer(); 732 733 // state of the automaton: 734 // 0: key parsing 735 // 1: antislash found while parsing the key 736 // 2: separator crossing 737 // 3: value parsing 738 int state = 0; 739 740 for (int pos = 0; pos < line.length(); pos++) 741 { 742 char c = line.charAt(pos); 743 744 switch (state) 745 { 746 case 0: 747 if (c == '\\') 748 { 749 state = 1; 750 } 751 else if (ArrayUtils.contains(WHITE_SPACE, c)) 752 { 753 // switch to the separator crossing state 754 state = 2; 755 } 756 else if (ArrayUtils.contains(SEPARATORS, c)) 757 { 758 // switch to the value parsing state 759 state = 3; 760 } 761 else 762 { 763 key.append(c); 764 } 765 766 break; 767 768 case 1: 769 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) 770 { 771 // this is an escaped separator or white space 772 key.append(c); 773 } 774 else 775 { 776 // another escaped character, the '\' is preserved 777 key.append('\\'); 778 key.append(c); 779 } 780 781 // return to the key parsing state 782 state = 0; 783 784 break; 785 786 case 2: 787 if (ArrayUtils.contains(WHITE_SPACE, c)) 788 { 789 // do nothing, eat all white spaces 790 state = 2; 791 } 792 else if (ArrayUtils.contains(SEPARATORS, c)) 793 { 794 // switch to the value parsing state 795 state = 3; 796 } 797 else 798 { 799 // any other character indicates we encoutered the beginning of the value 800 value.append(c); 801 802 // switch to the value parsing state 803 state = 3; 804 } 805 806 break; 807 808 case 3: 809 value.append(c); 810 break; 811 } 812 } 813 814 result[0] = key.toString().trim(); 815 result[1] = value.toString().trim(); 816 817 return result; 818 } 819 } // class PropertiesReader 820 821 /** 822 * This class is used to write properties lines. 823 */ 824 public static class PropertiesWriter extends FilterWriter 825 { 826 /** The delimiter for multi-valued properties.*/ 827 private char delimiter; 828 829 /** 830 * Constructor. 831 * 832 * @param writer a Writer object providing the underlying stream 833 * @param delimiter the delimiter character for multi-valued properties 834 */ 835 public PropertiesWriter(Writer writer, char delimiter) 836 { 837 super(writer); 838 this.delimiter = delimiter; 839 } 840 841 /** 842 * Write a property. 843 * 844 * @param key the key of the property 845 * @param value the value of the property 846 * 847 * @throws IOException if an I/O error occurs 848 */ 849 public void writeProperty(String key, Object value) throws IOException 850 { 851 writeProperty(key, value, false); 852 } 853 854 /** 855 * Write a property. 856 * 857 * @param key The key of the property 858 * @param values The array of values of the property 859 * 860 * @throws IOException if an I/O error occurs 861 */ 862 public void writeProperty(String key, List values) throws IOException 863 { 864 for (int i = 0; i < values.size(); i++) 865 { 866 writeProperty(key, values.get(i)); 867 } 868 } 869 870 /** 871 * Writes the given property and its value. If the value happens to be a 872 * list, the <code>forceSingleLine</code> flag is evaluated. If it is 873 * set, all values are written on a single line using the list delimiter 874 * as separator. 875 * 876 * @param key the property key 877 * @param value the property value 878 * @param forceSingleLine the "force single line" flag 879 * @throws IOException if an error occurs 880 * @since 1.3 881 */ 882 public void writeProperty(String key, Object value, 883 boolean forceSingleLine) throws IOException 884 { 885 String v; 886 887 if (value instanceof List) 888 { 889 List values = (List) value; 890 if (forceSingleLine) 891 { 892 v = makeSingleLineValue(values); 893 } 894 else 895 { 896 writeProperty(key, values); 897 return; 898 } 899 } 900 else 901 { 902 v = escapeValue(value); 903 } 904 905 write(escapeKey(key)); 906 write(" = "); 907 write(v); 908 909 writeln(null); 910 } 911 912 /** 913 * Write a comment. 914 * 915 * @param comment the comment to write 916 * @throws IOException if an I/O error occurs 917 */ 918 public void writeComment(String comment) throws IOException 919 { 920 writeln("# " + comment); 921 } 922 923 /** 924 * Escape the separators in the key. 925 * 926 * @param key the key 927 * @return the escaped key 928 * @since 1.2 929 */ 930 private String escapeKey(String key) 931 { 932 StringBuffer newkey = new StringBuffer(); 933 934 for (int i = 0; i < key.length(); i++) 935 { 936 char c = key.charAt(i); 937 938 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) 939 { 940 // escape the separator 941 newkey.append('\\'); 942 newkey.append(c); 943 } 944 else 945 { 946 newkey.append(c); 947 } 948 } 949 950 return newkey.toString(); 951 } 952 953 /** 954 * Escapes the given property value. Delimiter characters in the value 955 * will be escaped. 956 * 957 * @param value the property value 958 * @return the escaped property value 959 * @since 1.3 960 */ 961 private String escapeValue(Object value) 962 { 963 String escapedValue = StringEscapeUtils.escapeJava(String.valueOf(value)); 964 if (delimiter != 0) 965 { 966 escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter); 967 } 968 return escapedValue; 969 } 970 971 /** 972 * Transforms a list of values into a single line value. 973 * 974 * @param values the list with the values 975 * @return a string with the single line value (can be <b>null</b>) 976 * @since 1.3 977 */ 978 private String makeSingleLineValue(List values) 979 { 980 if (!values.isEmpty()) 981 { 982 Iterator it = values.iterator(); 983 String lastValue = escapeValue(it.next()); 984 StringBuffer buf = new StringBuffer(lastValue); 985 while (it.hasNext()) 986 { 987 // if the last value ended with an escape character, it has 988 // to be escaped itself; otherwise the list delimiter will 989 // be escaped 990 if (lastValue.endsWith(ESCAPE)) 991 { 992 buf.append(ESCAPE).append(ESCAPE); 993 } 994 buf.append(delimiter); 995 lastValue = escapeValue(it.next()); 996 buf.append(lastValue); 997 } 998 return buf.toString(); 999 } 1000 else 1001 { 1002 return null; 1003 } 1004 } 1005 1006 /** 1007 * Helper method for writing a line with the platform specific line 1008 * ending. 1009 * 1010 * @param s the content of the line (may be <b>null</b>) 1011 * @throws IOException if an error occurs 1012 * @since 1.3 1013 */ 1014 public void writeln(String s) throws IOException 1015 { 1016 if (s != null) 1017 { 1018 write(s); 1019 } 1020 write(LINE_SEPARATOR); 1021 } 1022 1023 } // class PropertiesWriter 1024 1025 /** 1026 * <p>Unescapes any Java literals found in the <code>String</code> to a 1027 * <code>Writer</code>.</p> This is a slightly modified version of the 1028 * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't 1029 * drop escaped separators (i.e '\,'). 1030 * 1031 * @param str the <code>String</code> to unescape, may be null 1032 * @param delimiter the delimiter for multi-valued properties 1033 * @return the processed string 1034 * @throws IllegalArgumentException if the Writer is <code>null</code> 1035 */ 1036 protected static String unescapeJava(String str, char delimiter) 1037 { 1038 if (str == null) 1039 { 1040 return null; 1041 } 1042 int sz = str.length(); 1043 StringBuffer out = new StringBuffer(sz); 1044 StringBuffer unicode = new StringBuffer(UNICODE_LEN); 1045 boolean hadSlash = false; 1046 boolean inUnicode = false; 1047 for (int i = 0; i < sz; i++) 1048 { 1049 char ch = str.charAt(i); 1050 if (inUnicode) 1051 { 1052 // if in unicode, then we're reading unicode 1053 // values in somehow 1054 unicode.append(ch); 1055 if (unicode.length() == UNICODE_LEN) 1056 { 1057 // unicode now contains the four hex digits 1058 // which represents our unicode character 1059 try 1060 { 1061 int value = Integer.parseInt(unicode.toString(), HEX_RADIX); 1062 out.append((char) value); 1063 unicode.setLength(0); 1064 inUnicode = false; 1065 hadSlash = false; 1066 } 1067 catch (NumberFormatException nfe) 1068 { 1069 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe); 1070 } 1071 } 1072 continue; 1073 } 1074 1075 if (hadSlash) 1076 { 1077 // handle an escaped value 1078 hadSlash = false; 1079 1080 if (ch == '\\') 1081 { 1082 out.append('\\'); 1083 } 1084 else if (ch == '\'') 1085 { 1086 out.append('\''); 1087 } 1088 else if (ch == '\"') 1089 { 1090 out.append('"'); 1091 } 1092 else if (ch == 'r') 1093 { 1094 out.append('\r'); 1095 } 1096 else if (ch == 'f') 1097 { 1098 out.append('\f'); 1099 } 1100 else if (ch == 't') 1101 { 1102 out.append('\t'); 1103 } 1104 else if (ch == 'n') 1105 { 1106 out.append('\n'); 1107 } 1108 else if (ch == 'b') 1109 { 1110 out.append('\b'); 1111 } 1112 else if (ch == delimiter) 1113 { 1114 out.append('\\'); 1115 out.append(delimiter); 1116 } 1117 else if (ch == 'u') 1118 { 1119 // uh-oh, we're in unicode country.... 1120 inUnicode = true; 1121 } 1122 else 1123 { 1124 out.append(ch); 1125 } 1126 1127 continue; 1128 } 1129 else if (ch == '\\') 1130 { 1131 hadSlash = true; 1132 continue; 1133 } 1134 out.append(ch); 1135 } 1136 1137 if (hadSlash) 1138 { 1139 // then we're in the weird case of a \ at the end of the 1140 // string, let's output it anyway. 1141 out.append('\\'); 1142 } 1143 1144 return out.toString(); 1145 } 1146 1147 /** 1148 * Helper method for loading an included properties file. This method is 1149 * called by <code>load()</code> when an <code>include</code> property 1150 * is encountered. It tries to resolve relative file names based on the 1151 * current base path. If this fails, a resolution based on the location of 1152 * this properties file is tried. 1153 * 1154 * @param fileName the name of the file to load 1155 * @throws ConfigurationException if loading fails 1156 */ 1157 private void loadIncludeFile(String fileName) throws ConfigurationException 1158 { 1159 URL url = ConfigurationUtils.locate(getBasePath(), fileName); 1160 if (url == null) 1161 { 1162 URL baseURL = getURL(); 1163 if (baseURL != null) 1164 { 1165 url = ConfigurationUtils.locate(baseURL.toString(), fileName); 1166 } 1167 } 1168 1169 if (url == null) 1170 { 1171 throw new ConfigurationException("Cannot resolve include file " 1172 + fileName); 1173 } 1174 load(url); 1175 } 1176 }