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 018package org.apache.commons.configuration; 019 020import java.io.File; 021import java.io.FilterWriter; 022import java.io.IOException; 023import java.io.LineNumberReader; 024import java.io.Reader; 025import java.io.Writer; 026import java.net.URL; 027import java.util.ArrayList; 028import java.util.Iterator; 029import java.util.List; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import org.apache.commons.lang.ArrayUtils; 034import org.apache.commons.lang.StringEscapeUtils; 035import org.apache.commons.lang.StringUtils; 036 037/** 038 * This is the "classic" Properties loader which loads the values from 039 * a single or multiple files (which can be chained with "include =". 040 * All given path references are either absolute or relative to the 041 * file name supplied in the constructor. 042 * <p> 043 * In this class, empty PropertyConfigurations can be built, properties 044 * added and later saved. include statements are (obviously) not supported 045 * if you don't construct a PropertyConfiguration from a file. 046 * 047 * <p>The properties file syntax is explained here, basically it follows 048 * the syntax of the stream parsed by {@link java.util.Properties#load} and 049 * adds several useful extensions: 050 * 051 * <ul> 052 * <li> 053 * Each property has the syntax <code>key <separator> value</code>. The 054 * separators accepted are {@code '='}, {@code ':'} and any white 055 * space character. Examples: 056 * <pre> 057 * key1 = value1 058 * key2 : value2 059 * key3 value3</pre> 060 * </li> 061 * <li> 062 * The <i>key</i> may use any character, separators must be escaped: 063 * <pre> 064 * key\:foo = bar</pre> 065 * </li> 066 * <li> 067 * <i>value</i> may be separated on different lines if a backslash 068 * is placed at the end of the line that continues below. 069 * </li> 070 * <li> 071 * <i>value</i> can contain <em>value delimiters</em> and will then be interpreted 072 * as a list of tokens. Default value delimiter is the comma ','. So the 073 * following property definition 074 * <pre> 075 * key = This property, has multiple, values 076 * </pre> 077 * will result in a property with three values. You can change the value 078 * delimiter using the {@link AbstractConfiguration#setListDelimiter(char)} 079 * method. Setting the delimiter to 0 will disable value splitting completely. 080 * </li> 081 * <li> 082 * Commas in each token are escaped placing a backslash right before 083 * the comma. 084 * </li> 085 * <li> 086 * If a <i>key</i> is used more than once, the values are appended 087 * like if they were on the same line separated with commas. <em>Note</em>: 088 * When the configuration file is written back to disk the associated 089 * {@link PropertiesConfigurationLayout} object (see below) will 090 * try to preserve as much of the original format as possible, i.e. properties 091 * with multiple values defined on a single line will also be written back on 092 * a single line, and multiple occurrences of a single key will be written on 093 * multiple lines. If the {@code addProperty()} method was called 094 * multiple times for adding multiple values to a property, these properties 095 * will per default be written on multiple lines in the output file, too. 096 * Some options of the {@code PropertiesConfigurationLayout} class have 097 * influence on that behavior. 098 * </li> 099 * <li> 100 * Blank lines and lines starting with character '#' or '!' are skipped. 101 * </li> 102 * <li> 103 * If a property is named "include" (or whatever is defined by 104 * setInclude() and getInclude() and the value of that property is 105 * the full path to a file on disk, that file will be included into 106 * the configuration. You can also pull in files relative to the parent 107 * configuration file. So if you have something like the following: 108 * 109 * include = additional.properties 110 * 111 * Then "additional.properties" is expected to be in the same 112 * directory as the parent configuration file. 113 * 114 * The properties in the included file are added to the parent configuration, 115 * they do not replace existing properties with the same key. 116 * 117 * </li> 118 * </ul> 119 * 120 * <p>Here is an example of a valid extended properties file: 121 * 122 * <p><pre> 123 * # lines starting with # are comments 124 * 125 * # This is the simplest property 126 * key = value 127 * 128 * # A long property may be separated on multiple lines 129 * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ 130 * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 131 * 132 * # This is a property with many tokens 133 * tokens_on_a_line = first token, second token 134 * 135 * # This sequence generates exactly the same result 136 * tokens_on_multiple_lines = first token 137 * tokens_on_multiple_lines = second token 138 * 139 * # commas may be escaped in tokens 140 * commas.escaped = Hi\, what'up? 141 * 142 * # properties can reference other properties 143 * base.prop = /base 144 * first.prop = ${base.prop}/first 145 * second.prop = ${first.prop}/second 146 * </pre> 147 * 148 * <p>A {@code PropertiesConfiguration} object is associated with an 149 * instance of the {@link PropertiesConfigurationLayout} class, 150 * which is responsible for storing the layout of the parsed properties file 151 * (i.e. empty lines, comments, and such things). The {@code getLayout()} 152 * method can be used to obtain this layout object. With {@code setLayout()} 153 * a new layout object can be set. This should be done before a properties file 154 * was loaded. 155 * <p><em>Note:</em>Configuration objects of this type can be read concurrently 156 * by multiple threads. However if one of these threads modifies the object, 157 * synchronization has to be performed manually. 158 * 159 * @see java.util.Properties#load 160 * 161 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a> 162 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a> 163 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a> 164 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 165 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a> 166 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a> 167 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 168 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a> 169 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 170 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a> 171 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 172 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 173 * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a> 174 * @version $Id: PropertiesConfiguration.java 1534445 2013-10-22 01:19:43Z henning $ 175 */ 176public class PropertiesConfiguration extends AbstractFileConfiguration 177{ 178 /** Constant for the supported comment characters.*/ 179 static final String COMMENT_CHARS = "#!"; 180 181 /** Constant for the default properties separator.*/ 182 static final String DEFAULT_SEPARATOR = " = "; 183 184 /** 185 * Constant for the default {@code IOFactory}. This instance is used 186 * when no specific factory was set. 187 */ 188 private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory(); 189 190 /** 191 * This is the name of the property that can point to other 192 * properties file for including other properties files. 193 */ 194 private static String include = "include"; 195 196 /** The list of possible key/value separators */ 197 private static final char[] SEPARATORS = new char[] {'=', ':'}; 198 199 /** The white space characters used as key/value separators. */ 200 private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; 201 202 /** 203 * The default encoding (ISO-8859-1 as specified by 204 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) 205 */ 206 private static final String DEFAULT_ENCODING = "ISO-8859-1"; 207 208 /** Constant for the platform specific line separator.*/ 209 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 210 211 /** Constant for the escaping character.*/ 212 private static final String ESCAPE = "\\"; 213 214 /** Constant for the escaped escaping character.*/ 215 private static final String DOUBLE_ESC = ESCAPE + ESCAPE; 216 217 /** Constant for the radix of hex numbers.*/ 218 private static final int HEX_RADIX = 16; 219 220 /** Constant for the length of a unicode literal.*/ 221 private static final int UNICODE_LEN = 4; 222 223 /** Stores the layout object.*/ 224 private PropertiesConfigurationLayout layout; 225 226 /** The IOFactory for creating readers and writers.*/ 227 private volatile IOFactory ioFactory; 228 229 /** Allow file inclusion or not */ 230 private boolean includesAllowed = true; 231 232 /** 233 * Creates an empty PropertyConfiguration object which can be 234 * used to synthesize a new Properties file by adding values and 235 * then saving(). 236 */ 237 public PropertiesConfiguration() 238 { 239 layout = createLayout(); 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()} 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. This is <b>true</b> per default. 312 * 313 * @param includesAllowed True if Includes are allowed. 314 */ 315 public void setIncludesAllowed(boolean includesAllowed) 316 { 317 this.includesAllowed = includesAllowed; 318 } 319 320 /** 321 * Reports the status of file inclusion. 322 * 323 * @return True if include files are loaded. 324 * 325 * @see PropertiesConfiguration#isIncludesAllowed() 326 * 327 * @deprecated Only exists to not break backwards compatibility. 328 * Use {@link PropertiesConfiguration#isIncludesAllowed()} instead. 329 */ 330 @Deprecated 331 public boolean getIncludesAllowed() 332 { 333 return isIncludesAllowed(); 334 } 335 336 /** 337 * Reports the status of file inclusion. 338 * 339 * @return True if include files are loaded. 340 */ 341 public boolean isIncludesAllowed() 342 { 343 return this.includesAllowed; 344 } 345 346 /** 347 * Return the comment header. 348 * 349 * @return the comment header 350 * @since 1.1 351 */ 352 public String getHeader() 353 { 354 return getLayout().getHeaderComment(); 355 } 356 357 /** 358 * Set the comment header. 359 * 360 * @param header the header to use 361 * @since 1.1 362 */ 363 public void setHeader(String header) 364 { 365 getLayout().setHeaderComment(header); 366 } 367 368 /** 369 * Returns the footer comment. This is a comment at the very end of the 370 * file. 371 * 372 * @return the footer comment 373 * @since 1.10 374 */ 375 public String getFooter() 376 { 377 return getLayout().getFooterComment(); 378 } 379 380 /** 381 * Sets the footer comment. If set, this comment is written after all 382 * properties at the end of the file. 383 * 384 * @param footer the footer comment 385 * @since 1.10 386 */ 387 public void setFooter(String footer) 388 { 389 getLayout().setFooterComment(footer); 390 } 391 392 /** 393 * Returns the encoding to be used when loading or storing configuration 394 * data. This implementation ensures that the default encoding will be used 395 * if none has been set explicitly. 396 * 397 * @return the encoding 398 */ 399 @Override 400 public String getEncoding() 401 { 402 String enc = super.getEncoding(); 403 return (enc != null) ? enc : DEFAULT_ENCODING; 404 } 405 406 /** 407 * Returns the associated layout object. 408 * 409 * @return the associated layout object 410 * @since 1.3 411 */ 412 public synchronized PropertiesConfigurationLayout getLayout() 413 { 414 if (layout == null) 415 { 416 layout = createLayout(); 417 } 418 return layout; 419 } 420 421 /** 422 * Sets the associated layout object. 423 * 424 * @param layout the new layout object; can be <b>null</b>, then a new 425 * layout object will be created 426 * @since 1.3 427 */ 428 public synchronized void setLayout(PropertiesConfigurationLayout layout) 429 { 430 // only one layout must exist 431 if (this.layout != null) 432 { 433 removeConfigurationListener(this.layout); 434 } 435 436 if (layout == null) 437 { 438 this.layout = createLayout(); 439 } 440 else 441 { 442 this.layout = layout; 443 } 444 } 445 446 /** 447 * Creates the associated layout object. This method is invoked when the 448 * layout object is accessed and has not been created yet. Derived classes 449 * can override this method to hook in a different layout implementation. 450 * 451 * @return the layout object to use 452 * @since 1.3 453 */ 454 protected PropertiesConfigurationLayout createLayout() 455 { 456 return new PropertiesConfigurationLayout(this); 457 } 458 459 /** 460 * Returns the {@code IOFactory} to be used for creating readers and 461 * writers when loading or saving this configuration. 462 * 463 * @return the {@code IOFactory} 464 * @since 1.7 465 */ 466 public IOFactory getIOFactory() 467 { 468 return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY; 469 } 470 471 /** 472 * Sets the {@code IOFactory} to be used for creating readers and 473 * writers when loading or saving this configuration. Using this method a 474 * client can customize the reader and writer classes used by the load and 475 * save operations. Note that this method must be called before invoking 476 * one of the {@code load()} and {@code save()} methods. 477 * Especially, if you want to use a custom {@code IOFactory} for 478 * changing the {@code PropertiesReader}, you cannot load the 479 * configuration data in the constructor. 480 * 481 * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>) 482 * @throws IllegalArgumentException if the {@code IOFactory} is 483 * <b>null</b> 484 * @since 1.7 485 */ 486 public void setIOFactory(IOFactory ioFactory) 487 { 488 if (ioFactory == null) 489 { 490 throw new IllegalArgumentException("IOFactory must not be null!"); 491 } 492 493 this.ioFactory = ioFactory; 494 } 495 496 /** 497 * Load the properties from the given reader. 498 * Note that the {@code clear()} method is not called, so 499 * the properties contained in the loaded file will be added to the 500 * actual set of properties. 501 * 502 * @param in An InputStream. 503 * 504 * @throws ConfigurationException if an error occurs 505 */ 506 public synchronized void load(Reader in) throws ConfigurationException 507 { 508 boolean oldAutoSave = isAutoSave(); 509 setAutoSave(false); 510 511 try 512 { 513 getLayout().load(in); 514 } 515 finally 516 { 517 setAutoSave(oldAutoSave); 518 } 519 } 520 521 /** 522 * Save the configuration to the specified stream. 523 * 524 * @param writer the output stream used to save the configuration 525 * @throws ConfigurationException if an error occurs 526 */ 527 public void save(Writer writer) throws ConfigurationException 528 { 529 enterNoReload(); 530 try 531 { 532 getLayout().save(writer); 533 } 534 finally 535 { 536 exitNoReload(); 537 } 538 } 539 540 /** 541 * Extend the setBasePath method to turn includes 542 * on and off based on the existence of a base path. 543 * 544 * @param basePath The new basePath to set. 545 */ 546 @Override 547 public void setBasePath(String basePath) 548 { 549 super.setBasePath(basePath); 550 setIncludesAllowed(StringUtils.isNotEmpty(basePath)); 551 } 552 553 /** 554 * Creates a copy of this object. 555 * 556 * @return the copy 557 */ 558 @Override 559 public Object clone() 560 { 561 PropertiesConfiguration copy = (PropertiesConfiguration) super.clone(); 562 if (layout != null) 563 { 564 copy.setLayout(new PropertiesConfigurationLayout(copy, layout)); 565 } 566 return copy; 567 } 568 569 /** 570 * This method is invoked by the associated 571 * {@link PropertiesConfigurationLayout} object for each 572 * property definition detected in the parsed properties file. Its task is 573 * to check whether this is a special property definition (e.g. the 574 * {@code include} property). If not, the property must be added to 575 * this configuration. The return value indicates whether the property 576 * should be treated as a normal property. If it is <b>false</b>, the 577 * layout object will ignore this property. 578 * 579 * @param key the property key 580 * @param value the property value 581 * @return a flag whether this is a normal property 582 * @throws ConfigurationException if an error occurs 583 * @since 1.3 584 */ 585 boolean propertyLoaded(String key, String value) 586 throws ConfigurationException 587 { 588 boolean result; 589 590 if (StringUtils.isNotEmpty(getInclude()) 591 && key.equalsIgnoreCase(getInclude())) 592 { 593 if (isIncludesAllowed()) 594 { 595 String[] files; 596 if (!isDelimiterParsingDisabled()) 597 { 598 files = StringUtils.split(value, getListDelimiter()); 599 } 600 else 601 { 602 files = new String[]{value}; 603 } 604 for (String f : files) 605 { 606 loadIncludeFile(interpolate(f.trim())); 607 } 608 } 609 result = false; 610 } 611 612 else 613 { 614 addProperty(key, value); 615 result = true; 616 } 617 618 return result; 619 } 620 621 /** 622 * Tests whether a line is a comment, i.e. whether it starts with a comment 623 * character. 624 * 625 * @param line the line 626 * @return a flag if this is a comment line 627 * @since 1.3 628 */ 629 static boolean isCommentLine(String line) 630 { 631 String s = line.trim(); 632 // blanc lines are also treated as comment lines 633 return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; 634 } 635 636 /** 637 * Returns the number of trailing backslashes. This is sometimes needed for 638 * the correct handling of escape characters. 639 * 640 * @param line the string to investigate 641 * @return the number of trailing backslashes 642 */ 643 private static int countTrailingBS(String line) 644 { 645 int bsCount = 0; 646 for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) 647 { 648 bsCount++; 649 } 650 651 return bsCount; 652 } 653 654 /** 655 * This class is used to read properties lines. These lines do 656 * not terminate with new-line chars but rather when there is no 657 * backslash sign a the end of the line. This is used to 658 * concatenate multiple lines for readability. 659 */ 660 public static class PropertiesReader extends LineNumberReader 661 { 662 /** The regular expression to parse the key and the value of a property. */ 663 private static final Pattern PROPERTY_PATTERN = Pattern 664 .compile("(([\\S&&[^\\\\" + new String(SEPARATORS) 665 + "]]|\\\\.)*)(\\s*(\\s+|[" + new String(SEPARATORS) 666 + "])\\s*)(.*)"); 667 668 /** Constant for the index of the group for the key. */ 669 private static final int IDX_KEY = 1; 670 671 /** Constant for the index of the group for the value. */ 672 private static final int IDX_VALUE = 5; 673 674 /** Constant for the index of the group for the separator. */ 675 private static final int IDX_SEPARATOR = 3; 676 677 /** Stores the comment lines for the currently processed property.*/ 678 private List<String> commentLines; 679 680 /** Stores the name of the last read property.*/ 681 private String propertyName; 682 683 /** Stores the value of the last read property.*/ 684 private String propertyValue; 685 686 /** Stores the property separator of the last read property.*/ 687 private String propertySeparator = DEFAULT_SEPARATOR; 688 689 /** Stores the list delimiter character.*/ 690 private char delimiter; 691 692 /** 693 * Constructor. 694 * 695 * @param reader A Reader. 696 */ 697 public PropertiesReader(Reader reader) 698 { 699 this(reader, AbstractConfiguration.getDefaultListDelimiter()); 700 } 701 702 /** 703 * Creates a new instance of {@code PropertiesReader} and sets 704 * the underlying reader and the list delimiter. 705 * 706 * @param reader the reader 707 * @param listDelimiter the list delimiter character 708 * @since 1.3 709 */ 710 public PropertiesReader(Reader reader, char listDelimiter) 711 { 712 super(reader); 713 commentLines = new ArrayList<String>(); 714 delimiter = listDelimiter; 715 } 716 717 /** 718 * Reads a property line. Returns null if Stream is 719 * at EOF. Concatenates lines ending with "\". 720 * Skips lines beginning with "#" or "!" and empty lines. 721 * The return value is a property definition (<code><name></code> 722 * = <code><value></code>) 723 * 724 * @return A string containing a property value or null 725 * 726 * @throws IOException in case of an I/O error 727 */ 728 public String readProperty() throws IOException 729 { 730 commentLines.clear(); 731 StringBuilder buffer = new StringBuilder(); 732 733 while (true) 734 { 735 String line = readLine(); 736 if (line == null) 737 { 738 // EOF 739 return null; 740 } 741 742 if (isCommentLine(line)) 743 { 744 commentLines.add(line); 745 continue; 746 } 747 748 line = line.trim(); 749 750 if (checkCombineLines(line)) 751 { 752 line = line.substring(0, line.length() - 1); 753 buffer.append(line); 754 } 755 else 756 { 757 buffer.append(line); 758 break; 759 } 760 } 761 return buffer.toString(); 762 } 763 764 /** 765 * Parses the next property from the input stream and stores the found 766 * name and value in internal fields. These fields can be obtained using 767 * the provided getter methods. The return value indicates whether EOF 768 * was reached (<b>false</b>) or whether further properties are 769 * available (<b>true</b>). 770 * 771 * @return a flag if further properties are available 772 * @throws IOException if an error occurs 773 * @since 1.3 774 */ 775 public boolean nextProperty() throws IOException 776 { 777 String line = readProperty(); 778 779 if (line == null) 780 { 781 return false; // EOF 782 } 783 784 // parse the line 785 parseProperty(line); 786 return true; 787 } 788 789 /** 790 * Returns the comment lines that have been read for the last property. 791 * 792 * @return the comment lines for the last property returned by 793 * {@code readProperty()} 794 * @since 1.3 795 */ 796 public List<String> getCommentLines() 797 { 798 return commentLines; 799 } 800 801 /** 802 * Returns the name of the last read property. This method can be called 803 * after {@link #nextProperty()} was invoked and its 804 * return value was <b>true</b>. 805 * 806 * @return the name of the last read property 807 * @since 1.3 808 */ 809 public String getPropertyName() 810 { 811 return propertyName; 812 } 813 814 /** 815 * Returns the value of the last read property. This method can be 816 * called after {@link #nextProperty()} was invoked and 817 * its return value was <b>true</b>. 818 * 819 * @return the value of the last read property 820 * @since 1.3 821 */ 822 public String getPropertyValue() 823 { 824 return propertyValue; 825 } 826 827 /** 828 * Returns the separator that was used for the last read property. The 829 * separator can be stored so that it can later be restored when saving 830 * the configuration. 831 * 832 * @return the separator for the last read property 833 * @since 1.7 834 */ 835 public String getPropertySeparator() 836 { 837 return propertySeparator; 838 } 839 840 /** 841 * Parses a line read from the properties file. This method is called 842 * for each non-comment line read from the source file. Its task is to 843 * split the passed in line into the property key and its value. The 844 * results of the parse operation can be stored by calling the 845 * {@code initPropertyXXX()} methods. 846 * 847 * @param line the line read from the properties file 848 * @since 1.7 849 */ 850 protected void parseProperty(String line) 851 { 852 String[] property = doParseProperty(line); 853 initPropertyName(property[0]); 854 initPropertyValue(property[1]); 855 initPropertySeparator(property[2]); 856 } 857 858 /** 859 * Sets the name of the current property. This method can be called by 860 * {@code parseProperty()} for storing the results of the parse 861 * operation. It also ensures that the property key is correctly 862 * escaped. 863 * 864 * @param name the name of the current property 865 * @since 1.7 866 */ 867 protected void initPropertyName(String name) 868 { 869 propertyName = StringEscapeUtils.unescapeJava(name); 870 } 871 872 /** 873 * Sets the value of the current property. This method can be called by 874 * {@code parseProperty()} for storing the results of the parse 875 * operation. It also ensures that the property value is correctly 876 * escaped. 877 * 878 * @param value the value of the current property 879 * @since 1.7 880 */ 881 protected void initPropertyValue(String value) 882 { 883 propertyValue = unescapeJava(value, delimiter); 884 } 885 886 /** 887 * Sets the separator of the current property. This method can be called 888 * by {@code parseProperty()}. It allows the associated layout 889 * object to keep track of the property separators. When saving the 890 * configuration the separators can be restored. 891 * 892 * @param value the separator used for the current property 893 * @since 1.7 894 */ 895 protected void initPropertySeparator(String value) 896 { 897 propertySeparator = value; 898 } 899 900 /** 901 * Checks if the passed in line should be combined with the following. 902 * This is true, if the line ends with an odd number of backslashes. 903 * 904 * @param line the line 905 * @return a flag if the lines should be combined 906 */ 907 private static boolean checkCombineLines(String line) 908 { 909 return countTrailingBS(line) % 2 != 0; 910 } 911 912 /** 913 * Parse a property line and return the key, the value, and the separator in an array. 914 * 915 * @param line the line to parse 916 * @return an array with the property's key, value, and separator 917 */ 918 private static String[] doParseProperty(String line) 919 { 920 Matcher matcher = PROPERTY_PATTERN.matcher(line); 921 922 String[] result = {"", "", ""}; 923 924 if (matcher.matches()) 925 { 926 result[0] = matcher.group(IDX_KEY).trim(); 927 result[1] = matcher.group(IDX_VALUE).trim(); 928 result[2] = matcher.group(IDX_SEPARATOR); 929 } 930 931 return result; 932 } 933 } // class PropertiesReader 934 935 /** 936 * This class is used to write properties lines. The most important method 937 * is {@code writeProperty(String, Object, boolean)}, which is called 938 * during a save operation for each property found in the configuration. 939 */ 940 public static class PropertiesWriter extends FilterWriter 941 { 942 /** Constant for the initial size when creating a string buffer. */ 943 private static final int BUF_SIZE = 8; 944 945 /** The delimiter for multi-valued properties.*/ 946 private char delimiter; 947 948 /** The separator to be used for the current property. */ 949 private String currentSeparator; 950 951 /** The global separator. If set, it overrides the current separator.*/ 952 private String globalSeparator; 953 954 /** The line separator.*/ 955 private String lineSeparator; 956 957 /** 958 * Constructor. 959 * 960 * @param writer a Writer object providing the underlying stream 961 * @param delimiter the delimiter character for multi-valued properties 962 */ 963 public PropertiesWriter(Writer writer, char delimiter) 964 { 965 super(writer); 966 this.delimiter = delimiter; 967 } 968 969 /** 970 * Returns the current property separator. 971 * 972 * @return the current property separator 973 * @since 1.7 974 */ 975 public String getCurrentSeparator() 976 { 977 return currentSeparator; 978 } 979 980 /** 981 * Sets the current property separator. This separator is used when 982 * writing the next property. 983 * 984 * @param currentSeparator the current property separator 985 * @since 1.7 986 */ 987 public void setCurrentSeparator(String currentSeparator) 988 { 989 this.currentSeparator = currentSeparator; 990 } 991 992 /** 993 * Returns the global property separator. 994 * 995 * @return the global property separator 996 * @since 1.7 997 */ 998 public String getGlobalSeparator() 999 { 1000 return globalSeparator; 1001 } 1002 1003 /** 1004 * Sets the global property separator. This separator corresponds to the 1005 * {@code globalSeparator} property of 1006 * {@link PropertiesConfigurationLayout}. It defines the separator to be 1007 * used for all properties. If it is undefined, the current separator is 1008 * used. 1009 * 1010 * @param globalSeparator the global property separator 1011 * @since 1.7 1012 */ 1013 public void setGlobalSeparator(String globalSeparator) 1014 { 1015 this.globalSeparator = globalSeparator; 1016 } 1017 1018 /** 1019 * Returns the line separator. 1020 * 1021 * @return the line separator 1022 * @since 1.7 1023 */ 1024 public String getLineSeparator() 1025 { 1026 return (lineSeparator != null) ? lineSeparator : LINE_SEPARATOR; 1027 } 1028 1029 /** 1030 * Sets the line separator. Each line written by this writer is 1031 * terminated with this separator. If not set, the platform-specific 1032 * line separator is used. 1033 * 1034 * @param lineSeparator the line separator to be used 1035 * @since 1.7 1036 */ 1037 public void setLineSeparator(String lineSeparator) 1038 { 1039 this.lineSeparator = lineSeparator; 1040 } 1041 1042 /** 1043 * Write a property. 1044 * 1045 * @param key the key of the property 1046 * @param value the value of the property 1047 * 1048 * @throws IOException if an I/O error occurs 1049 */ 1050 public void writeProperty(String key, Object value) throws IOException 1051 { 1052 writeProperty(key, value, false); 1053 } 1054 1055 /** 1056 * Write a property. 1057 * 1058 * @param key The key of the property 1059 * @param values The array of values of the property 1060 * 1061 * @throws IOException if an I/O error occurs 1062 */ 1063 public void writeProperty(String key, List<?> values) throws IOException 1064 { 1065 for (int i = 0; i < values.size(); i++) 1066 { 1067 writeProperty(key, values.get(i)); 1068 } 1069 } 1070 1071 /** 1072 * Writes the given property and its value. If the value happens to be a 1073 * list, the {@code forceSingleLine} flag is evaluated. If it is 1074 * set, all values are written on a single line using the list delimiter 1075 * as separator. 1076 * 1077 * @param key the property key 1078 * @param value the property value 1079 * @param forceSingleLine the "force single line" flag 1080 * @throws IOException if an error occurs 1081 * @since 1.3 1082 */ 1083 public void writeProperty(String key, Object value, 1084 boolean forceSingleLine) throws IOException 1085 { 1086 String v; 1087 1088 if (value instanceof List) 1089 { 1090 List<?> values = (List<?>) value; 1091 if (forceSingleLine) 1092 { 1093 v = makeSingleLineValue(values); 1094 } 1095 else 1096 { 1097 writeProperty(key, values); 1098 return; 1099 } 1100 } 1101 else 1102 { 1103 v = escapeValue(value, false); 1104 } 1105 1106 write(escapeKey(key)); 1107 write(fetchSeparator(key, value)); 1108 write(v); 1109 1110 writeln(null); 1111 } 1112 1113 /** 1114 * Write a comment. 1115 * 1116 * @param comment the comment to write 1117 * @throws IOException if an I/O error occurs 1118 */ 1119 public void writeComment(String comment) throws IOException 1120 { 1121 writeln("# " + comment); 1122 } 1123 1124 /** 1125 * Escape the separators in the key. 1126 * 1127 * @param key the key 1128 * @return the escaped key 1129 * @since 1.2 1130 */ 1131 private String escapeKey(String key) 1132 { 1133 StringBuilder newkey = new StringBuilder(); 1134 1135 for (int i = 0; i < key.length(); i++) 1136 { 1137 char c = key.charAt(i); 1138 1139 if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) 1140 { 1141 // escape the separator 1142 newkey.append('\\'); 1143 newkey.append(c); 1144 } 1145 else 1146 { 1147 newkey.append(c); 1148 } 1149 } 1150 1151 return newkey.toString(); 1152 } 1153 1154 /** 1155 * Escapes the given property value. Delimiter characters in the value 1156 * will be escaped. 1157 * 1158 * @param value the property value 1159 * @param inList a flag whether the value is part of a list 1160 * @return the escaped property value 1161 * @since 1.3 1162 */ 1163 private String escapeValue(Object value, boolean inList) 1164 { 1165 String escapedValue = handleBackslashs(value, inList); 1166 if (delimiter != 0) 1167 { 1168 escapedValue = StringUtils.replace(escapedValue, String.valueOf(delimiter), ESCAPE + delimiter); 1169 } 1170 return escapedValue; 1171 } 1172 1173 /** 1174 * Performs the escaping of backslashes in the specified properties 1175 * value. Because a double backslash is used to escape the escape 1176 * character of a list delimiter, double backslashes also have to be 1177 * escaped if the property is part of a (single line) list. Then, in all 1178 * cases each backslash has to be doubled in order to produce a valid 1179 * properties file. 1180 * 1181 * @param value the value to be escaped 1182 * @param inList a flag whether the value is part of a list 1183 * @return the value with escaped backslashes as string 1184 */ 1185 private String handleBackslashs(Object value, boolean inList) 1186 { 1187 String strValue = String.valueOf(value); 1188 1189 if (inList && strValue.indexOf(DOUBLE_ESC) >= 0) 1190 { 1191 char esc = ESCAPE.charAt(0); 1192 StringBuilder buf = new StringBuilder(strValue.length() + BUF_SIZE); 1193 for (int i = 0; i < strValue.length(); i++) 1194 { 1195 if (strValue.charAt(i) == esc && i < strValue.length() - 1 1196 && strValue.charAt(i + 1) == esc) 1197 { 1198 buf.append(DOUBLE_ESC).append(DOUBLE_ESC); 1199 i++; 1200 } 1201 else 1202 { 1203 buf.append(strValue.charAt(i)); 1204 } 1205 } 1206 1207 strValue = buf.toString(); 1208 } 1209 1210 return StringEscapeUtils.escapeJava(strValue); 1211 } 1212 1213 /** 1214 * Transforms a list of values into a single line value. 1215 * 1216 * @param values the list with the values 1217 * @return a string with the single line value (can be <b>null</b>) 1218 * @since 1.3 1219 */ 1220 private String makeSingleLineValue(List<?> values) 1221 { 1222 if (!values.isEmpty()) 1223 { 1224 Iterator<?> it = values.iterator(); 1225 String lastValue = escapeValue(it.next(), true); 1226 StringBuilder buf = new StringBuilder(lastValue); 1227 while (it.hasNext()) 1228 { 1229 // if the last value ended with an escape character, it has 1230 // to be escaped itself; otherwise the list delimiter will 1231 // be escaped 1232 if (lastValue.endsWith(ESCAPE) && (countTrailingBS(lastValue) / 2) % 2 != 0) 1233 { 1234 buf.append(ESCAPE).append(ESCAPE); 1235 } 1236 buf.append(delimiter); 1237 lastValue = escapeValue(it.next(), true); 1238 buf.append(lastValue); 1239 } 1240 return buf.toString(); 1241 } 1242 else 1243 { 1244 return null; 1245 } 1246 } 1247 1248 /** 1249 * Helper method for writing a line with the platform specific line 1250 * ending. 1251 * 1252 * @param s the content of the line (may be <b>null</b>) 1253 * @throws IOException if an error occurs 1254 * @since 1.3 1255 */ 1256 public void writeln(String s) throws IOException 1257 { 1258 if (s != null) 1259 { 1260 write(s); 1261 } 1262 write(getLineSeparator()); 1263 } 1264 1265 /** 1266 * Returns the separator to be used for the given property. This method 1267 * is called by {@code writeProperty()}. The string returned here 1268 * is used as separator between the property key and its value. Per 1269 * default the method checks whether a global separator is set. If this 1270 * is the case, it is returned. Otherwise the separator returned by 1271 * {@code getCurrentSeparator()} is used, which was set by the 1272 * associated layout object. Derived classes may implement a different 1273 * strategy for defining the separator. 1274 * 1275 * @param key the property key 1276 * @param value the value 1277 * @return the separator to be used 1278 * @since 1.7 1279 */ 1280 protected String fetchSeparator(String key, Object value) 1281 { 1282 return (getGlobalSeparator() != null) ? getGlobalSeparator() 1283 : getCurrentSeparator(); 1284 } 1285 } // class PropertiesWriter 1286 1287 /** 1288 * <p> 1289 * Definition of an interface that allows customization of read and write 1290 * operations. 1291 * </p> 1292 * <p> 1293 * For reading and writing properties files the inner classes 1294 * {@code PropertiesReader} and {@code PropertiesWriter} are used. 1295 * This interface defines factory methods for creating both a 1296 * {@code PropertiesReader} and a {@code PropertiesWriter}. An 1297 * object implementing this interface can be passed to the 1298 * {@code setIOFactory()} method of 1299 * {@code PropertiesConfiguration}. Every time the configuration is 1300 * read or written the {@code IOFactory} is asked to create the 1301 * appropriate reader or writer object. This provides an opportunity to 1302 * inject custom reader or writer implementations. 1303 * </p> 1304 * 1305 * @since 1.7 1306 */ 1307 public interface IOFactory 1308 { 1309 /** 1310 * Creates a {@code PropertiesReader} for reading a properties 1311 * file. This method is called whenever the 1312 * {@code PropertiesConfiguration} is loaded. The reader returned 1313 * by this method is then used for parsing the properties file. 1314 * 1315 * @param in the underlying reader (of the properties file) 1316 * @param delimiter the delimiter character for list parsing 1317 * @return the {@code PropertiesReader} for loading the 1318 * configuration 1319 */ 1320 PropertiesReader createPropertiesReader(Reader in, char delimiter); 1321 1322 /** 1323 * Creates a {@code PropertiesWriter} for writing a properties 1324 * file. This method is called before the 1325 * {@code PropertiesConfiguration} is saved. The writer returned by 1326 * this method is then used for writing the properties file. 1327 * 1328 * @param out the underlying writer (to the properties file) 1329 * @param delimiter the delimiter character for list parsing 1330 * @return the {@code PropertiesWriter} for saving the 1331 * configuration 1332 */ 1333 PropertiesWriter createPropertiesWriter(Writer out, char delimiter); 1334 } 1335 1336 /** 1337 * <p> 1338 * A default implementation of the {@code IOFactory} interface. 1339 * </p> 1340 * <p> 1341 * This class implements the {@code createXXXX()} methods defined by 1342 * the {@code IOFactory} interface in a way that the default objects 1343 * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are 1344 * returned. Customizing either the reader or the writer (or both) can be 1345 * done by extending this class and overriding the corresponding 1346 * {@code createXXXX()} method. 1347 * </p> 1348 * 1349 * @since 1.7 1350 */ 1351 public static class DefaultIOFactory implements IOFactory 1352 { 1353 public PropertiesReader createPropertiesReader(Reader in, char delimiter) 1354 { 1355 return new PropertiesReader(in, delimiter); 1356 } 1357 1358 public PropertiesWriter createPropertiesWriter(Writer out, 1359 char delimiter) 1360 { 1361 return new PropertiesWriter(out, delimiter); 1362 } 1363 } 1364 1365 /** 1366 * <p>Unescapes any Java literals found in the {@code String} to a 1367 * {@code Writer}.</p> This is a slightly modified version of the 1368 * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't 1369 * drop escaped separators (i.e '\,'). 1370 * 1371 * @param str the {@code String} to unescape, may be null 1372 * @param delimiter the delimiter for multi-valued properties 1373 * @return the processed string 1374 * @throws IllegalArgumentException if the Writer is {@code null} 1375 */ 1376 protected static String unescapeJava(String str, char delimiter) 1377 { 1378 if (str == null) 1379 { 1380 return null; 1381 } 1382 int sz = str.length(); 1383 StringBuilder out = new StringBuilder(sz); 1384 StringBuilder unicode = new StringBuilder(UNICODE_LEN); 1385 boolean hadSlash = false; 1386 boolean inUnicode = false; 1387 for (int i = 0; i < sz; i++) 1388 { 1389 char ch = str.charAt(i); 1390 if (inUnicode) 1391 { 1392 // if in unicode, then we're reading unicode 1393 // values in somehow 1394 unicode.append(ch); 1395 if (unicode.length() == UNICODE_LEN) 1396 { 1397 // unicode now contains the four hex digits 1398 // which represents our unicode character 1399 try 1400 { 1401 int value = Integer.parseInt(unicode.toString(), HEX_RADIX); 1402 out.append((char) value); 1403 unicode.setLength(0); 1404 inUnicode = false; 1405 hadSlash = false; 1406 } 1407 catch (NumberFormatException nfe) 1408 { 1409 throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe); 1410 } 1411 } 1412 continue; 1413 } 1414 1415 if (hadSlash) 1416 { 1417 // handle an escaped value 1418 hadSlash = false; 1419 1420 if (ch == '\\') 1421 { 1422 out.append('\\'); 1423 } 1424 else if (ch == '\'') 1425 { 1426 out.append('\''); 1427 } 1428 else if (ch == '\"') 1429 { 1430 out.append('"'); 1431 } 1432 else if (ch == 'r') 1433 { 1434 out.append('\r'); 1435 } 1436 else if (ch == 'f') 1437 { 1438 out.append('\f'); 1439 } 1440 else if (ch == 't') 1441 { 1442 out.append('\t'); 1443 } 1444 else if (ch == 'n') 1445 { 1446 out.append('\n'); 1447 } 1448 else if (ch == 'b') 1449 { 1450 out.append('\b'); 1451 } 1452 else if (ch == delimiter) 1453 { 1454 out.append('\\'); 1455 out.append(delimiter); 1456 } 1457 else if (ch == 'u') 1458 { 1459 // uh-oh, we're in unicode country.... 1460 inUnicode = true; 1461 } 1462 else 1463 { 1464 out.append(ch); 1465 } 1466 1467 continue; 1468 } 1469 else if (ch == '\\') 1470 { 1471 hadSlash = true; 1472 continue; 1473 } 1474 out.append(ch); 1475 } 1476 1477 if (hadSlash) 1478 { 1479 // then we're in the weird case of a \ at the end of the 1480 // string, let's output it anyway. 1481 out.append('\\'); 1482 } 1483 1484 return out.toString(); 1485 } 1486 1487 /** 1488 * Helper method for loading an included properties file. This method is 1489 * called by {@code load()} when an {@code include} property 1490 * is encountered. It tries to resolve relative file names based on the 1491 * current base path. If this fails, a resolution based on the location of 1492 * this properties file is tried. 1493 * 1494 * @param fileName the name of the file to load 1495 * @throws ConfigurationException if loading fails 1496 */ 1497 private void loadIncludeFile(String fileName) throws ConfigurationException 1498 { 1499 URL url = ConfigurationUtils.locate(getFileSystem(), getBasePath(), fileName); 1500 if (url == null) 1501 { 1502 URL baseURL = getURL(); 1503 if (baseURL != null) 1504 { 1505 url = ConfigurationUtils.locate(getFileSystem(), baseURL.toString(), fileName); 1506 } 1507 } 1508 1509 if (url == null) 1510 { 1511 throw new ConfigurationException("Cannot resolve include file " 1512 + fileName); 1513 } 1514 load(url); 1515 } 1516}