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