org.apache.velocity.util.ExtProperties Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.velocity.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
/**
* This class extends normal Java properties by adding the possibility
* to use the same key many times concatenating the value strings
* instead of overwriting them.
*
* Please consider using the PropertiesConfiguration
class in
* Commons-Configuration as soon as it is released.
*
* The Extended Properties syntax is explained here:
*
*
* -
* Each property has the syntax
key = value
*
* -
* The key may use any character but the equal sign '='.
*
* -
* value may be separated on different lines if a backslash
* is placed at the end of the line that continues below.
*
* -
* If value is a list of strings, each token is separated
* by a comma ','.
*
* -
* Commas in each token are escaped placing a backslash right before
* the comma.
*
* -
* Backslashes are escaped by using two consecutive backslashes i.e. \\
*
* -
* If a key is used more than once, the values are appended
* as if they were on the same line separated with commas.
*
* -
* Blank lines and lines starting with character '#' are skipped.
*
* -
* 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.
*
*
*
*
* Here is an example of a valid extended properties file:
*
*
* # 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.escaped = Hi\, what'up?
*
*
* NOTE: this class has not been written for
* performance nor low memory usage. In fact, it's way slower than it
* could be and generates too much memory garbage. But since
* performance is not an issue during intialization (and there is not
* much time to improve it), I wrote it this way. If you don't like
* it, go ahead and tune it up!
*
* This class is a clone of org.apache.commons.collections.ExtendedProperties
* (which has been removed from commons-collections-4.x)
*
* @since 2.0
* @version $Revision: $
* @version $Id: ExtProperties.java$
*
* @author Stefano Mazzocchi
* @author Jon S. Stevens
* @author Dave Bryson
* @author Jason van Zyl
* @author Geir Magnusson Jr.
* @author Leon Messerschmidt
* @author Kent Johnson
* @author Daniel Rall
* @author Ilkka Priha
* @author Janek Bogucki
* @author Mohan Kishore
* @author Stephen Colebourne
* @author Shinobu Kawai
* @author Henning P. Schmiedehausen
* @author Claude Brisson
*/
@SuppressWarnings("deprecation")
public class ExtProperties extends DeprecationAwareExtProperties
{
/**
* Default configurations repository.
*/
private ExtProperties defaults;
/**
* The file connected to this repository (holding comments and
* such).
*
* @serial
*/
protected String file;
/**
* Base path of the configuration file used to create
* this ExtProperties object.
*/
protected String basePath;
/**
* File separator.
*/
protected String fileSeparator;
{
try
{
fileSeparator = (String) AccessController.doPrivileged(
(PrivilegedAction) () -> System.getProperty("file.separator"));
}
catch (SecurityException ex)
{
fileSeparator = File.separator;
}
}
/**
* Has this configuration been initialized.
*/
protected boolean isInitialized = false;
/**
* This is the name of the property that can point to other
* properties file for including other properties files.
*/
protected static String include = "include";
/**
* These are the keys in the order they listed
* in the configuration file. This is useful when
* you wish to perform operations with configuration
* information in a particular order.
*/
protected ArrayList keysAsListed = new ArrayList<>();
protected final static String START_TOKEN="${";
protected final static String END_TOKEN="}";
/**
* Interpolate key names to handle ${key} stuff
*
* @param base string to interpolate
* @return returns the key name with the ${key} substituted
*/
protected String interpolate(String base)
{
// COPIED from [configuration] 2003-12-29
return (interpolateHelper(base, null));
}
/**
* Recursive handler for multiple levels of interpolation.
*
* When called the first time, priorVariables should be null.
*
* @param base string with the ${key} variables
* @param priorVariables serves two purposes: to allow checking for
* loops, and creating a meaningful exception message should a loop
* occur. It's 0'th element will be set to the value of base from
* the first call. All subsequent interpolated variables are added
* afterward.
*
* @return the string with the interpolation taken care of
*/
protected String interpolateHelper(String base, List priorVariables)
{
// COPIED from [configuration] 2003-12-29
if (base == null)
{
return null;
}
// on the first call initialize priorVariables
// and add base as the first element
if (priorVariables == null)
{
priorVariables = new ArrayList<>();
priorVariables.add(base);
}
int begin = -1;
int end = -1;
int prec = 0 - END_TOKEN.length();
String variable = null;
StringBuilder result = new StringBuilder();
// FIXME: we should probably allow the escaping of the start token
while (((begin = base.indexOf(START_TOKEN, prec + END_TOKEN.length())) > -1)
&& ((end = base.indexOf(END_TOKEN, begin)) > -1))
{
result.append(base.substring(prec + END_TOKEN.length(), begin));
variable = base.substring(begin + START_TOKEN.length(), end);
// if we've got a loop, create a useful exception message and throw
if (priorVariables.contains(variable))
{
String initialBase = priorVariables.remove(0).toString();
priorVariables.add(variable);
StringBuilder priorVariableSb = new StringBuilder();
// create a nice trace of interpolated variables like so:
// var1->var2->var3
for (Iterator it = priorVariables.iterator(); it.hasNext();)
{
priorVariableSb.append(it.next());
if (it.hasNext())
{
priorVariableSb.append("->");
}
}
throw new IllegalStateException(
"infinite loop in property interpolation of " + initialBase + ": " + priorVariableSb.toString());
}
// otherwise, add this variable to the interpolation list.
else
{
priorVariables.add(variable);
}
//QUESTION: getProperty or getPropertyDirect
Object value = getProperty(variable);
if (value != null)
{
result.append(interpolateHelper(value.toString(), priorVariables));
// pop the interpolated variable off the stack
// this maintains priorVariables correctness for
// properties with multiple interpolations, e.g.
// prop.name=${some.other.prop1}/blahblah/${some.other.prop2}
priorVariables.remove(priorVariables.size() - 1);
}
else if (defaults != null && defaults.getString(variable, null) != null)
{
result.append(defaults.getString(variable));
}
else
{
//variable not defined - so put it back in the value
result.append(START_TOKEN).append(variable).append(END_TOKEN);
}
prec = end;
}
result.append(base.substring(prec + END_TOKEN.length(), base.length()));
return result.toString();
}
/**
* Inserts a backslash before every comma and backslash.
*/
private static String escape(String s)
{
StringBuilder buf = new StringBuilder(s);
for (int i = 0; i < buf.length(); i++)
{
char c = buf.charAt(i);
if (c == ',' || c == '\\')
{
buf.insert(i, '\\');
i++;
}
}
return buf.toString();
}
/**
* Removes a backslash from every pair of backslashes.
*/
private static String unescape(String s)
{
StringBuilder buf = new StringBuilder(s);
for (int i = 0; i < buf.length() - 1; i++)
{
char c1 = buf.charAt(i);
char c2 = buf.charAt(i + 1);
if (c1 == '\\' && c2 == '\\')
{
buf.deleteCharAt(i);
}
}
return buf.toString();
}
/**
* Counts the number of successive times 'ch' appears in the
* 'line' before the position indicated by the 'index'.
*/
private static int countPreceding(String line, int index, char ch)
{
int i;
for (i = index - 1; i >= 0; i--)
{
if (line.charAt(i) != ch)
{
break;
}
}
return index - 1 - i;
}
/**
* Checks if the line ends with odd number of backslashes
*/
private static boolean endsWithSlash(String line)
{
if (!line.endsWith("\\"))
{
return false;
}
return (countPreceding(line, line.length() - 1, '\\') % 2 == 0);
}
/**
* 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.
*/
static class PropertiesReader extends LineNumberReader
{
/**
* Constructor.
*
* @param reader A Reader.
*/
public PropertiesReader(Reader reader)
{
super(reader);
}
/**
* Read a property.
*
* @return a String property
* @throws IOException if there is difficulty reading the source.
*/
public String readProperty() throws IOException
{
StringBuilder buffer = new StringBuilder();
String line = readLine();
while (line != null)
{
line = line.trim();
if ((line.length() != 0) && (line.charAt(0) != '#'))
{
if (endsWithSlash(line))
{
line = line.substring(0, line.length() - 1);
buffer.append(line);
}
else
{
buffer.append(line);
return buffer.toString(); // normal method end
}
}
line = readLine();
}
return null; // EOF reached
}
}
/**
* This class divides into tokens a property value. Token
* separator is "," but commas into the property value are escaped
* using the backslash in front.
*/
static class PropertiesTokenizer extends StringTokenizer
{
/**
* The property delimiter used while parsing (a comma).
*/
static final String DELIMITER = ",";
/**
* Constructor.
*
* @param string A String.
*/
public PropertiesTokenizer(String string)
{
super(string, DELIMITER);
}
/**
* Check whether the object has more tokens.
*
* @return True if the object has more tokens.
*/
@Override
public boolean hasMoreTokens()
{
return super.hasMoreTokens();
}
/**
* Get next token.
*
* @return A String.
*/
@Override
public String nextToken()
{
StringBuilder buffer = new StringBuilder();
while (hasMoreTokens())
{
String token = super.nextToken();
if (endsWithSlash(token))
{
buffer.append(token.substring(0, token.length() - 1));
buffer.append(DELIMITER);
}
else
{
buffer.append(token);
break;
}
}
return buffer.toString().trim();
}
}
/**
* Creates an empty extended properties object.
*/
public ExtProperties()
{
super();
}
/**
* Creates and loads the extended properties from the specified file.
*
* @param file the filename to load
* @throws IOException if a file error occurs
*/
public ExtProperties(String file) throws IOException
{
this(file, null);
}
/**
* Creates and loads the extended properties from the specified file.
*
* @param file the filename to load
* @param defaultFile a second filename to load default values from
* @throws IOException if a file error occurs
*/
public ExtProperties(String file, String defaultFile) throws IOException
{
this.file = file;
basePath = new File(file).getAbsolutePath();
basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1);
FileInputStream in = null;
try
{
in = new FileInputStream(file);
this.load(in);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (IOException ex) {}
}
if (defaultFile != null)
{
defaults = new ExtProperties(defaultFile);
}
}
/**
* Indicate to client code whether property
* resources have been initialized or not.
* @return initialization status
*/
public boolean isInitialized()
{
return isInitialized;
}
/**
* Gets the property value for including other properties files.
* By default it is "include".
*
* @return A String.
*/
public String getInclude()
{
return include;
}
/**
* Sets the property value for including other properties files.
* By default it is "include".
*
* @param inc A String.
*/
public void setInclude(String inc)
{
include = inc;
}
/**
* Load the properties from the given input stream.
*
* @param input the InputStream to load from
* @throws IOException if an IO error occurs
*/
public void load(InputStream input) throws IOException
{
load(input, null);
}
/**
* Load the properties from the given input stream
* and using the specified encoding.
*
* @param input the InputStream to load from
* @param enc the encoding to use
* @throws IOException if an IO error occurs
*/
public synchronized void load(InputStream input, String enc) throws IOException
{
PropertiesReader reader = null;
if (enc != null)
{
try
{
reader = new PropertiesReader(new InputStreamReader(input, enc));
}
catch (UnsupportedEncodingException ex)
{
// Another try coming up....
}
}
// fall back to UTF-8
if (reader == null)
{
reader = new PropertiesReader(new InputStreamReader(input, StandardCharsets.UTF_8));
}
try
{
while (true)
{
String line = reader.readProperty();
if (line == null)
{
return; // EOF
}
int equalSign = line.indexOf('=');
if (equalSign > 0)
{
String key = line.substring(0, equalSign).trim();
String value = line.substring(equalSign + 1).trim();
// Configure produces lines like this ... just ignore them
if ("".equals(value))
{
continue;
}
if (getInclude() != null && key.equalsIgnoreCase(getInclude()))
{
// Recursively load properties files.
File file = null;
if (value.startsWith(fileSeparator))
{
// We have an absolute path so we'll use this
file = new File(value);
}
else
{
// We have a relative path, and we have two
// possible forms here. If we have the "./" form
// then just strip that off first before continuing.
if (value.startsWith("." + fileSeparator))
{
value = value.substring(2);
}
file = new File(basePath + value);
}
if (file.exists() && file.canRead())
{
load(new FileInputStream(file));
}
}
else
{
addProperty(key, value);
}
}
}
}
finally
{
// Loading is initializing
isInitialized = true;
}
}
/**
* Gets a property from the configuration.
*
* @param key property to retrieve
* @return value as object. Will return user value if exists,
* if not then default value if exists, otherwise null
*/
public Object getProperty(String key)
{
// first, try to get from the 'user value' store
Object obj = this.get(key);
if (obj == null)
{
// if there isn't a value there, get it from the
// defaults if we have them
if (defaults != null)
{
obj = defaults.get(key);
}
}
return obj;
}
/**
* Add a property to the configuration. If it already
* exists then the value stated here will be added
* to the configuration entry. For example, if
*
* resource.loaders = file
*
* is already present in the configuration and you
*
* addProperty("resource.loaders", "classpath")
*
* Then you will end up with a Vector like the
* following:
*
* ["file", "classpath"]
*
* @param key the key to add
* @param value the value to add
*/
public void addProperty(String key, Object value)
{
if (value instanceof String)
{
String str = (String) value;
if (str.indexOf(PropertiesTokenizer.DELIMITER) > 0)
{
// token contains commas, so must be split apart then added
PropertiesTokenizer tokenizer = new PropertiesTokenizer(str);
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
addPropertyInternal(key, unescape(token));
}
}
else
{
// token contains no commas, so can be simply added
addPropertyInternal(key, unescape(str));
}
}
else
{
addPropertyInternal(key, value);
}
// Adding a property connotes initialization
isInitialized = true;
}
/**
* Adds a key/value pair to the map. This routine does
* no magic morphing. It ensures the keylist is maintained
*
* @param key the key to store at
* @param value the decoded object to store
*/
private void addPropertyDirect(String key, Object value)
{
// safety check
if (!containsKey(key))
{
keysAsListed.add(translateKey(key));
}
put(key, value);
}
/**
* Adds a decoded property to the map w/o checking for commas - used
* internally when a property has been broken up into
* strings that could contain escaped commas to prevent
* the inadvertent vectorization.
*
* Thanks to Leon Messerschmidt for this one.
*
* @param key the key to store at
* @param value the decoded object to store
*/
private void addPropertyInternal(String key, Object value)
{
Object current = this.get(key);
if (current instanceof String)
{
// one object already in map - convert it to a vector
List