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