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