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