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