This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-configuration.git
commit bc15cb565eeaf2825b3ae9f7235a5680dc3f0f47 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Fri Sep 18 16:58:51 2020 -0400 Sort members. --- .../configuration2/PropertiesConfiguration.java | 2200 ++++++++++---------- 1 file changed, 1100 insertions(+), 1100 deletions(-) diff --git a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java index e3d6967..c022ab8 100644 --- a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java +++ b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java @@ -206,518 +206,333 @@ public class PropertiesConfiguration extends BaseConfiguration { /** - * Defines default error handling for the special {@code "include"} key by throwing the given exception. - * - * @since 2.6 - */ - public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> { throw e; }; - - /** - * Defines error handling as a noop for the special {@code "include"} key. - * - * @since 2.6 - */ - public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ }; - - /** - * The default encoding (ISO-8859-1 as specified by - * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) - */ - public static final String DEFAULT_ENCODING = "ISO-8859-1"; - - /** Constant for the supported comment characters.*/ - static final String COMMENT_CHARS = "#!"; - - /** Constant for the default properties separator.*/ - static final String DEFAULT_SEPARATOR = " = "; - - /** - * A string with special characters that need to be unescaped when reading - * a properties file. {@code java.util.Properties} escapes these characters - * when writing out a properties file. - */ - private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\""; - - /** - * This is the name of the property that can point to other - * properties file for including other properties files. - */ - private static String include = "include"; - - /** - * This is the name of the property that can point to other - * properties file for including other properties files. - * <p> - * If the file is absent, processing continues normally. - * </p> - */ - private static String includeOptional = "includeoptional"; - - /** The list of possible key/value separators */ - private static final char[] SEPARATORS = new char[] {'=', ':'}; - - /** The white space characters used as key/value separators. */ - private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; - - /** Constant for the platform specific line separator.*/ - private static final String LINE_SEPARATOR = System.getProperty("line.separator"); - - /** Constant for the radix of hex numbers.*/ - private static final int HEX_RADIX = 16; - - /** Constant for the length of a unicode literal.*/ - private static final int UNICODE_LEN = 4; - - /** Stores the layout object.*/ - private PropertiesConfigurationLayout layout; - - /** The include listener for the special {@code "include"} key. */ - private ConfigurationConsumer<ConfigurationException> includeListener; - - /** The IOFactory for creating readers and writers.*/ - private IOFactory ioFactory; - - /** The current {@code FileLocator}. */ - private FileLocator locator; - - /** Allow file inclusion or not */ - private boolean includesAllowed = true; - - /** - * Creates an empty PropertyConfiguration object which can be - * used to synthesize a new Properties file by adding values and - * then saving(). - */ - public PropertiesConfiguration() - { - installLayout(createLayout()); - } - - /** - * Gets the property value for including other properties files. - * By default it is "include". - * - * @return A String. - */ - public static String getInclude() - { - return PropertiesConfiguration.include; - } - - /** - * Gets the property value for including other properties files. - * By default it is "includeoptional". * <p> - * If the file is absent, processing continues normally. + * A default implementation of the {@code IOFactory} interface. * </p> - * - * @return A String. - * @since 2.5 - */ - public static String getIncludeOptional() - { - return PropertiesConfiguration.includeOptional; - } - - /** - * Sets the property value for including other properties files. - * By default it is "include". - * - * @param inc A String. - */ - public static void setInclude(final String inc) - { - PropertiesConfiguration.include = inc; - } - - /** - * Sets the property value for including other properties files. - * By default it is "include". * <p> - * If the file is absent, processing continues normally. + * This class implements the {@code createXXXX()} methods defined by + * the {@code IOFactory} interface in a way that the default objects + * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are + * returned. Customizing either the reader or the writer (or both) can be + * done by extending this class and overriding the corresponding + * {@code createXXXX()} method. * </p> * - * @param inc A String. - * @since 2.5 - */ - public static void setIncludeOptional(final String inc) - { - PropertiesConfiguration.includeOptional = inc; - } - - /** - * Controls whether additional files can be loaded by the {@code include = <xxx>} - * statement or not. This is <b>true</b> per default. - * - * @param includesAllowed True if Includes are allowed. - */ - public void setIncludesAllowed(final boolean includesAllowed) - { - this.includesAllowed = includesAllowed; - } - - /** - * Reports the status of file inclusion. - * - * @return True if include files are loaded. + * @since 1.7 */ - public boolean isIncludesAllowed() + public static class DefaultIOFactory implements IOFactory { - return this.includesAllowed; - } + /** + * The singleton instance. + */ + static final DefaultIOFactory INSTANCE = new DefaultIOFactory(); - /** - * Return the comment header. - * - * @return the comment header - * @since 1.1 - */ - public String getHeader() - { - beginRead(false); - try - { - return getLayout().getHeaderComment(); - } - finally + @Override + public PropertiesReader createPropertiesReader(final Reader in) { - endRead(); + return new PropertiesReader(in); } - } - /** - * Set the comment header. - * - * @param header the header to use - * @since 1.1 - */ - public void setHeader(final String header) - { - beginWrite(false); - try - { - getLayout().setHeaderComment(header); - } - finally + @Override + public PropertiesWriter createPropertiesWriter(final Writer out, + final ListDelimiterHandler handler) { - endWrite(); + return new PropertiesWriter(out, handler); } } /** - * Returns the footer comment. This is a comment at the very end of the - * file. + * <p> + * Definition of an interface that allows customization of read and write + * operations. + * </p> + * <p> + * For reading and writing properties files the inner classes + * {@code PropertiesReader} and {@code PropertiesWriter} are used. + * This interface defines factory methods for creating both a + * {@code PropertiesReader} and a {@code PropertiesWriter}. An + * object implementing this interface can be passed to the + * {@code setIOFactory()} method of + * {@code PropertiesConfiguration}. Every time the configuration is + * read or written the {@code IOFactory} is asked to create the + * appropriate reader or writer object. This provides an opportunity to + * inject custom reader or writer implementations. + * </p> * - * @return the footer comment - * @since 2.0 + * @since 1.7 */ - public String getFooter() + public interface IOFactory { - beginRead(false); - try - { - return getLayout().getFooterComment(); - } - finally - { - endRead(); - } - } + /** + * Creates a {@code PropertiesReader} for reading a properties + * file. This method is called whenever the + * {@code PropertiesConfiguration} is loaded. The reader returned + * by this method is then used for parsing the properties file. + * + * @param in the underlying reader (of the properties file) + * @return the {@code PropertiesReader} for loading the + * configuration + */ + PropertiesReader createPropertiesReader(Reader in); - /** - * Sets the footer comment. If set, this comment is written after all - * properties at the end of the file. - * - * @param footer the footer comment - * @since 2.0 - */ - public void setFooter(final String footer) - { - beginWrite(false); - try - { - getLayout().setFooterComment(footer); - } - finally - { - endWrite(); - } + /** + * Creates a {@code PropertiesWriter} for writing a properties + * file. This method is called before the + * {@code PropertiesConfiguration} is saved. The writer returned by + * this method is then used for writing the properties file. + * + * @param out the underlying writer (to the properties file) + * @param handler the list delimiter delimiter for list parsing + * @return the {@code PropertiesWriter} for saving the + * configuration + */ + PropertiesWriter createPropertiesWriter(Writer out, + ListDelimiterHandler handler); } /** - * Returns the associated layout object. + * An alternative {@link IOFactory} that tries to mimic the behavior of + * {@link java.util.Properties} (Jup) more closely. The goal is to allow both of + * them be used interchangeably when reading and writing properties files + * without losing or changing information. + * <p> + * It also has the option to <em>not</em> use Unicode escapes. When using UTF-8 + * encoding (which is e.g. the new default for resource bundle properties files + * since Java 9), Unicode escapes are no longer required and avoiding them makes + * properties files more readable with regular text editors. + * <p> + * Some of the ways this implementation differs from {@link DefaultIOFactory}: + * <ul> + * <li>Trailing whitespace will not be trimmed from each line.</li> + * <li>Unknown escape sequences will have their backslash removed.</li> + * <li>{@code \b} is not a recognized escape sequence.</li> + * <li>Leading spaces in property values are preserved by escaping them.</li> + * <li>All natural lines (i.e. in the file) of a logical property line will have + * their leading whitespace trimmed.</li> + * <li>Natural lines that look like comment lines within a logical line are not + * treated as such; they're part of the property value.</li> + * </ul> * - * @return the associated layout object - * @since 1.3 + * @since 2.4 */ - public PropertiesConfigurationLayout getLayout() + public static class JupIOFactory implements IOFactory { - return layout; - } - /** - * Sets the associated layout object. - * - * @param layout the new layout object; can be <b>null</b>, then a new - * layout object will be created - * @since 1.3 - */ - public void setLayout(final PropertiesConfigurationLayout layout) - { - installLayout(layout); - } + /** + * Whether characters less than {@code \u0020} and characters greater than + * {@code \u007E} in property keys or values should be escaped using + * Unicode escape sequences. Not necessary when e.g. writing as UTF-8. + */ + private final boolean escapeUnicode; - /** - * Installs a layout object. It has to be ensured that the layout is - * registered as change listener at this configuration. If there is already - * a layout object installed, it has to be removed properly. - * - * @param layout the layout object to be installed - */ - private void installLayout(final PropertiesConfigurationLayout layout) - { - // only one layout must exist - if (this.layout != null) + /** + * Constructs a new {@link JupIOFactory} with Unicode escaping. + */ + public JupIOFactory() { - removeEventListener(ConfigurationEvent.ANY, this.layout); + this(true); } - if (layout == null) - { - this.layout = createLayout(); - } - else + /** + * Constructs a new {@link JupIOFactory} with optional Unicode escaping. Whether + * Unicode escaping is required depends on 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 necessary. Unfortunately this factory can't determine the encoding on its + * own. + * + * @param escapeUnicode whether Unicode characters should be escaped + */ + public JupIOFactory(final boolean escapeUnicode) { - this.layout = layout; + this.escapeUnicode = escapeUnicode; } - addEventListener(ConfigurationEvent.ANY, this.layout); - } - - /** - * Creates a standard layout object. This configuration is initialized with - * such a standard layout. - * - * @return the newly created layout object - */ - private PropertiesConfigurationLayout createLayout() - { - return new PropertiesConfigurationLayout(); - } - /** - * Gets the current include listener, never null. - * - * @return the current include listener, never null. - * @since 2.6 - */ - public ConfigurationConsumer<ConfigurationException> getIncludeListener() - { - return includeListener != null ? includeListener : PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER; - } - - /** - * Returns the {@code IOFactory} to be used for creating readers and - * writers when loading or saving this configuration. - * - * @return the {@code IOFactory} - * @since 1.7 - */ - public IOFactory getIOFactory() - { - return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE; - } - - /** - * Sets the current include listener, may not be null. - * - * @param includeListener the current include listener, may not be null. - * @throws IllegalArgumentException if the {@code includeListener} is null. - * @since 2.6 - */ - public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener) - { - if (includeListener == null) + @Override + public PropertiesReader createPropertiesReader(final Reader in) { - throw new IllegalArgumentException("includeListener must not be null."); + return new JupPropertiesReader(in); } - this.includeListener = includeListener; - } - /** - * Sets the {@code IOFactory} to be used for creating readers and - * writers when loading or saving this configuration. Using this method a - * client can customize the reader and writer classes used by the load and - * save operations. Note that this method must be called before invoking - * one of the {@code load()} and {@code save()} methods. - * Especially, if you want to use a custom {@code IOFactory} for - * changing the {@code PropertiesReader}, you cannot load the - * configuration data in the constructor. - * - * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>) - * @throws IllegalArgumentException if the {@code IOFactory} is - * <b>null</b> - * @since 1.7 - */ - public void setIOFactory(final IOFactory ioFactory) - { - if (ioFactory == null) + @Override + public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) { - throw new IllegalArgumentException("IOFactory must not be null."); + return new JupPropertiesWriter(out, handler, escapeUnicode); } - this.ioFactory = ioFactory; } /** - * Stores the current {@code FileLocator} for a following IO operation. The - * {@code FileLocator} is needed to resolve include files with relative file - * names. + * A {@link PropertiesReader} that tries to mimic the behavior of + * {@link java.util.Properties}. * - * @param locator the current {@code FileLocator} - * @since 2.0 + * @since 2.4 */ - @Override - public void initFileLocator(final FileLocator locator) + public static class JupPropertiesReader extends PropertiesReader { - this.locator = locator; - } - /** - * {@inheritDoc} This implementation delegates to the associated layout - * object which does the actual loading. Note that this method does not - * do any synchronization. This lies in the responsibility of the caller. - * (Typically, the caller is a {@code FileHandler} object which takes - * care for proper synchronization.) - * - * @since 2.0 - */ - @Override - public void read(final Reader in) throws ConfigurationException, IOException - { - getLayout().load(this, in); - } + /** + * Constructor. + * + * @param reader A Reader. + */ + public JupPropertiesReader(final Reader reader) + { + super(reader); + } - /** - * {@inheritDoc} This implementation delegates to the associated layout - * object which does the actual saving. Note that, analogous to - * {@link #read(Reader)}, this method does not do any synchronization. - * - * @since 2.0 - */ - @Override - public void write(final Writer out) throws ConfigurationException, IOException - { - getLayout().save(this, out); - } - /** - * Creates a copy of this object. - * - * @return the copy - */ - @Override - public Object clone() - { - final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone(); - if (layout != null) + @Override + protected void parseProperty(final String line) { - copy.setLayout(new PropertiesConfigurationLayout(layout)); + final String[] property = doParseProperty(line, false); + initPropertyName(property[0]); + initPropertyValue(property[1]); + initPropertySeparator(property[2]); } - return copy; - } - - /** - * This method is invoked by the associated - * {@link PropertiesConfigurationLayout} object for each - * property definition detected in the parsed properties file. Its task is - * to check whether this is a special property definition (e.g. the - * {@code include} property). If not, the property must be added to - * this configuration. The return value indicates whether the property - * should be treated as a normal property. If it is <b>false</b>, the - * layout object will ignore this property. - * - * @param key the property key - * @param value the property value - * @param seenStack the stack of seen include URLs - * @return a flag whether this is a normal property - * @throws ConfigurationException if an error occurs - * @since 1.3 - */ - boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack) - throws ConfigurationException - { - boolean result; - if (StringUtils.isNotEmpty(getInclude()) - && key.equalsIgnoreCase(getInclude())) + @Override + public String readProperty() throws IOException { - if (isIncludesAllowed()) + getCommentLines().clear(); + final StringBuilder buffer = new StringBuilder(); + + while (true) { - final Collection<String> files = - getListDelimiterHandler().split(value, true); - for (final String f : files) + String line = readLine(); + if (line == null) { - loadIncludeFile(interpolate(f), false, seenStack); + // EOF + if (buffer.length() > 0) + { + break; + } + return null; } - } - result = false; - } - else if (StringUtils.isNotEmpty(getIncludeOptional()) - && key.equalsIgnoreCase(getIncludeOptional())) - { - if (isIncludesAllowed()) - { - final Collection<String> files = - getListDelimiterHandler().split(value, true); - for (final String f : files) + // while a property line continues there are no comments (even if the line from + // the file looks like one) + if (isCommentLine(line) && (buffer.length() == 0)) { - loadIncludeFile(interpolate(f), true, seenStack); + getCommentLines().add(line); + continue; + } + + // while property line continues left trim all following lines read from the + // file + if (buffer.length() > 0) + { + // index of the first non-whitespace character + int i; + for (i = 0; i < line.length(); i++) + { + if (!Character.isWhitespace(line.charAt(i))) + { + break; + } + } + + line = line.substring(i); + } + + if (checkCombineLines(line)) + { + line = line.substring(0, line.length() - 1); + buffer.append(line); + } + else + { + buffer.append(line); + break; } } - result = false; + return buffer.toString(); } - else + @Override + protected String unescapePropertyValue(final String value) { - addPropertyInternal(key, value); - result = true; + return unescapeJava(value, true); } - return result; } - /** - * Tests whether a line is a comment, i.e. whether it starts with a comment - * character. - * - * @param line the line - * @return a flag if this is a comment line - * @since 1.3 - */ - static boolean isCommentLine(final String line) - { - final String s = line.trim(); - // blanc lines are also treated as comment lines - return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; - } + /** + * A {@link PropertiesWriter} that tries to mimic the behavior of + * {@link java.util.Properties}. + * + * @since 2.4 + */ + public static class JupPropertiesWriter extends PropertiesWriter + { + + /** + * The starting ASCII printable character. + */ + private static final int PRINTABLE_INDEX_END = 0x7e; + + /** + * The ending ASCII printable character. + */ + private static final int PRINTABLE_INDEX_START = 0x20; + + /** + * A UnicodeEscaper for characters outside the ASCII printable range. + */ + private static final UnicodeEscaper ESCAPER = UnicodeEscaper.outsideOf(PRINTABLE_INDEX_START, + PRINTABLE_INDEX_END); + + /** + * Characters that need to be escaped when wring a properties file. + */ + private static final Map<CharSequence, CharSequence> JUP_CHARS_ESCAPE; + static + { + final Map<CharSequence, CharSequence> initialMap = new HashMap<>(); + initialMap.put("\\", "\\\\"); + initialMap.put("\n", "\\n"); + initialMap.put("\t", "\\t"); + initialMap.put("\f", "\\f"); + initialMap.put("\r", "\\r"); + JUP_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap); + } + + /** + * Creates a new instance of {@code JupPropertiesWriter}. + * + * @param writer a Writer object providing the underlying stream + * @param delHandler the delimiter handler for dealing with properties with + * multiple values + * @param escapeUnicode whether Unicode characters should be escaped using + * Unicode escapes + */ + public JupPropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, + final boolean escapeUnicode) + { + super(writer, delHandler, value -> { + String valueString = String.valueOf(value); + + CharSequenceTranslator translator; + if (escapeUnicode) + { + translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE), ESCAPER); + } + else + { + translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE)); + } + + valueString = translator.translate(valueString); + + // escape the first leading space to preserve it (and all after it) + if (valueString.startsWith(" ")) + { + valueString = "\\" + valueString; + } - /** - * Returns the number of trailing backslashes. This is sometimes needed for - * the correct handling of escape characters. - * - * @param line the string to investigate - * @return the number of trailing backslashes - */ - private static int countTrailingBS(final String line) - { - int bsCount = 0; - for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) - { - bsCount++; + return valueString; + }); } - return bsCount; } /** @@ -743,99 +558,70 @@ public class PropertiesConfiguration extends BaseConfiguration /** Constant for the index of the group for the separator. */ private static final int IDX_SEPARATOR = 3; - /** Stores the comment lines for the currently processed property.*/ - private final List<String> commentLines; - - /** Stores the name of the last read property.*/ - private String propertyName; - - /** Stores the value of the last read property.*/ - private String propertyValue; - - /** Stores the property separator of the last read property.*/ - private String propertySeparator = DEFAULT_SEPARATOR; - /** - * Constructor. + * Checks if the passed in line should be combined with the following. + * This is true, if the line ends with an odd number of backslashes. * - * @param reader A Reader. + * @param line the line + * @return a flag if the lines should be combined */ - public PropertiesReader(final Reader reader) + static boolean checkCombineLines(final String line) { - super(reader); - commentLines = new ArrayList<>(); + return countTrailingBS(line) % 2 != 0; } /** - * Reads a property line. Returns null if Stream is - * at EOF. Concatenates lines ending with "\". - * Skips lines beginning with "#" or "!" and empty lines. - * The return value is a property definition ({@code <name>} - * = {@code <value>}) - * - * @return A string containing a property value or null + * Parse a property line and return the key, the value, and the separator in an + * array. * - * @throws IOException in case of an I/O error + * @param line the line to parse + * @param trimValue flag whether the value is to be trimmed + * @return an array with the property's key, value, and separator */ - public String readProperty() throws IOException + static String[] doParseProperty(final String line, final boolean trimValue) { - commentLines.clear(); - final StringBuilder buffer = new StringBuilder(); + final Matcher matcher = PROPERTY_PATTERN.matcher(line); - while (true) + final String[] result = {"", "", ""}; + + if (matcher.matches()) { - String line = readLine(); - if (line == null) - { - // EOF - return null; - } + result[0] = matcher.group(IDX_KEY).trim(); - if (isCommentLine(line)) + String value = matcher.group(IDX_VALUE); + if (trimValue) { - commentLines.add(line); - continue; + value = value.trim(); } + result[1] = value; - line = line.trim(); - - if (checkCombineLines(line)) - { - line = line.substring(0, line.length() - 1); - buffer.append(line); - } - else - { - buffer.append(line); - break; - } + result[2] = matcher.group(IDX_SEPARATOR); } - return buffer.toString(); + + return result; } + /** Stores the comment lines for the currently processed property.*/ + private final List<String> commentLines; + + /** Stores the name of the last read property.*/ + private String propertyName; + + /** Stores the value of the last read property.*/ + private String propertyValue; + + /** Stores the property separator of the last read property.*/ + private String propertySeparator = DEFAULT_SEPARATOR; + /** - * Parses the next property from the input stream and stores the found - * name and value in internal fields. These fields can be obtained using - * the provided getter methods. The return value indicates whether EOF - * was reached (<b>false</b>) or whether further properties are - * available (<b>true</b>). + * Constructor. * - * @return a flag if further properties are available - * @throws IOException if an error occurs - * @since 1.3 + * @param reader A Reader. */ - public boolean nextProperty() throws IOException + public PropertiesReader(final Reader reader) { - final String line = readProperty(); - - if (line == null) - { - return false; // EOF - } - - // parse the line - parseProperty(line); - return true; + super(reader); + commentLines = new ArrayList<>(); } /** @@ -864,6 +650,19 @@ public class PropertiesConfiguration extends BaseConfiguration } /** + * Returns the separator that was used for the last read property. The + * separator can be stored so that it can later be restored when saving + * the configuration. + * + * @return the separator for the last read property + * @since 1.7 + */ + public String getPropertySeparator() + { + return propertySeparator; + } + + /** * Returns the value of the last read property. This method can be * called after {@link #nextProperty()} was invoked and * its return value was <b>true</b>. @@ -877,16 +676,70 @@ public class PropertiesConfiguration extends BaseConfiguration } /** - * Returns the separator that was used for the last read property. The - * separator can be stored so that it can later be restored when saving - * the configuration. + * Sets the name of the current property. This method can be called by + * {@code parseProperty()} for storing the results of the parse + * operation. It also ensures that the property key is correctly + * escaped. * - * @return the separator for the last read property + * @param name the name of the current property * @since 1.7 */ - public String getPropertySeparator() + protected void initPropertyName(final String name) { - return propertySeparator; + propertyName = unescapePropertyName(name); + } + + /** + * Sets the separator of the current property. This method can be called + * by {@code parseProperty()}. It allows the associated layout + * object to keep track of the property separators. When saving the + * configuration the separators can be restored. + * + * @param value the separator used for the current property + * @since 1.7 + */ + protected void initPropertySeparator(final String value) + { + propertySeparator = value; + } + + /** + * Sets the value of the current property. This method can be called by + * {@code parseProperty()} for storing the results of the parse + * operation. It also ensures that the property value is correctly + * escaped. + * + * @param value the value of the current property + * @since 1.7 + */ + protected void initPropertyValue(final String value) + { + propertyValue = unescapePropertyValue(value); + } + + /** + * Parses the next property from the input stream and stores the found + * name and value in internal fields. These fields can be obtained using + * the provided getter methods. The return value indicates whether EOF + * was reached (<b>false</b>) or whether further properties are + * available (<b>true</b>). + * + * @return a flag if further properties are available + * @throws IOException if an error occurs + * @since 1.3 + */ + public boolean nextProperty() throws IOException + { + final String line = readProperty(); + + if (line == null) + { + return false; // EOF + } + + // parse the line + parseProperty(line); + return true; } /** @@ -908,17 +761,50 @@ public class PropertiesConfiguration extends BaseConfiguration } /** - * Sets the name of the current property. This method can be called by - * {@code parseProperty()} for storing the results of the parse - * operation. It also ensures that the property key is correctly - * escaped. + * Reads a property line. Returns null if Stream is + * at EOF. Concatenates lines ending with "\". + * Skips lines beginning with "#" or "!" and empty lines. + * The return value is a property definition ({@code <name>} + * = {@code <value>}) * - * @param name the name of the current property - * @since 1.7 + * @return A string containing a property value or null + * + * @throws IOException in case of an I/O error */ - protected void initPropertyName(final String name) + public String readProperty() throws IOException { - propertyName = unescapePropertyName(name); + commentLines.clear(); + final StringBuilder buffer = new StringBuilder(); + + while (true) + { + String line = readLine(); + if (line == null) + { + // EOF + return null; + } + + if (isCommentLine(line)) + { + commentLines.add(line); + continue; + } + + line = line.trim(); + + if (checkCombineLines(line)) + { + line = line.substring(0, line.length() - 1); + buffer.append(line); + } + else + { + buffer.append(line); + break; + } + } + return buffer.toString(); } /** @@ -934,20 +820,6 @@ public class PropertiesConfiguration extends BaseConfiguration } /** - * Sets the value of the current property. This method can be called by - * {@code parseProperty()} for storing the results of the parse - * operation. It also ensures that the property value is correctly - * escaped. - * - * @param value the value of the current property - * @since 1.7 - */ - protected void initPropertyValue(final String value) - { - propertyValue = unescapePropertyValue(value); - } - - /** * Performs unescaping on the given property value. * * @param value the property value @@ -958,63 +830,6 @@ public class PropertiesConfiguration extends BaseConfiguration { return unescapeJava(value); } - - /** - * Sets the separator of the current property. This method can be called - * by {@code parseProperty()}. It allows the associated layout - * object to keep track of the property separators. When saving the - * configuration the separators can be restored. - * - * @param value the separator used for the current property - * @since 1.7 - */ - protected void initPropertySeparator(final String value) - { - propertySeparator = value; - } - - /** - * Checks if the passed in line should be combined with the following. - * This is true, if the line ends with an odd number of backslashes. - * - * @param line the line - * @return a flag if the lines should be combined - */ - static boolean checkCombineLines(final String line) - { - return countTrailingBS(line) % 2 != 0; - } - - /** - * Parse a property line and return the key, the value, and the separator in an - * array. - * - * @param line the line to parse - * @param trimValue flag whether the value is to be trimmed - * @return an array with the property's key, value, and separator - */ - static String[] doParseProperty(final String line, final boolean trimValue) - { - final Matcher matcher = PROPERTY_PATTERN.matcher(line); - - final String[] result = {"", "", ""}; - - if (matcher.matches()) - { - result[0] = matcher.group(IDX_KEY).trim(); - - String value = matcher.group(IDX_VALUE); - if (trimValue) - { - value = value.trim(); - } - result[1] = value; - - result[2] = matcher.group(IDX_SEPARATOR); - } - - return result; - } } // class PropertiesReader /** @@ -1065,218 +880,40 @@ public class PropertiesConfiguration extends BaseConfiguration private final ListDelimiterHandler delimiterHandler; /** The separator to be used for the current property. */ - private String currentSeparator; - - /** The global separator. If set, it overrides the current separator.*/ - private String globalSeparator; - - /** The line separator.*/ - private String lineSeparator; - - /** - * Creates a new instance of {@code PropertiesWriter}. - * - * @param writer a Writer object providing the underlying stream - * @param delHandler the delimiter handler for dealing with properties - * with multiple values - */ - public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler) - { - this(writer, delHandler, DEFAULT_TRANSFORMER); - } - - /** - * Creates a new instance of {@code PropertiesWriter}. - * - * @param writer a Writer object providing the underlying stream - * @param delHandler the delimiter handler for dealing with properties - * with multiple values - * @param valueTransformer the value transformer used to escape property values - */ - public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, - final ValueTransformer valueTransformer) - { - super(writer); - delimiterHandler = delHandler; - this.valueTransformer = valueTransformer; - } - - /** - * Returns the delimiter handler for properties with multiple values. - * This object is used to escape property values so that they can be - * read in correctly the next time they are loaded. - * - * @return the delimiter handler for properties with multiple values - * @since 2.0 - */ - public ListDelimiterHandler getDelimiterHandler() - { - return delimiterHandler; - } - - /** - * Returns the current property separator. - * - * @return the current property separator - * @since 1.7 - */ - public String getCurrentSeparator() - { - return currentSeparator; - } - - /** - * Sets the current property separator. This separator is used when - * writing the next property. - * - * @param currentSeparator the current property separator - * @since 1.7 - */ - public void setCurrentSeparator(final String currentSeparator) - { - this.currentSeparator = currentSeparator; - } - - /** - * Returns the global property separator. - * - * @return the global property separator - * @since 1.7 - */ - public String getGlobalSeparator() - { - return globalSeparator; - } - - /** - * Sets the global property separator. This separator corresponds to the - * {@code globalSeparator} property of - * {@link PropertiesConfigurationLayout}. It defines the separator to be - * used for all properties. If it is undefined, the current separator is - * used. - * - * @param globalSeparator the global property separator - * @since 1.7 - */ - public void setGlobalSeparator(final String globalSeparator) - { - this.globalSeparator = globalSeparator; - } - - /** - * Returns the line separator. - * - * @return the line separator - * @since 1.7 - */ - public String getLineSeparator() - { - return lineSeparator != null ? lineSeparator : LINE_SEPARATOR; - } - - /** - * Sets the line separator. Each line written by this writer is - * terminated with this separator. If not set, the platform-specific - * line separator is used. - * - * @param lineSeparator the line separator to be used - * @since 1.7 - */ - public void setLineSeparator(final String lineSeparator) - { - this.lineSeparator = lineSeparator; - } - - /** - * Write a property. - * - * @param key the key of the property - * @param value the value of the property - * - * @throws IOException if an I/O error occurs - */ - public void writeProperty(final String key, final Object value) throws IOException - { - writeProperty(key, value, false); - } - - /** - * Write a property. - * - * @param key The key of the property - * @param values The array of values of the property - * - * @throws IOException if an I/O error occurs - */ - public void writeProperty(final String key, final List<?> values) throws IOException - { - for (int i = 0; i < values.size(); i++) - { - writeProperty(key, values.get(i)); - } - } - - /** - * Writes the given property and its value. If the value happens to be a - * list, the {@code forceSingleLine} flag is evaluated. If it is - * set, all values are written on a single line using the list delimiter - * as separator. - * - * @param key the property key - * @param value the property value - * @param forceSingleLine the "force single line" flag - * @throws IOException if an error occurs - * @since 1.3 - */ - public void writeProperty(final String key, final Object value, - final boolean forceSingleLine) throws IOException - { - String v; + private String currentSeparator; - if (value instanceof List) - { - v = null; - final List<?> values = (List<?>) value; - if (forceSingleLine) - { - try - { - v = String.valueOf(getDelimiterHandler() - .escapeList(values, valueTransformer)); - } - catch (final UnsupportedOperationException uoex) - { - // the handler may not support escaping lists, - // then the list is written in multiple lines - } - } - if (v == null) - { - writeProperty(key, values); - return; - } - } - else - { - v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer)); - } + /** The global separator. If set, it overrides the current separator.*/ + private String globalSeparator; - write(escapeKey(key)); - write(fetchSeparator(key, value)); - write(v); + /** The line separator.*/ + private String lineSeparator; - writeln(null); + /** + * Creates a new instance of {@code PropertiesWriter}. + * + * @param writer a Writer object providing the underlying stream + * @param delHandler the delimiter handler for dealing with properties + * with multiple values + */ + public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler) + { + this(writer, delHandler, DEFAULT_TRANSFORMER); } /** - * Write a comment. + * Creates a new instance of {@code PropertiesWriter}. * - * @param comment the comment to write - * @throws IOException if an I/O error occurs + * @param writer a Writer object providing the underlying stream + * @param delHandler the delimiter handler for dealing with properties + * with multiple values + * @param valueTransformer the value transformer used to escape property values */ - public void writeComment(final String comment) throws IOException + public PropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, + final ValueTransformer valueTransformer) { - writeln("# " + comment); + super(writer); + delimiterHandler = delHandler; + this.valueTransformer = valueTransformer; } /** @@ -1313,23 +950,6 @@ public class PropertiesConfiguration extends BaseConfiguration } /** - * Helper method for writing a line with the platform specific line - * ending. - * - * @param s the content of the line (may be <b>null</b>) - * @throws IOException if an error occurs - * @since 1.3 - */ - public void writeln(final String s) throws IOException - { - if (s != null) - { - write(s); - } - write(getLineSeparator()); - } - - /** * Returns the separator to be used for the given property. This method * is called by {@code writeProperty()}. The string returned here * is used as separator between the property key and its value. Per @@ -1349,336 +969,364 @@ public class PropertiesConfiguration extends BaseConfiguration return getGlobalSeparator() != null ? getGlobalSeparator() : StringUtils.defaultString(getCurrentSeparator()); } - } // class PropertiesWriter - /** - * <p> - * Definition of an interface that allows customization of read and write - * operations. - * </p> - * <p> - * For reading and writing properties files the inner classes - * {@code PropertiesReader} and {@code PropertiesWriter} are used. - * This interface defines factory methods for creating both a - * {@code PropertiesReader} and a {@code PropertiesWriter}. An - * object implementing this interface can be passed to the - * {@code setIOFactory()} method of - * {@code PropertiesConfiguration}. Every time the configuration is - * read or written the {@code IOFactory} is asked to create the - * appropriate reader or writer object. This provides an opportunity to - * inject custom reader or writer implementations. - * </p> - * - * @since 1.7 - */ - public interface IOFactory - { /** - * Creates a {@code PropertiesReader} for reading a properties - * file. This method is called whenever the - * {@code PropertiesConfiguration} is loaded. The reader returned - * by this method is then used for parsing the properties file. + * Returns the current property separator. * - * @param in the underlying reader (of the properties file) - * @return the {@code PropertiesReader} for loading the - * configuration + * @return the current property separator + * @since 1.7 */ - PropertiesReader createPropertiesReader(Reader in); + public String getCurrentSeparator() + { + return currentSeparator; + } /** - * Creates a {@code PropertiesWriter} for writing a properties - * file. This method is called before the - * {@code PropertiesConfiguration} is saved. The writer returned by - * this method is then used for writing the properties file. + * Returns the delimiter handler for properties with multiple values. + * This object is used to escape property values so that they can be + * read in correctly the next time they are loaded. * - * @param out the underlying writer (to the properties file) - * @param handler the list delimiter delimiter for list parsing - * @return the {@code PropertiesWriter} for saving the - * configuration + * @return the delimiter handler for properties with multiple values + * @since 2.0 */ - PropertiesWriter createPropertiesWriter(Writer out, - ListDelimiterHandler handler); - } + public ListDelimiterHandler getDelimiterHandler() + { + return delimiterHandler; + } - /** - * <p> - * A default implementation of the {@code IOFactory} interface. - * </p> - * <p> - * This class implements the {@code createXXXX()} methods defined by - * the {@code IOFactory} interface in a way that the default objects - * (i.e. {@code PropertiesReader} and {@code PropertiesWriter} are - * returned. Customizing either the reader or the writer (or both) can be - * done by extending this class and overriding the corresponding - * {@code createXXXX()} method. - * </p> - * - * @since 1.7 - */ - public static class DefaultIOFactory implements IOFactory - { /** - * The singleton instance. + * Returns the global property separator. + * + * @return the global property separator + * @since 1.7 */ - static final DefaultIOFactory INSTANCE = new DefaultIOFactory(); - - @Override - public PropertiesReader createPropertiesReader(final Reader in) + public String getGlobalSeparator() { - return new PropertiesReader(in); + return globalSeparator; } - @Override - public PropertiesWriter createPropertiesWriter(final Writer out, - final ListDelimiterHandler handler) + /** + * Returns the line separator. + * + * @return the line separator + * @since 1.7 + */ + public String getLineSeparator() { - return new PropertiesWriter(out, handler); + return lineSeparator != null ? lineSeparator : LINE_SEPARATOR; } - } - /** - * An alternative {@link IOFactory} that tries to mimic the behavior of - * {@link java.util.Properties} (Jup) more closely. The goal is to allow both of - * them be used interchangeably when reading and writing properties files - * without losing or changing information. - * <p> - * It also has the option to <em>not</em> use Unicode escapes. When using UTF-8 - * encoding (which is e.g. the new default for resource bundle properties files - * since Java 9), Unicode escapes are no longer required and avoiding them makes - * properties files more readable with regular text editors. - * <p> - * Some of the ways this implementation differs from {@link DefaultIOFactory}: - * <ul> - * <li>Trailing whitespace will not be trimmed from each line.</li> - * <li>Unknown escape sequences will have their backslash removed.</li> - * <li>{@code \b} is not a recognized escape sequence.</li> - * <li>Leading spaces in property values are preserved by escaping them.</li> - * <li>All natural lines (i.e. in the file) of a logical property line will have - * their leading whitespace trimmed.</li> - * <li>Natural lines that look like comment lines within a logical line are not - * treated as such; they're part of the property value.</li> - * </ul> - * - * @since 2.4 - */ - public static class JupIOFactory implements IOFactory - { + /** + * Sets the current property separator. This separator is used when + * writing the next property. + * + * @param currentSeparator the current property separator + * @since 1.7 + */ + public void setCurrentSeparator(final String currentSeparator) + { + this.currentSeparator = currentSeparator; + } /** - * Whether characters less than {@code \u0020} and characters greater than - * {@code \u007E} in property keys or values should be escaped using - * Unicode escape sequences. Not necessary when e.g. writing as UTF-8. + * Sets the global property separator. This separator corresponds to the + * {@code globalSeparator} property of + * {@link PropertiesConfigurationLayout}. It defines the separator to be + * used for all properties. If it is undefined, the current separator is + * used. + * + * @param globalSeparator the global property separator + * @since 1.7 */ - private final boolean escapeUnicode; + public void setGlobalSeparator(final String globalSeparator) + { + this.globalSeparator = globalSeparator; + } /** - * Constructs a new {@link JupIOFactory} with Unicode escaping. + * Sets the line separator. Each line written by this writer is + * terminated with this separator. If not set, the platform-specific + * line separator is used. + * + * @param lineSeparator the line separator to be used + * @since 1.7 */ - public JupIOFactory() + public void setLineSeparator(final String lineSeparator) { - this(true); + this.lineSeparator = lineSeparator; } /** - * Constructs a new {@link JupIOFactory} with optional Unicode escaping. Whether - * Unicode escaping is required depends on 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 necessary. Unfortunately this factory can't determine the encoding on its - * own. + * Write a comment. * - * @param escapeUnicode whether Unicode characters should be escaped + * @param comment the comment to write + * @throws IOException if an I/O error occurs */ - public JupIOFactory(final boolean escapeUnicode) + public void writeComment(final String comment) throws IOException { - this.escapeUnicode = escapeUnicode; + writeln("# " + comment); } - @Override - public PropertiesReader createPropertiesReader(final Reader in) + /** + * Helper method for writing a line with the platform specific line + * ending. + * + * @param s the content of the line (may be <b>null</b>) + * @throws IOException if an error occurs + * @since 1.3 + */ + public void writeln(final String s) throws IOException { - return new JupPropertiesReader(in); + if (s != null) + { + write(s); + } + write(getLineSeparator()); } - @Override - public PropertiesWriter createPropertiesWriter(final Writer out, final ListDelimiterHandler handler) + /** + * Write a property. + * + * @param key The key of the property + * @param values The array of values of the property + * + * @throws IOException if an I/O error occurs + */ + public void writeProperty(final String key, final List<?> values) throws IOException { - return new JupPropertiesWriter(out, handler, escapeUnicode); + for (int i = 0; i < values.size(); i++) + { + writeProperty(key, values.get(i)); + } } - } - - /** - * A {@link PropertiesReader} that tries to mimic the behavior of - * {@link java.util.Properties}. - * - * @since 2.4 - */ - public static class JupPropertiesReader extends PropertiesReader - { - /** - * Constructor. + * Write a property. * - * @param reader A Reader. + * @param key the key of the property + * @param value the value of the property + * + * @throws IOException if an I/O error occurs */ - public JupPropertiesReader(final Reader reader) + public void writeProperty(final String key, final Object value) throws IOException { - super(reader); + writeProperty(key, value, false); } - - @Override - public String readProperty() throws IOException + /** + * Writes the given property and its value. If the value happens to be a + * list, the {@code forceSingleLine} flag is evaluated. If it is + * set, all values are written on a single line using the list delimiter + * as separator. + * + * @param key the property key + * @param value the property value + * @param forceSingleLine the "force single line" flag + * @throws IOException if an error occurs + * @since 1.3 + */ + public void writeProperty(final String key, final Object value, + final boolean forceSingleLine) throws IOException { - getCommentLines().clear(); - final StringBuilder buffer = new StringBuilder(); + String v; - while (true) + if (value instanceof List) { - String line = readLine(); - if (line == null) + v = null; + final List<?> values = (List<?>) value; + if (forceSingleLine) { - // EOF - if (buffer.length() > 0) + try { - break; + v = String.valueOf(getDelimiterHandler() + .escapeList(values, valueTransformer)); } - return null; - } - - // while a property line continues there are no comments (even if the line from - // the file looks like one) - if (isCommentLine(line) && (buffer.length() == 0)) - { - getCommentLines().add(line); - continue; - } - - // while property line continues left trim all following lines read from the - // file - if (buffer.length() > 0) - { - // index of the first non-whitespace character - int i; - for (i = 0; i < line.length(); i++) + catch (final UnsupportedOperationException uoex) { - if (!Character.isWhitespace(line.charAt(i))) - { - break; - } + // the handler may not support escaping lists, + // then the list is written in multiple lines } - - line = line.substring(i); - } - - if (checkCombineLines(line)) - { - line = line.substring(0, line.length() - 1); - buffer.append(line); } - else + if (v == null) { - buffer.append(line); - break; + writeProperty(key, values); + return; } } - return buffer.toString(); - } + else + { + v = String.valueOf(getDelimiterHandler().escape(value, valueTransformer)); + } - @Override - protected void parseProperty(final String line) - { - final String[] property = doParseProperty(line, false); - initPropertyName(property[0]); - initPropertyValue(property[1]); - initPropertySeparator(property[2]); + write(escapeKey(key)); + write(fetchSeparator(key, value)); + write(v); + + writeln(null); } + } // class PropertiesWriter - @Override - protected String unescapePropertyValue(final String value) + /** + * Defines default error handling for the special {@code "include"} key by throwing the given exception. + * + * @since 2.6 + */ + public static final ConfigurationConsumer<ConfigurationException> DEFAULT_INCLUDE_LISTENER = e -> { throw e; }; + + /** + * Defines error handling as a noop for the special {@code "include"} key. + * + * @since 2.6 + */ + public static final ConfigurationConsumer<ConfigurationException> NOOP_INCLUDE_LISTENER = e -> { /* noop */ }; + + /** + * The default encoding (ISO-8859-1 as specified by + * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) + */ + public static final String DEFAULT_ENCODING = "ISO-8859-1"; + + /** Constant for the supported comment characters.*/ + static final String COMMENT_CHARS = "#!"; + + /** Constant for the default properties separator.*/ + static final String DEFAULT_SEPARATOR = " = "; + + /** + * A string with special characters that need to be unescaped when reading + * a properties file. {@code java.util.Properties} escapes these characters + * when writing out a properties file. + */ + private static final String UNESCAPE_CHARACTERS = ":#=!\\\'\""; + + /** + * This is the name of the property that can point to other + * properties file for including other properties files. + */ + private static String include = "include"; + + /** + * This is the name of the property that can point to other + * properties file for including other properties files. + * <p> + * If the file is absent, processing continues normally. + * </p> + */ + private static String includeOptional = "includeoptional"; + + /** The list of possible key/value separators */ + private static final char[] SEPARATORS = new char[] {'=', ':'}; + + /** The white space characters used as key/value separators. */ + private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; + + /** Constant for the platform specific line separator.*/ + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + /** Constant for the radix of hex numbers.*/ + private static final int HEX_RADIX = 16; + + /** Constant for the length of a unicode literal.*/ + private static final int UNICODE_LEN = 4; + + /** + * Returns the number of trailing backslashes. This is sometimes needed for + * the correct handling of escape characters. + * + * @param line the string to investigate + * @return the number of trailing backslashes + */ + private static int countTrailingBS(final String line) + { + int bsCount = 0; + for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) { - return unescapeJava(value, true); + bsCount++; } + return bsCount; + } + + /** + * Gets the property value for including other properties files. + * By default it is "include". + * + * @return A String. + */ + public static String getInclude() + { + return PropertiesConfiguration.include; + } + + /** + * Gets the property value for including other properties files. + * By default it is "includeoptional". + * <p> + * If the file is absent, processing continues normally. + * </p> + * + * @return A String. + * @since 2.5 + */ + public static String getIncludeOptional() + { + return PropertiesConfiguration.includeOptional; + } + + /** + * Tests whether a line is a comment, i.e. whether it starts with a comment + * character. + * + * @param line the line + * @return a flag if this is a comment line + * @since 1.3 + */ + static boolean isCommentLine(final String line) + { + final String s = line.trim(); + // blanc lines are also treated as comment lines + return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; } /** - * A {@link PropertiesWriter} that tries to mimic the behavior of - * {@link java.util.Properties}. + * Checks whether the specified character needs to be unescaped. This method + * is called when during reading a property file an escape character ('\') + * is detected. If the character following the escape character is + * recognized as a special character which is escaped per default in a Java + * properties file, it has to be unescaped. * - * @since 2.4 + * @param ch the character in question + * @return a flag whether this character has to be unescaped */ - public static class JupPropertiesWriter extends PropertiesWriter + private static boolean needsUnescape(final char ch) { + return UNESCAPE_CHARACTERS.indexOf(ch) >= 0; + } - /** - * The starting ASCII printable character. - */ - private static final int PRINTABLE_INDEX_END = 0x7e; - - /** - * The ending ASCII printable character. - */ - private static final int PRINTABLE_INDEX_START = 0x20; - - /** - * A UnicodeEscaper for characters outside the ASCII printable range. - */ - private static final UnicodeEscaper ESCAPER = UnicodeEscaper.outsideOf(PRINTABLE_INDEX_START, - PRINTABLE_INDEX_END); - - /** - * Characters that need to be escaped when wring a properties file. - */ - private static final Map<CharSequence, CharSequence> JUP_CHARS_ESCAPE; - static - { - final Map<CharSequence, CharSequence> initialMap = new HashMap<>(); - initialMap.put("\\", "\\\\"); - initialMap.put("\n", "\\n"); - initialMap.put("\t", "\\t"); - initialMap.put("\f", "\\f"); - initialMap.put("\r", "\\r"); - JUP_CHARS_ESCAPE = Collections.unmodifiableMap(initialMap); - } - - /** - * Creates a new instance of {@code JupPropertiesWriter}. - * - * @param writer a Writer object providing the underlying stream - * @param delHandler the delimiter handler for dealing with properties with - * multiple values - * @param escapeUnicode whether Unicode characters should be escaped using - * Unicode escapes - */ - public JupPropertiesWriter(final Writer writer, final ListDelimiterHandler delHandler, - final boolean escapeUnicode) - { - super(writer, delHandler, value -> { - String valueString = String.valueOf(value); - - CharSequenceTranslator translator; - if (escapeUnicode) - { - translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE), ESCAPER); - } - else - { - translator = new AggregateTranslator(new LookupTranslator(JUP_CHARS_ESCAPE)); - } - - valueString = translator.translate(valueString); - - // escape the first leading space to preserve it (and all after it) - if (valueString.startsWith(" ")) - { - valueString = "\\" + valueString; - } - - return valueString; - }); - } + /** + * Sets the property value for including other properties files. + * By default it is "include". + * + * @param inc A String. + */ + public static void setInclude(final String inc) + { + PropertiesConfiguration.include = inc; + } + /** + * Sets the property value for including other properties files. + * By default it is "include". + * <p> + * If the file is absent, processing continues normally. + * </p> + * + * @param inc A String. + * @since 2.5 + */ + public static void setIncludeOptional(final String inc) + { + PropertiesConfiguration.includeOptional = inc; } /** @@ -1795,39 +1443,199 @@ public class PropertiesConfiguration extends BaseConfiguration out.append(ch); } - continue; - } - else if (ch == '\\') - { - hadSlash = true; - continue; - } - out.append(ch); - } + continue; + } + else if (ch == '\\') + { + hadSlash = true; + continue; + } + out.append(ch); + } + + if (hadSlash) + { + // then we're in the weird case of a \ at the end of the + // string, let's output it anyway. + out.append('\\'); + } + + return out.toString(); + } + + /** Stores the layout object.*/ + private PropertiesConfigurationLayout layout; + + /** The include listener for the special {@code "include"} key. */ + private ConfigurationConsumer<ConfigurationException> includeListener; + + /** The IOFactory for creating readers and writers.*/ + private IOFactory ioFactory; + + /** The current {@code FileLocator}. */ + private FileLocator locator; + + /** Allow file inclusion or not */ + private boolean includesAllowed = true; + + /** + * Creates an empty PropertyConfiguration object which can be + * used to synthesize a new Properties file by adding values and + * then saving(). + */ + public PropertiesConfiguration() + { + installLayout(createLayout()); + } + + /** + * Creates a copy of this object. + * + * @return the copy + */ + @Override + public Object clone() + { + final PropertiesConfiguration copy = (PropertiesConfiguration) super.clone(); + if (layout != null) + { + copy.setLayout(new PropertiesConfigurationLayout(layout)); + } + return copy; + } + + /** + * Creates a standard layout object. This configuration is initialized with + * such a standard layout. + * + * @return the newly created layout object + */ + private PropertiesConfigurationLayout createLayout() + { + return new PropertiesConfigurationLayout(); + } + + /** + * Returns the footer comment. This is a comment at the very end of the + * file. + * + * @return the footer comment + * @since 2.0 + */ + public String getFooter() + { + beginRead(false); + try + { + return getLayout().getFooterComment(); + } + finally + { + endRead(); + } + } + + /** + * Return the comment header. + * + * @return the comment header + * @since 1.1 + */ + public String getHeader() + { + beginRead(false); + try + { + return getLayout().getHeaderComment(); + } + finally + { + endRead(); + } + } + + /** + * Gets the current include listener, never null. + * + * @return the current include listener, never null. + * @since 2.6 + */ + public ConfigurationConsumer<ConfigurationException> getIncludeListener() + { + return includeListener != null ? includeListener : PropertiesConfiguration.DEFAULT_INCLUDE_LISTENER; + } + + /** + * Returns the {@code IOFactory} to be used for creating readers and + * writers when loading or saving this configuration. + * + * @return the {@code IOFactory} + * @since 1.7 + */ + public IOFactory getIOFactory() + { + return ioFactory != null ? ioFactory : DefaultIOFactory.INSTANCE; + } + + /** + * Returns the associated layout object. + * + * @return the associated layout object + * @since 1.3 + */ + public PropertiesConfigurationLayout getLayout() + { + return layout; + } + + /** + * Stores the current {@code FileLocator} for a following IO operation. The + * {@code FileLocator} is needed to resolve include files with relative file + * names. + * + * @param locator the current {@code FileLocator} + * @since 2.0 + */ + @Override + public void initFileLocator(final FileLocator locator) + { + this.locator = locator; + } - if (hadSlash) + /** + * Installs a layout object. It has to be ensured that the layout is + * registered as change listener at this configuration. If there is already + * a layout object installed, it has to be removed properly. + * + * @param layout the layout object to be installed + */ + private void installLayout(final PropertiesConfigurationLayout layout) + { + // only one layout must exist + if (this.layout != null) { - // then we're in the weird case of a \ at the end of the - // string, let's output it anyway. - out.append('\\'); + removeEventListener(ConfigurationEvent.ANY, this.layout); } - return out.toString(); + if (layout == null) + { + this.layout = createLayout(); + } + else + { + this.layout = layout; + } + addEventListener(ConfigurationEvent.ANY, this.layout); } /** - * Checks whether the specified character needs to be unescaped. This method - * is called when during reading a property file an escape character ('\') - * is detected. If the character following the escape character is - * recognized as a special character which is escaped per default in a Java - * properties file, it has to be unescaped. + * Reports the status of file inclusion. * - * @param ch the character in question - * @return a flag whether this character has to be unescaped + * @return True if include files are loaded. */ - private static boolean needsUnescape(final char ch) + public boolean isIncludesAllowed() { - return UNESCAPE_CHARACTERS.indexOf(ch) >= 0; + return this.includesAllowed; } /** @@ -1926,4 +1734,196 @@ public class PropertiesConfiguration extends BaseConfiguration return FileLocatorUtils.locate(includeLocator); } + /** + * This method is invoked by the associated + * {@link PropertiesConfigurationLayout} object for each + * property definition detected in the parsed properties file. Its task is + * to check whether this is a special property definition (e.g. the + * {@code include} property). If not, the property must be added to + * this configuration. The return value indicates whether the property + * should be treated as a normal property. If it is <b>false</b>, the + * layout object will ignore this property. + * + * @param key the property key + * @param value the property value + * @param seenStack the stack of seen include URLs + * @return a flag whether this is a normal property + * @throws ConfigurationException if an error occurs + * @since 1.3 + */ + boolean propertyLoaded(final String key, final String value, final Deque<URL> seenStack) + throws ConfigurationException + { + boolean result; + + if (StringUtils.isNotEmpty(getInclude()) + && key.equalsIgnoreCase(getInclude())) + { + if (isIncludesAllowed()) + { + final Collection<String> files = + getListDelimiterHandler().split(value, true); + for (final String f : files) + { + loadIncludeFile(interpolate(f), false, seenStack); + } + } + result = false; + } + + else if (StringUtils.isNotEmpty(getIncludeOptional()) + && key.equalsIgnoreCase(getIncludeOptional())) + { + if (isIncludesAllowed()) + { + final Collection<String> files = + getListDelimiterHandler().split(value, true); + for (final String f : files) + { + loadIncludeFile(interpolate(f), true, seenStack); + } + } + result = false; + } + + else + { + addPropertyInternal(key, value); + result = true; + } + + return result; + } + + /** + * {@inheritDoc} This implementation delegates to the associated layout + * object which does the actual loading. Note that this method does not + * do any synchronization. This lies in the responsibility of the caller. + * (Typically, the caller is a {@code FileHandler} object which takes + * care for proper synchronization.) + * + * @since 2.0 + */ + @Override + public void read(final Reader in) throws ConfigurationException, IOException + { + getLayout().load(this, in); + } + + /** + * Sets the footer comment. If set, this comment is written after all + * properties at the end of the file. + * + * @param footer the footer comment + * @since 2.0 + */ + public void setFooter(final String footer) + { + beginWrite(false); + try + { + getLayout().setFooterComment(footer); + } + finally + { + endWrite(); + } + } + + /** + * Set the comment header. + * + * @param header the header to use + * @since 1.1 + */ + public void setHeader(final String header) + { + beginWrite(false); + try + { + getLayout().setHeaderComment(header); + } + finally + { + endWrite(); + } + } + + /** + * Sets the current include listener, may not be null. + * + * @param includeListener the current include listener, may not be null. + * @throws IllegalArgumentException if the {@code includeListener} is null. + * @since 2.6 + */ + public void setIncludeListener(final ConfigurationConsumer<ConfigurationException> includeListener) + { + if (includeListener == null) + { + throw new IllegalArgumentException("includeListener must not be null."); + } + this.includeListener = includeListener; + } + + /** + * Controls whether additional files can be loaded by the {@code include = <xxx>} + * statement or not. This is <b>true</b> per default. + * + * @param includesAllowed True if Includes are allowed. + */ + public void setIncludesAllowed(final boolean includesAllowed) + { + this.includesAllowed = includesAllowed; + } + + /** + * Sets the {@code IOFactory} to be used for creating readers and + * writers when loading or saving this configuration. Using this method a + * client can customize the reader and writer classes used by the load and + * save operations. Note that this method must be called before invoking + * one of the {@code load()} and {@code save()} methods. + * Especially, if you want to use a custom {@code IOFactory} for + * changing the {@code PropertiesReader}, you cannot load the + * configuration data in the constructor. + * + * @param ioFactory the new {@code IOFactory} (must not be <b>null</b>) + * @throws IllegalArgumentException if the {@code IOFactory} is + * <b>null</b> + * @since 1.7 + */ + public void setIOFactory(final IOFactory ioFactory) + { + if (ioFactory == null) + { + throw new IllegalArgumentException("IOFactory must not be null."); + } + + this.ioFactory = ioFactory; + } + + /** + * Sets the associated layout object. + * + * @param layout the new layout object; can be <b>null</b>, then a new + * layout object will be created + * @since 1.3 + */ + public void setLayout(final PropertiesConfigurationLayout layout) + { + installLayout(layout); + } + + /** + * {@inheritDoc} This implementation delegates to the associated layout + * object which does the actual saving. Note that, analogous to + * {@link #read(Reader)}, this method does not do any synchronization. + * + * @since 2.0 + */ + @Override + public void write(final Writer out) throws ConfigurationException, IOException + { + getLayout().save(this, out); + } + }