blob: 5e776b8a12444a9cacdf0d632208751c52957d75 [file] [log] [blame]
/*
* Copyright 2001-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.configuration;
import java.io.File;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
/**
* This is the "classic" Properties loader which loads the values from
* a single or multiple files (which can be chained with "include =".
* All given path references are either absolute or relative to the
* file name supplied in the Constructor.
* <p>
* In this class, empty PropertyConfigurations can be built, properties
* added and later saved. include statements are (obviously) not supported
* if you don't construct a PropertyConfiguration from a file.
*
* <p>The properties file syntax is explained here:
*
* <ul>
* <li>
* Each property has the syntax <code>key = value</code>
* </li>
* <li>
* The <i>key</i> may use any character but the equal sign '='.
* </li>
* <li>
* <i>value</i> may be separated on different lines if a backslash
* is placed at the end of the line that continues below.
* </li>
* <li>
* If <i>value</i> is a list of strings, each token is separated
* by a comma ',' by default.
* </li>
* <li>
* Commas in each token are escaped placing a backslash right before
* the comma.
* </li>
* <li>
* If a <i>key</i> is used more than once, the values are appended
* like if they were on the same line separated with commas.
* </li>
* <li>
* Blank lines and lines starting with character '#' are skipped.
* </li>
* <li>
* If a property is named "include" (or whatever is defined by
* setInclude() and getInclude() and the value of that property is
* the full path to a file on disk, that file will be included into
* the ConfigurationsRepository. You can also pull in files relative
* to the parent configuration file. So if you have something
* like the following:
*
* include = additional.properties
*
* Then "additional.properties" is expected to be in the same
* directory as the parent configuration file.
*
* Duplicate name values will be replaced, so be careful.
*
* </li>
* </ul>
*
* <p>Here is an example of a valid extended properties file:
*
* <p><pre>
* # lines starting with # are comments
*
* # This is the simplest property
* key = value
*
* # A long property may be separated on multiple lines
* longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
* aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
*
* # This is a property with many tokens
* tokens_on_a_line = first token, second token
*
* # This sequence generates exactly the same result
* tokens_on_multiple_lines = first token
* tokens_on_multiple_lines = second token
*
* # commas may be escaped in tokens
* commas.excaped = Hi\, what'up?
*
* # properties can reference other properties
* base.prop = /base
* first.prop = ${base.prop}/first
* second.prop = ${first.prop}/second
* </pre>
*
* @author <a href="mailto:e.bourg@cross-systems.com">Emmanuel Bourg</a>
* @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
* @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
* @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
* @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
* @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
* @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
* @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
* @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
* @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
* @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
* @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
* @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
* @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger</a>
* @version $Id: PropertiesConfiguration.java,v 1.15 2004/09/23 11:45:07 ebourg Exp $
*/
public class PropertiesConfiguration extends AbstractFileConfiguration
{
/**
* This is the name of the property that can point to other
* properties file for including other properties files.
*/
static String include = "include";
/** Allow file inclusion or not */
private boolean includesAllowed;
/**
* Creates an empty PropertyConfiguration object which can be
* used to synthesize a new Properties file by adding values and
* then saving(). An object constructed by this C'tor can not be
* tickled into loading included files because it cannot supply a
* base for relative includes.
*/
public PropertiesConfiguration()
{
setIncludesAllowed(false);
}
/**
* Creates and loads the extended properties from the specified file.
* The specified file can contain "include = " properties which then
* are loaded and merged into the properties.
*
* @param fileName The name of the properties file to load.
* @throws ConfigurationException Error while loading the properties file
*/
public PropertiesConfiguration(String fileName) throws ConfigurationException
{
// enable includes
setIncludesAllowed(true);
// store the file name
this.fileName = fileName;
// locate the resource
url = ConfigurationUtils.locate(fileName);
// update the base path
setBasePath(ConfigurationUtils.getBasePath(url));
// load the file
load();
}
/**
* Creates and loads the extended properties from the specified file.
* The specified file can contain "include = " properties which then
* are loaded and merged into the properties.
*
* @param file The properties file to load.
* @throws ConfigurationException Error while loading the properties file
*/
public PropertiesConfiguration(File file) throws ConfigurationException
{
// enable includes
setIncludesAllowed(true);
// set the file and update the url, the base path and the file name
setFile(file);
// load the file
load();
}
/**
* Creates and loads the extended properties from the specified URL.
* The specified file can contain "include = " properties which then
* are loaded and merged into the properties.
*
* @param url The location of the properties file to load.
* @throws ConfigurationException Error while loading the properties file
*/
public PropertiesConfiguration(URL url) throws ConfigurationException
{
// enable includes
setIncludesAllowed(true);
// set the URL and update the base path and the file name
setURL(url);
// load the file
load();
}
/**
* Gets the property value for including other properties files.
* By default it is "include".
*
* @return A String.
*/
public static String getInclude()
{
return PropertiesConfiguration.include;
}
/**
* Sets the property value for including other properties files.
* By default it is "include".
*
* @param inc A String.
*/
public static void setInclude(String inc)
{
PropertiesConfiguration.include = inc;
}
/**
* Controls whether additional files can be loaded by the include = <xxx>
* statement or not. Base rule is, that objects created by the empty
* C'tor can not have included files.
*
* @param includesAllowed includesAllowed True if Includes are allowed.
*/
protected void setIncludesAllowed(boolean includesAllowed)
{
this.includesAllowed = includesAllowed;
}
/**
* Reports the status of file inclusion.
*
* @return True if include files are loaded.
*/
public boolean getIncludesAllowed()
{
return this.includesAllowed;
}
/**
* Load the properties from the given input stream and using the specified
* encoding.
*
* @param in An InputStream.
*
* @throws ConfigurationException
*/
public synchronized void load(Reader in) throws ConfigurationException
{
PropertiesReader reader = new PropertiesReader(in);
try
{
while (true)
{
String line = reader.readProperty();
if (line == null)
{
break; // EOF
}
int equalSign = line.indexOf('=');
if (equalSign > 0)
{
String key = line.substring(0, equalSign).trim();
String value = line.substring(equalSign + 1).trim();
// Though some software (e.g. autoconf) may produce
// empty values like foo=\n, emulate the behavior of
// java.util.Properties by setting the value to the
// empty string.
if (StringUtils.isNotEmpty(getInclude())
&& key.equalsIgnoreCase(getInclude()))
{
if (getIncludesAllowed() && url != null)
{
String [] files = StringUtils.split(value, getDelimiter());
for (int i = 0; i < files.length; i++)
{
load(ConfigurationUtils.locate(getBasePath(), files[i].trim()));
}
}
}
else
{
addProperty(key, unescapeJava(value));
}
}
}
}
catch (IOException ioe)
{
throw new ConfigurationException("Could not load configuration from input stream.", ioe);
}
}
/**
* Save the configuration to the specified stream.
*
* @param writer the output stream used to save the configuration
*/
public void save(Writer writer) throws ConfigurationException
{
try
{
PropertiesWriter out = new PropertiesWriter(writer);
out.writeComment("written by PropertiesConfiguration");
out.writeComment(new Date().toString());
Iterator keys = getKeys();
while (keys.hasNext())
{
String key = (String) keys.next();
Object value = getProperty(key);
if (value instanceof List)
{
out.writeProperty(key, (List) value);
}
else
{
out.writeProperty(key, value);
}
}
out.flush();
}
catch (IOException e)
{
throw new ConfigurationException(e.getMessage(), e);
}
}
/**
* Extend the setBasePath method to turn includes
* on and off based on the existence of a base path.
*
* @param basePath The new basePath to set.
*/
public void setBasePath(String basePath)
{
super.setBasePath(basePath);
setIncludesAllowed(StringUtils.isNotEmpty(basePath));
}
/**
* This class is used to read properties lines. These lines do
* not terminate with new-line chars but rather when there is no
* backslash sign a the end of the line. This is used to
* concatenate multiple lines for readability.
*/
public static class PropertiesReader extends LineNumberReader
{
/**
* Constructor.
*
* @param reader A Reader.
*/
public PropertiesReader(Reader reader)
{
super(reader);
}
/**
* Read a property. Returns null if Stream is
* at EOF. Concatenates lines ending with "\".
* Skips lines beginning with "#" and empty lines.
*
* @return A string containing a property value or null
*
* @throws IOException
*/
public String readProperty() throws IOException
{
StringBuffer buffer = new StringBuffer();
while (true)
{
String line = readLine();
if (line == null)
{
// EOF
return null;
}
line = line.trim();
if (StringUtils.isEmpty(line)
|| (line.charAt(0) == '#'))
{
continue;
}
if (line.endsWith("\\"))
{
line = line.substring(0, line.length() - 1);
buffer.append(line);
}
else
{
buffer.append(line);
break;
}
}
return buffer.toString();
}
} // class PropertiesReader
/**
* This class is used to write properties lines.
*/
public static class PropertiesWriter extends FilterWriter
{
/**
* Constructor.
*
* @param writer a Writer object providing the underlying stream
*/
public PropertiesWriter(Writer writer) throws IOException
{
super(writer);
}
/**
* Write a property.
*
* @param key
* @param value
* @throws IOException
*/
public void writeProperty(String key, Object value) throws IOException
{
write(key);
write(" = ");
if (value != null)
{
String v = StringEscapeUtils.escapeJava(String.valueOf(value));
v = StringUtils.replace(v, String.valueOf(getDelimiter()), "\\" + getDelimiter());
write(v);
}
write('\n');
}
/**
* Write a property.
*
* @param key The key of the property
* @param values The array of values of the property
*/
public void writeProperty(String key, List values) throws IOException
{
for (int i = 0; i < values.size(); i++)
{
writeProperty(key, values.get(i));
}
}
/**
* Write a comment.
*
* @param comment
* @throws IOException
*/
public void writeComment(String comment) throws IOException
{
write("# " + comment + "\n");
}
} // class PropertiesWriter
/**
* <p>Unescapes any Java literals found in the <code>String</code> to a
* <code>Writer</code>.</p> This is a slightly modified version of the
* StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
* drop escaped commas (i.e '\,').
*
* @param str the <code>String</code> to unescape, may be null
*
* @throws IllegalArgumentException if the Writer is <code>null</code>
*/
protected static String unescapeJava(String str)
{
if (str == null)
{
return null;
}
int sz = str.length();
StringBuffer out = new StringBuffer(sz);
StringBuffer unicode = new StringBuffer(4);
boolean hadSlash = false;
boolean inUnicode = false;
for (int i = 0; i < sz; i++)
{
char ch = str.charAt(i);
if (inUnicode)
{
// if in unicode, then we're reading unicode
// values in somehow
unicode.append(ch);
if (unicode.length() == 4)
{
// unicode now contains the four hex digits
// which represents our unicode character
try
{
int value = Integer.parseInt(unicode.toString(), 16);
out.append((char) value);
unicode.setLength(0);
inUnicode = false;
hadSlash = false;
}
catch (NumberFormatException nfe)
{
throw new ConfigurationRuntimeException("Unable to parse unicode value: " + unicode, nfe);
}
}
continue;
}
if (hadSlash)
{
// handle an escaped value
hadSlash = false;
if (ch=='\\'){
out.append('\\');
}
else if (ch=='\''){
out.append('\'');
}
else if (ch=='\"'){
out.append('"');
}
else if (ch=='r'){
out.append('\r');
}
else if (ch=='f'){
out.append('\f');
}
else if (ch=='t'){
out.append('\t');
}
else if (ch=='n'){
out.append('\n');
}
else if (ch=='b'){
out.append('\b');
}
else if (ch==getDelimiter()){
out.append('\\');
out.append(getDelimiter());
}
else if (ch=='u'){
// uh-oh, we're in unicode country....
inUnicode = true;
}
else {
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();
}
}