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 1162387 2011-08-27 16:05:20Z oheger $ 174 */ 175 public class PropertiesConfiguration extends AbstractFileConfiguration 176 { 177 /** Constant for the supported comment characters.*/ 178 static final String COMMENT_CHARS = "#!"; 179 180 /** Constant for the default properties separator.*/ 181 static final String DEFAULT_SEPARATOR = " = "; 182 183 /** 184 * Constant for the default <code>IOFactory</code>. This instance is used 185 * when no specific factory was set. 186 */ 187 private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory(); 188 189 /** 190 * This is the name of the property that can point to other 191 * properties file for including other properties files. 192 */ 193 private static String include = "include"; 194 195 /** The list of possible key/value separators */ 196 private static final char[] SEPARATORS = new char[] {'=', ':'}; 197 198 /** The white space characters used as key/value separators. */ 199 private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; 200 201 /** 202 * The default encoding (ISO-8859-1 as specified by 203 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) 204 */ 205 private static final String DEFAULT_ENCODING = "ISO-8859-1"; 206 207 /** Constant for the platform specific line separator.*/ 208 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 209 210 /** Constant for the escaping character.*/ 211 private static final String ESCAPE = "\\"; 212 213 /** Constant for the escaped escaping character.*/ 214 private static final String DOUBLE_ESC = ESCAPE + ESCAPE; 215 216 /** Constant for the radix of hex numbers.*/ 217 private static final int HEX_RADIX = 16; 218 219 /** Constant for the length of a unicode literal.*/ 220 private static final int UNICODE_LEN = 4; 221 222 /** Stores the layout object.*/ 223 private PropertiesConfigurationLayout layout; 224 225 /** The IOFactory for creating readers and writers.*/ 226 private volatile IOFactory ioFactory; 227 228 /** Allow file inclusion or not */ 229 private boolean includesAllowed; 230 231 /** 232 * Creates an empty PropertyConfiguration object which can be 233 * used to synthesize a new Properties file by adding values and 234 * then saving(). 235 */ 236 public PropertiesConfiguration() 237 { 238 layout = createLayout(); 239 setIncludesAllowed(false); 240 } 241 242 /** 243 * Creates and loads the extended properties from the specified file. 244 * The specified file can contain "include = " properties which then 245 * are loaded and merged into the properties. 246 * 247 * @param fileName The name of the properties file to load. 248 * @throws ConfigurationException Error while loading the properties file 249 */ 250 public PropertiesConfiguration(String fileName) throws ConfigurationException 251 { 252 super(fileName); 253 } 254 255 /** 256 * Creates and loads the extended properties from the specified file. 257 * The specified file can contain "include = " properties which then 258 * are loaded and merged into the properties. If the file does not exist, 259 * an empty configuration will be created. Later the <code>save()</code> 260 * method can be called to save the properties to the specified file. 261 * 262 * @param file The properties file to load. 263 * @throws ConfigurationException Error while loading the properties file 264 */ 265 public PropertiesConfiguration(File file) throws ConfigurationException 266 { 267 super(file); 268 269 // If the file does not exist, no layout object was created. We have to 270 // do this manually in this case. 271 getLayout(); 272 } 273 274 /** 275 * Creates and loads the extended properties from the specified URL. 276 * The specified file can contain "include = " properties which then 277 * are loaded and merged into the properties. 278 * 279 * @param url The location of the properties file to load. 280 * @throws ConfigurationException Error while loading the properties file 281 */ 282 public PropertiesConfiguration(URL url) throws ConfigurationException 283 { 284 super(url); 285 } 286 287 /** 288 * Gets the property value for including other properties files. 289 * By default it is "include". 290 * 291 * @return A String. 292 */ 293 public static String getInclude() 294 { 295 return PropertiesConfiguration.include; 296 } 297 298 /** 299 * Sets the property value for including other properties files. 300 * By default it is "include". 301 * 302 * @param inc A String. 303 */ 304 public static void setInclude(String inc) 305 { 306 PropertiesConfiguration.include = inc; 307 } 308 309 /** 310 * Controls whether additional files can be loaded by the include = <xxx> 311 * statement or not. Base rule is, that objects created by the empty 312 * C'tor can not have included files. 313 * 314 * @param includesAllowed includesAllowed True if Includes are allowed. 315 */ 316 protected void setIncludesAllowed(boolean includesAllowed) 317 { 318 this.includesAllowed = includesAllowed; 319 } 320 321 /** 322 * Reports the status of file inclusion. 323 * 324 * @return True if include files are loaded. 325 */ 326 public boolean getIncludesAllowed() 327 { 328 return this.includesAllowed; 329 } 330 331 /** 332 * Return the comment header. 333 * 334 * @return the comment header 335 * @since 1.1 336 */ 337 public String getHeader() 338 { 339 return getLayout().getHeaderComment(); 340 } 341 342 /** 343 * Set the comment header. 344 * 345 * @param header the header to use 346 * @since 1.1 347 */ 348 public void setHeader(String header) 349 { 350 getLayout().setHeaderComment(header); 351 } 352 353 /** 354 * Returns the encoding to be used when loading or storing configuration 355 * data. This implementation ensures that the default encoding will be used 356 * if none has been set explicitly. 357 * 358 * @return the encoding 359 */ 360 public String getEncoding() 361 { 362 String enc = super.getEncoding(); 363 return (enc != null) ? enc : DEFAULT_ENCODING; 364 } 365 366 /** 367 * Returns the associated layout object. 368 * 369 * @return the associated layout object 370 * @since 1.3 371 */ 372 public synchronized PropertiesConfigurationLayout getLayout() 373 { 374 if (layout == null) 375 { 376 layout = createLayout(); 377 } 378 return layout; 379 } 380 381 /** 382 * Sets the associated layout object. 383 * 384 * @param layout the new layout object; can be <b>null</b>, then a new 385 * layout object will be created 386 * @since 1.3 387 */ 388 public synchronized void setLayout(PropertiesConfigurationLayout layout) 389 { 390 // only one layout must exist 391 if (this.layout != null) 392 { 393 removeConfigurationListener(this.layout); 394 } 395 396 if (layout == null) 397 { 398 this.layout = createLayout(); 399 } 400 else 401 { 402 this.layout = layout; 403 } 404 } 405 406 /** 407 * Creates the associated layout object. This method is invoked when the 408 * layout object is accessed and has not been created yet. Derived classes 409 * can override this method to hook in a different layout implementation. 410 * 411 * @return the layout object to use 412 * @since 1.3 413 */ 414 protected PropertiesConfigurationLayout createLayout() 415 { 416 return new PropertiesConfigurationLayout(this); 417 } 418 419 /** 420 * Returns the <code>IOFactory</code> to be used for creating readers and 421 * writers when loading or saving this configuration. 422 * 423 * @return the <code>IOFactory</code> 424 * @since 1.7 425 */ 426 public IOFactory getIOFactory() 427 { 428 return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY; 429 } 430 431 /** 432 * Sets the <code>IOFactory</code> to be used for creating readers and 433 * writers when loading or saving this configuration. Using this method a 434 * client can customize the reader and writer classes used by the load and 435 * save operations. Note that this method must be called before invoking 436 * one of the <code>load()</code> and <code>save()</code> methods. 437 * Especially, if you want to use a custom <code>IOFactory</code> for 438 * changing the <code>PropertiesReader</code>, you cannot load the 439 * configuration data in the constructor. 440 * 441 * @param ioFactory the new <code>IOFactory</code> (must not be <b>null</b>) 442 * @throws IllegalArgumentException if the <code>IOFactory</code> is 443 * <b>null</b> 444 * @since 1.7 445 */ 446 public void setIOFactory(IOFactory ioFactory) 447 { 448 if (ioFactory == null) 449 { 450 throw new IllegalArgumentException("IOFactory must not be null!"); 451 } 452 453 this.ioFactory = ioFactory; 454 } 455 456 /** 457 * Load the properties from the given reader. 458 * Note that the <code>clear()</code> method is not called, so 459 * the properties contained in the loaded file will be added to the 460 * actual set of properties. 461 * 462 * @param in An InputStream. 463 * 464 * @throws ConfigurationException if an error occurs 465 */ 466 public synchronized void load(Reader in) throws ConfigurationException 467 { 468 boolean oldAutoSave = isAutoSave(); 469 setAutoSave(false); 470 471 try 472 { 473 getLayout().load(in); 474 } 475 finally 476 { 477 setAutoSave(oldAutoSave); 478 } 479 } 480 481 /** 482 * Save the configuration to the specified stream. 483 * 484 * @param writer the output stream used to save the configuration 485 * @throws ConfigurationException if an error occurs 486 */ 487 public void save(Writer writer) throws ConfigurationException 488 { 489 enterNoReload(); 490 try 491 { 492 getLayout().save(writer); 493 } 494 finally 495 { 496 exitNoReload(); 497 } 498 } 499 500 /** 501 * Extend the setBasePath method to turn includes 502 * on and off based on the existence of a base path. 503 * 504 * @param basePath The new basePath to set. 505 */ 506 public void setBasePath(String basePath) 507 { 508 super.setBasePath(basePath); 509 setIncludesAllowed(StringUtils.isNotEmpty(basePath)); 510 } 511 512 /** 513 * Creates a copy of this object. 514 * 515 * @return the copy 516 */ 517 public Object clone() 518 { 519 PropertiesConfiguration copy = (PropertiesConfiguration) super.clone(); 520 if (layout != null) 521 { 522 copy.setLayout(new PropertiesConfigurationLayout(copy, layout)); 523 } 524 return copy; 525 } 526 527 /** 528 * This method is invoked by the associated 529 * <code>{@link PropertiesConfigurationLayout}</code> object for each 530 * property definition detected in the parsed properties file. Its task is 531 * to check whether this is a special property definition (e.g. the 532 * <code>include</code> property). If not, the property must be added to 533 * this configuration. The return value indicates whether the property 534 * should be treated as a normal property. If it is <b>false</b>, the 535 * layout object will ignore this property. 536 * 537 * @param key the property key 538 * @param value the property value 539 * @return a flag whether this is a normal property 540 * @throws ConfigurationException if an error occurs 541 * @since 1.3 542 */ 543 boolean propertyLoaded(String key, String value) 544 throws ConfigurationException 545 { 546 boolean result; 547 548 if (StringUtils.isNotEmpty(getInclude()) 549 && key.equalsIgnoreCase(getInclude())) 550 { 551 if (getIncludesAllowed()) 552 { 553 String[] files; 554 if (!isDelimiterParsingDisabled()) 555 { 556 files = StringUtils.split(value, getListDelimiter()); 557 } 558 else 559 { 560 files = new String[]{value}; 561 } 562 for (int i = 0; i < files.length; i++) 563 { 564 loadIncludeFile(interpolate(files[i].trim())); 565 } 566 } 567 result = false; 568 } 569 570 else 571 { 572 addProperty(key, value); 573 result = true; 574 } 575 576 return result; 577 } 578 579 /** 580 * Tests whether a line is a comment, i.e. whether it starts with a comment 581 * character. 582 * 583 * @param line the line 584 * @return a flag if this is a comment line 585 * @since 1.3 586 */ 587 static boolean isCommentLine(String line) 588 { 589 String s = line.trim(); 590 // blanc lines are also treated as comment lines 591 return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; 592 } 593 594 /** 595 * Returns the number of trailing backslashes. This is sometimes needed for 596 * the correct handling of escape characters. 597 * 598 * @param line the string to investigate 599 * @return the number of trailing backslashes 600 */ 601 private static int countTrailingBS(String line) 602 { 603 int bsCount = 0; 604 for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) 605 { 606 bsCount++; 607 } 608 609 return bsCount; 610 } 611 612 /** 613 * This class is used to read properties lines. These lines do 614 * not terminate with new-line chars but rather when there is no 615 * backslash sign a the end of the line. This is used to 616 * concatenate multiple lines for readability. 617 */ 618 public static class PropertiesReader extends LineNumberReader 619 { 620 /** Stores the comment lines for the currently processed property.*/ 621 private List commentLines; 622 623 /** Stores the name of the last read property.*/ 624 private String propertyName; 625 626 /** Stores the value of the last read property.*/ 627 private String propertyValue; 628 629 /** Stores the property separator of the last read property.*/ 630 private String propertySeparator = DEFAULT_SEPARATOR; 631 632 /** Stores the list delimiter character.*/ 633 private char delimiter; 634 635 /** 636 * Constructor. 637 * 638 * @param reader A Reader. 639 */ 640 public PropertiesReader(Reader reader) 641 { 642 this(reader, AbstractConfiguration.getDefaultListDelimiter()); 643 } 644 645 /** 646 * Creates a new instance of <code>PropertiesReader</code> and sets 647 * the underlying reader and the list delimiter. 648 * 649 * @param reader the reader 650 * @param listDelimiter the list delimiter character 651 * @since 1.3 652 */ 653 public PropertiesReader(Reader reader, char listDelimiter) 654 { 655 super(reader); 656 commentLines = new ArrayList(); 657 delimiter = listDelimiter; 658 } 659 660 /** 661 * Reads a property line. Returns null if Stream is 662 * at EOF. Concatenates lines ending with "\". 663 * Skips lines beginning with "#" or "!" and empty lines. 664 * The return value is a property definition (<code><name></code> 665 * = <code><value></code>) 666 * 667 * @return A string containing a property value or null 668 * 669 * @throws IOException in case of an I/O error 670 */ 671 public String readProperty() throws IOException 672 { 673 commentLines.clear(); 674 StringBuffer buffer = new StringBuffer(); 675 676 while (true) 677 { 678 String line = readLine(); 679 if (line == null) 680 { 681 // EOF 682 return null; 683 } 684 685 if (isCommentLine(line)) 686 { 687 commentLines.add(line); 688 continue; 689 } 690 691 line = line.trim(); 692 693 if (checkCombineLines(line)) 694 { 695 line = line.substring(0, line.length() - 1); 696 buffer.append(line); 697 } 698 else 699 { 700 buffer.append(line); 701 break; 702 } 703 } 704 return buffer.toString(); 705 } 706 707 /** 708 * Parses the next property from the input stream and stores the found 709 * name and value in internal fields. These fields can be obtained using 710 * the provided getter methods. The return value indicates whether EOF 711 * was reached (<b>false</b>) or whether further properties are 712 * available (<b>true</b>). 713 * 714 * @return a flag if further properties are available 715 * @throws IOException if an error occurs 716 * @since 1.3 717 */ 718 public boolean nextProperty() throws IOException 719 { 720 String line = readProperty(); 721 722 if (line == null) 723 { 724 return false; // EOF 725 } 726 727 // parse the line 728 parseProperty(line); 729 return true; 730 } 731 732 /** 733 * Returns the comment lines that have been read for the last property. 734 * 735 * @return the comment lines for the last property returned by 736 * <code>readProperty()</code> 737 * @since 1.3 738 */ 739 public List getCommentLines() 740 { 741 return commentLines; 742 } 743 744 /** 745 * Returns the name of the last read property. This method can be called 746 * after <code>{@link #nextProperty()}</code> was invoked and its 747 * return value was <b>true</b>. 748 * 749 * @return the name of the last read property 750 * @since 1.3 751 */ 752 public String getPropertyName() 753 { 754 return propertyName; 755 } 756 757 /** 758 * Returns the value of the last read property. This method can be 759 * called after <code>{@link #nextProperty()}</code> was invoked and 760 * its return value was <b>true</b>. 761 * 762 * @return the value of the last read property 763 * @since 1.3 764 */ 765 public String getPropertyValue() 766 { 767 return propertyValue; 768 } 769 770 /** 771 * Returns the separator that was used for the last read property. The 772 * separator can be stored so that it can later be restored when saving 773 * the configuration. 774 * 775 * @return the separator for the last read property 776 * @since 1.7 777 */ 778 public String getPropertySeparator() 779 { 780 return propertySeparator; 781 } 782 783 /** 784 * Parses a line read from the properties file. This method is called 785 * for each non-comment line read from the source file. Its task is to 786 * split the passed in line into the property key and its value. The 787 * results of the parse operation can be stored by calling the 788 * <code>initPropertyXXX()</code> methods. 789 * 790 * @param line the line read from the properties file 791 * @since 1.7 792 */ 793 protected void parseProperty(String line) 794 { 795 String[] property = doParseProperty(line); 796 initPropertyName(property[0]); 797 initPropertyValue(property[1]); 798 initPropertySeparator(property[2]); 799 } 800 801 /** 802 * Sets the name of the current property. This method can be called by 803 * <code>parseProperty()</code> for storing the results of the parse 804 * operation. It also ensures that the property key is correctly 805 * escaped. 806 * 807 * @param name the name of the current property 808 * @since 1.7 809 */ 810 protected void initPropertyName(String name) 811 { 812 propertyName = StringEscapeUtils.unescapeJava(name); 813 } 814 815 /** 816 * Sets the value of the current property. This method can be called by 817 * <code>parseProperty()</code> for storing the results of the parse 818 * operation. It also ensures that the property value is correctly 819 * escaped. 820 * 821 * @param value the value of the current property 822 * @since 1.7 823 */ 824 protected void initPropertyValue(String value) 825 { 826 propertyValue = unescapeJava(value, delimiter); 827 } 828 829 /** 830 * Sets the separator of the current property. This method can be called 831 * by <code>parseProperty()</code>. It allows the associated layout 832 * object to keep track of the property separators. When saving the 833 * configuration the separators can be restored. 834 * 835 * @param value the separator used for the current property 836 * @since 1.7 837 */ 838 protected void initPropertySeparator(String value) 839 { 840 propertySeparator = value; 841 } 842 843 /** 844 * Checks if the passed in line should be combined with the following. 845 * This is true, if the line ends with an odd number of backslashes. 846 * 847 * @param line the line 848 * @return a flag if the lines should be combined 849 */ 850 private static boolean checkCombineLines(String line) 851 { 852 return countTrailingBS(line) % 2 != 0; 853 } 854 855 /** 856 * Parse a property line and return the key, the value, and the separator in an array. 857 * 858 * @param line the line to parse 859 * @return an array with the property's key, value, and separator 860 */ 861 private static String[] doParseProperty(String line) 862 { 863 // sorry for this spaghetti code, please replace it as soon as 864 // possible with a regexp when the Java 1.3 requirement is dropped 865 866 String[] result = new String[3]; 867 StringBuffer key = new StringBuffer(); 868 StringBuffer value = new StringBuffer(); 869 StringBuffer separator = new StringBuffer(); 870 871 // state of the automaton: 872 // 0: key parsing 873 // 1: antislash found while parsing the key 874 // 2: separator crossing 875 // 3: value parsing 876 int state = 0; 877 878 for (int pos = 0; pos < line.length(); pos++) 879 { 880 char c = line.charAt(pos); 881 882 switch (state) 883 { 884 case 0: 885 if (c == '\\') 886 { 887 state = 1; 888 } 889 else if (ArrayUtils.contains(WHITE_SPACE, c)) 890 { 891 // switch to the separator crossing state 892 separator.append(c); 893 state = 2; 894 } 895 else if (ArrayUtils.contains(SEPARATORS, c)) 896 { 897 // switch to the value parsing state 898 separator.append(c); 899 state = 3; 900 } 901 else 902 { 903 key.append(c); 904 } 905 906 break; 907 908 case 1: 909 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) 910 { 911 // this is an escaped separator or white space 912 key.append(c); 913 } 914 else 915 { 916 // another escaped character, the '\' is preserved 917 key.append('\\'); 918 key.append(c); 919 } 920 921 // return to the key parsing state 922 state = 0; 923 924 break; 925 926 case 2: 927 if (ArrayUtils.contains(WHITE_SPACE, c) || ArrayUtils.contains(SEPARATORS, c)) 928 { 929 // record the separator 930 separator.append(c); 931 } 932 else 933 { 934 // any other character indicates we encountered the beginning of the value 935 value.append(c); 936 937 // switch to the value parsing state 938 state = 3; 939 } 940 941 break; 942 943 case 3: 944 value.append(c); 945 break; 946 } 947 } 948 949 result[0] = key.toString().trim(); 950 result[1] = value.toString().trim(); 951 result[2] = separator.toString(); 952 953 return result; 954 } 955 } // class PropertiesReader 956 957 /** 958 * This class is used to write properties lines. The most important method 959 * is <code>writeProperty(String, Object, boolean)</code>, which is called 960 * during a save operation for each property found in the configuration. 961 */ 962 public static class PropertiesWriter extends FilterWriter 963 { 964 /** Constant for the initial size when creating a string buffer. */ 965 private static final int BUF_SIZE = 8; 966 967 /** The delimiter for multi-valued properties.*/ 968 private char delimiter; 969 970 /** The separator to be used for the current property. */ 971 private String currentSeparator; 972 973 /** The global separator. If set, it overrides the current separator.*/ 974 private String globalSeparator; 975 976 /** The line separator.*/ 977 private String lineSeparator; 978 979 /** 980 * Constructor. 981 * 982 * @param writer a Writer object providing the underlying stream 983 * @param delimiter the delimiter character for multi-valued properties 984 */ 985 public PropertiesWriter(Writer writer, char delimiter) 986 { 987 super(writer); 988 this.delimiter = delimiter; 989 } 990 991 /** 992 * Returns the current property separator. 993 * 994 * @return the current property separator 995 * @since 1.7 996 */ 997 public String getCurrentSeparator() 998 { 999 return currentSeparator; 1000 } 1001 1002 /** 1003 * Sets the current property separator. This separator is used when 1004 * writing the next property. 1005 * 1006 * @param currentSeparator the current property separator 1007 * @since 1.7 1008 */ 1009 public void setCurrentSeparator(String currentSeparator) 1010 { 1011 this.currentSeparator = currentSeparator; 1012 } 1013 1014 /** 1015 * Returns the global property separator. 1016 * 1017 * @return the global property separator 1018 * @since 1.7 1019 */ 1020 public String getGlobalSeparator() 1021 { 1022 return globalSeparator; 1023 } 1024 1025 /** 1026 * Sets the global property separator. This separator corresponds to the 1027 * <code>globalSeparator</code> property of 1028 * {@link PropertiesConfigurationLayout}. It defines the separator to be 1029 * used for all properties. If it is undefined, the current separator is 1030 * used. 1031 * 1032 * @param globalSeparator the global property separator 1033 * @since 1.7 1034 */ 1035 public void setGlobalSeparator(String globalSeparator) 1036 { 1037 this.globalSeparator = globalSeparator; 1038 } 1039 1040 /** 1041 * Returns the line separator. 1042 * 1043 * @return the line separator 1044 * @since 1.7 1045 */ 1046 public String getLineSeparator() 1047 { 1048 return (lineSeparator != null) ? lineSeparator : LINE_SEPARATOR; 1049 } 1050 1051 /** 1052 * Sets the line separator. Each line written by this writer is 1053 * terminated with this separator. If not set, the platform-specific 1054 * line separator is used. 1055 * 1056 * @param lineSeparator the line separator to be used 1057 * @since 1.7 1058 */ 1059 public void setLineSeparator(String lineSeparator) 1060 { 1061 this.lineSeparator = lineSeparator; 1062 } 1063 1064 /** 1065 * Write a property. 1066 * 1067 * @param key the key of the property 1068 * @param value the value of the property 1069 * 1070 * @throws IOException if an I/O error occurs 1071 */ 1072 public void writeProperty(String key, Object value) throws IOException 1073 { 1074 writeProperty(key, value, false); 1075 } 1076 1077 /** 1078 * Write a property. 1079 * 1080 * @param key The key of the property 1081 * @param values The array of values of the property 1082 * 1083 * @throws IOException if an I/O error occurs 1084 */ 1085 public void writeProperty(String key, List values) throws IOException 1086 { 1087 for (int i = 0; i < values.size(); i++) 1088 { 1089 writeProperty(key, values.get(i)); 1090 } 1091 } 1092 1093 /** 1094 * Writes the given property and its value. If the value happens to be a 1095 * list, the <code>forceSingleLine</code> flag is evaluated. If it is 1096 * set, all values are written on a single line using the list delimiter 1097 * as separator. 1098 * 1099 * @param key the property key 1100 * @param value the property value 1101 * @param forceSingleLine the "force single line" flag 1102 * @throws IOException if an error occurs 1103 * @since 1.3 1104 */ 1105 public void writeProperty(String key, Object value, 1106 boolean forceSingleLine) throws IOException 1107 { 1108 String v; 1109 1110 if (value instanceof List) 1111 { 1112 List values = (List) value; 1113 if (forceSingleLine) 1114 { 1115 v = makeSingleLineValue(values); 1116 } 1117 else 1118 { 1119 writeProperty(key, values); 1120 return; 1121 } 1122 } 1123 else 1124 { 1125 v = escapeValue(value, false); 1126 } 1127 1128 write(escapeKey(key)); 1129 write(fetchSeparator(key, value)); 1130 write(v); 1131 1132 writeln(null); 1133 } 1134 1135 /** 1136 * Write a comment. 1137 * 1138 * @param comment the comment to write 1139 * @throws IOException if an I/O error occurs 1140 */ 1141 public void writeComment(String comment) throws IOException 1142 { 1143 writeln("# " + comment); 1144 } 1145 1146 /** 1147 * Escape the separators in the key. 1148 * 1149 * @param key the key 1150 * @return the escaped key 1151 * @since 1.2 1152 */ 1153 private String escapeKey(String key) 1154 { 1155 StringBuffer newkey = new StringBuffer(); 1156 1157 for (int i = 0; i < key.length(); i++) 1158 { 1159 char c = key.charAt(i); 1160 1161 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) 1162 { 1163 // escape the separator 1164 newkey.append('\\'); 1165 newkey.append(c); 1166 } 1167 else 1168 { 1169 newkey.append(c); 1170 } 1171 } 1172 1173 return newkey.toString(); 1174 } 1175 1176 /** 1177 * Escapes the given property value. Delimiter characters in the value 1178 * will be escaped. 1179 * 1180 * @param value the property value 1181 * @param inList a flag whether the value is part of a list 1182 * @return the escaped property value 1183 * @since 1.3 1184 */ 1185 private String escapeValue(Object value, boolean inList) 1186 { 1187 String escapedValue = handleBackslashs(value, inList); 1188 if (delimiter != 0) 1189 { 1190 escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter); 1191 } 1192 return escapedValue; 1193 } 1194 1195 /** 1196 * Performs the escaping of backslashes in the specified properties 1197 * value. Because a double backslash is used to escape the escape 1198 * character of a list delimiter, double backslashes also have to be 1199 * escaped if the property is part of a (single line) list. Then, in all 1200 * cases each backslash has to be doubled in order to produce a valid 1201 * properties file. 1202 * 1203 * @param value the value to be escaped 1204 * @param inList a flag whether the value is part of a list 1205 * @return the value with escaped backslashes as string 1206 */ 1207 private String handleBackslashs(Object value, boolean inList) 1208 { 1209 String strValue = String.valueOf(value); 1210 1211 if (inList && strValue.indexOf(DOUBLE_ESC) >= 0) 1212 { 1213 char esc = ESCAPE.charAt(0); 1214 StringBuffer buf = new StringBuffer(strValue.length() + BUF_SIZE); 1215 for (int i = 0; i < strValue.length(); i++) 1216 { 1217 if (strValue.charAt(i) == esc && i < strValue.length() - 1 1218 && strValue.charAt(i + 1) == esc) 1219 { 1220 buf.append(DOUBLE_ESC).append(DOUBLE_ESC); 1221 i++; 1222 } 1223 else 1224 { 1225 buf.append(strValue.charAt(i)); 1226 } 1227 } 1228 1229 strValue = buf.toString(); 1230 } 1231 1232 return StringEscapeUtils.escapeJava(strValue); 1233 } 1234 1235 /** 1236 * Transforms a list of values into a single line value. 1237 * 1238 * @param values the list with the values 1239 * @return a string with the single line value (can be <b>null</b>) 1240 * @since 1.3 1241 */ 1242 private String makeSingleLineValue(List values) 1243 { 1244 if (!values.isEmpty()) 1245 { 1246 Iterator it = values.iterator(); 1247 String lastValue = escapeValue(it.next(), true); 1248 StringBuffer buf = new StringBuffer(lastValue); 1249 while (it.hasNext()) 1250 { 1251 // if the last value ended with an escape character, it has 1252 // to be escaped itself; otherwise the list delimiter will 1253 // be escaped 1254 if (lastValue.endsWith(ESCAPE) && (countTrailingBS(lastValue) / 2) % 2 != 0) 1255 { 1256 buf.append(ESCAPE).append(ESCAPE); 1257 } 1258 buf.append(delimiter); 1259 lastValue = escapeValue(it.next(), true); 1260 buf.append(lastValue); 1261 } 1262 return buf.toString(); 1263 } 1264 else 1265 { 1266 return null; 1267 } 1268 } 1269 1270 /** 1271 * Helper method for writing a line with the platform specific line 1272 * ending. 1273 * 1274 * @param s the content of the line (may be <b>null</b>) 1275 * @throws IOException if an error occurs 1276 * @since 1.3 1277 */ 1278 public void writeln(String s) throws IOException 1279 { 1280 if (s != null) 1281 { 1282 write(s); 1283 } 1284 write(getLineSeparator()); 1285 } 1286 1287 /** 1288 * Returns the separator to be used for the given property. This method 1289 * is called by <code>writeProperty()</code>. The string returned here 1290 * is used as separator between the property key and its value. Per 1291 * default the method checks whether a global separator is set. If this 1292 * is the case, it is returned. Otherwise the separator returned by 1293 * <code>getCurrentSeparator()</code> is used, which was set by the 1294 * associated layout object. Derived classes may implement a different 1295 * strategy for defining the separator. 1296 * 1297 * @param key the property key 1298 * @param value the value 1299 * @return the separator to be used 1300 * @since 1.7 1301 */ 1302 protected String fetchSeparator(String key, Object value) 1303 { 1304 return (getGlobalSeparator() != null) ? getGlobalSeparator() 1305 : getCurrentSeparator(); 1306 } 1307 } // class PropertiesWriter 1308 1309 /** 1310 * <p> 1311 * Definition of an interface that allows customization of read and write 1312 * operations. 1313 * </p> 1314 * <p> 1315 * For reading and writing properties files the inner classes 1316 * <code>PropertiesReader</code> and <code>PropertiesWriter</code> are used. 1317 * This interface defines factory methods for creating both a 1318 * <code>PropertiesReader</code> and a <code>PropertiesWriter</code>. An 1319 * object implementing this interface can be passed to the 1320 * <code>setIOFactory()</code> method of 1321 * <code>PropertiesConfiguration</code>. Every time the configuration is 1322 * read or written the <code>IOFactory</code> is asked to create the 1323 * appropriate reader or writer object. This provides an opportunity to 1324 * inject custom reader or writer implementations. 1325 * </p> 1326 * 1327 * @since 1.7 1328 */ 1329 public interface IOFactory 1330 { 1331 /** 1332 * Creates a <code>PropertiesReader</code> for reading a properties 1333 * file. This method is called whenever the 1334 * <code>PropertiesConfiguration</code> is loaded. The reader returned 1335 * by this method is then used for parsing the properties file. 1336 * 1337 * @param in the underlying reader (of the properties file) 1338 * @param delimiter the delimiter character for list parsing 1339 * @return the <code>PropertiesReader</code> for loading the 1340 * configuration 1341 */ 1342 PropertiesReader createPropertiesReader(Reader in, char delimiter); 1343 1344 /** 1345 * Creates a <code>PropertiesWriter</code> for writing a properties 1346 * file. This method is called before the 1347 * <code>PropertiesConfiguration</code> is saved. The writer returned by 1348 * this method is then used for writing the properties file. 1349 * 1350 * @param out the underlying writer (to the properties file) 1351 * @param delimiter the delimiter character for list parsing 1352 * @return the <code>PropertiesWriter</code> for saving the 1353 * configuration 1354 */ 1355 PropertiesWriter createPropertiesWriter(Writer out, char delimiter); 1356 } 1357 1358 /** 1359 * <p> 1360 * A default implementation of the <code>IOFactory</code> interface. 1361 * </p> 1362 * <p> 1363 * This class implements the <code>createXXXX()</code> methods defined by 1364 * the <code>IOFactory</code> interface in a way that the default objects 1365 * (i.e. <code>PropertiesReader</code> and <code>PropertiesWriter</code> are 1366 * returned. Customizing either the reader or the writer (or both) can be 1367 * done by extending this class and overriding the corresponding 1368 * <code>createXXXX()</code> method. 1369 * </p> 1370 * 1371 * @since 1.7 1372 */ 1373 public static class DefaultIOFactory implements IOFactory 1374 { 1375 public PropertiesReader createPropertiesReader(Reader in, char delimiter) 1376 { 1377 return new PropertiesReader(in, delimiter); 1378 } 1379 1380 public PropertiesWriter createPropertiesWriter(Writer out, 1381 char delimiter) 1382 { 1383 return new PropertiesWriter(out, delimiter); 1384 } 1385 } 1386 1387 /** 1388 * <p>Unescapes any Java literals found in the <code>String</code> to a 1389 * <code>Writer</code>.</p> This is a slightly modified version of the 1390 * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't 1391 * drop escaped separators (i.e '\,'). 1392 * 1393 * @param str the <code>String</code> to unescape, may be null 1394 * @param delimiter the delimiter for multi-valued properties 1395 * @return the processed string 1396 * @throws IllegalArgumentException if the Writer is <code>null</code> 1397 */ 1398 protected static String unescapeJava(String str, char delimiter) 1399 { 1400 if (str == null) 1401 { 1402 return null; 1403 } 1404 int sz = str.length(); 1405 StringBuffer out = new StringBuffer(sz); 1406 StringBuffer unicode = new StringBuffer(UNICODE_LEN); 1407 boolean hadSlash = false; 1408 boolean inUnicode = false; 1409 for (int i = 0; i < sz; i++) 1410 { 1411 char ch = str.charAt(i); 1412 if (inUnicode) 1413 { 1414 // if in unicode, then we're reading unicode 1415 // values in somehow 1416 unicode.append(ch); 1417 if (unicode.length() == UNICODE_LEN) 1418 { 1419 // unicode now contains the four hex digits 1420 // which represents our unicode character 1421 try 1422 { 1423 int value = Integer.parseInt(unicode.toString(), HEX_RADIX); 1424 out.append((char) value); 1425 unicode.setLength(0); 1426 inUnicode = false; 1427 hadSlash = false; 1428 } 1429 catch (NumberFormatException nfe) 1430 { 1431 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe); 1432 } 1433 } 1434 continue; 1435 } 1436 1437 if (hadSlash) 1438 { 1439 // handle an escaped value 1440 hadSlash = false; 1441 1442 if (ch == '\\') 1443 { 1444 out.append('\\'); 1445 } 1446 else if (ch == '\'') 1447 { 1448 out.append('\''); 1449 } 1450 else if (ch == '\"') 1451 { 1452 out.append('"'); 1453 } 1454 else if (ch == 'r') 1455 { 1456 out.append('\r'); 1457 } 1458 else if (ch == 'f') 1459 { 1460 out.append('\f'); 1461 } 1462 else if (ch == 't') 1463 { 1464 out.append('\t'); 1465 } 1466 else if (ch == 'n') 1467 { 1468 out.append('\n'); 1469 } 1470 else if (ch == 'b') 1471 { 1472 out.append('\b'); 1473 } 1474 else if (ch == delimiter) 1475 { 1476 out.append('\\'); 1477 out.append(delimiter); 1478 } 1479 else if (ch == 'u') 1480 { 1481 // uh-oh, we're in unicode country.... 1482 inUnicode = true; 1483 } 1484 else 1485 { 1486 out.append(ch); 1487 } 1488 1489 continue; 1490 } 1491 else if (ch == '\\') 1492 { 1493 hadSlash = true; 1494 continue; 1495 } 1496 out.append(ch); 1497 } 1498 1499 if (hadSlash) 1500 { 1501 // then we're in the weird case of a \ at the end of the 1502 // string, let's output it anyway. 1503 out.append('\\'); 1504 } 1505 1506 return out.toString(); 1507 } 1508 1509 /** 1510 * Helper method for loading an included properties file. This method is 1511 * called by <code>load()</code> when an <code>include</code> property 1512 * is encountered. It tries to resolve relative file names based on the 1513 * current base path. If this fails, a resolution based on the location of 1514 * this properties file is tried. 1515 * 1516 * @param fileName the name of the file to load 1517 * @throws ConfigurationException if loading fails 1518 */ 1519 private void loadIncludeFile(String fileName) throws ConfigurationException 1520 { 1521 URL url = ConfigurationUtils.locate(getFileSystem(), getBasePath(), fileName); 1522 if (url == null) 1523 { 1524 URL baseURL = getURL(); 1525 if (baseURL != null) 1526 { 1527 url = ConfigurationUtils.locate(getFileSystem(), baseURL.toString(), fileName); 1528 } 1529 } 1530 1531 if (url == null) 1532 { 1533 throw new ConfigurationException("Cannot resolve include file " 1534 + fileName); 1535 } 1536 load(url); 1537 } 1538 }