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