org.apache.karaf.util.Properties Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.apache.karaf.util Show documentation
Show all versions of org.apache.karaf.util Show documentation
Karaf Util contains utility classes used in different Karaf modules.
/*
* 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.karaf.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Enhancement of the standard Properties
* managing the maintain of comments, etc.
*/
public class Properties extends AbstractMap {
/** Constant for the supported comment characters.*/
private static final String COMMENT_CHARS = "#!";
/** 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'};
/**
* The default encoding (ISO-8859-1 as specified by
* http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
*/
private static final String DEFAULT_ENCODING = "ISO-8859-1";
/** Constant for the platform specific line separator.*/
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private Map storage = new LinkedHashMap();
private Map layout = new LinkedHashMap();
private List header;
private List footer;
private File location;
public Properties() {
}
public Properties(File location) throws IOException {
this.location = location;
if(location.exists())
load(location);
}
public void load(File location) throws IOException {
InputStream is = new FileInputStream(location);
try {
load(is);
} finally {
is.close();
}
}
public void load(URL location) throws IOException {
InputStream is = location.openStream();
try {
load(is);
} finally {
is.close();
}
}
public void load(InputStream is) throws IOException {
load(new InputStreamReader(is, DEFAULT_ENCODING));
}
public void load(Reader reader) throws IOException {
loadLayout(reader);
}
public void save() throws IOException {
save(this.location);
}
public void save(File location) throws IOException {
OutputStream os = new FileOutputStream(location);
try {
save(os);
} finally {
os.close();
}
}
public void save(OutputStream os) throws IOException {
save(new OutputStreamWriter(os, DEFAULT_ENCODING));
}
public void save(Writer writer) throws IOException {
saveLayout(writer);
}
@Override
public Set> entrySet() {
return storage.entrySet();
}
@Override
public String put(String key, String value) {
String old = storage.put(key, value);
if (old == null || !old.equals(value)) {
Layout l = layout.get(key);
if (l != null) {
l.clearValue();
}
}
return old;
}
@Override
public String remove(Object key) {
Layout l = layout.get(key);
if (l != null) {
l.clearValue();
}
return storage.remove(key);
}
@Override
public void clear() {
for (Layout l : layout.values()) {
l.clearValue();
}
storage.clear();
}
/**
* Return the comment header.
*
* @return the comment header
*/
public List getHeader()
{
return header;
}
/**
* Set the comment header.
*
* @param header the header to use
*/
public void setHeader(List header)
{
this.header = header;
}
/**
* Return the comment footer.
*
* @return the comment footer
*/
public List getFooter()
{
return footer;
}
/**
* Set the comment footer.
*
* @param footer the footer to use
*/
public void setFooter(List footer)
{
this.footer = footer;
}
/**
* Reads a properties file and stores its internal structure. The found
* properties will be added to the associated configuration object.
*
* @param in the reader to the properties file
* @throws java.io.IOException if an error occurs
*/
protected void loadLayout(Reader in) throws IOException
{
PropertiesReader reader = new PropertiesReader(in);
while (reader.nextProperty())
{
storage.put(reader.getPropertyName(), reader.getPropertyValue());
int idx = checkHeaderComment(reader.getCommentLines());
layout.put(reader.getPropertyName(),
new Layout(idx < reader.getCommentLines().size() ?
new ArrayList(reader.getCommentLines().subList(idx, reader.getCommentLines().size())) :
null,
new ArrayList(reader.getValueLines())));
}
footer = new ArrayList(reader.getCommentLines());
performSubstitution(storage);
}
/**
* Writes the properties file to the given writer, preserving as much of its
* structure as possible.
*
* @param out the writer
* @throws java.io.IOException if an error occurs
*/
protected void saveLayout(Writer out) throws IOException
{
PropertiesWriter writer = new PropertiesWriter(out);
if (header != null)
{
for (String s : header)
{
writer.writeln(s);
}
}
for (String key : storage.keySet())
{
Layout l = layout.get(key);
if (l != null && l.getCommentLines() != null)
{
for (String s : l.getCommentLines())
{
writer.writeln(s);
}
}
if (l != null && l.getValueLines() != null)
{
for (String s : l.getValueLines())
{
writer.writeln(s);
}
}
else
{
writer.writeProperty(key, storage.get(key));
}
}
if (footer != null)
{
for (String s : footer)
{
writer.writeln(s);
}
}
writer.flush();
}
/**
* Checks if parts of the passed in comment can be used as header comment.
* This method checks whether a header comment can be defined (i.e. whether
* this is the first comment in the loaded file). If this is the case, it is
* searched for the lates blank line. This line will mark the end of the
* header comment. The return value is the index of the first line in the
* passed in list, which does not belong to the header comment.
*
* @param commentLines the comment lines
* @return the index of the next line after the header comment
*/
private int checkHeaderComment(List commentLines)
{
if (getHeader() == null && layout.isEmpty())
{
// This is the first comment. Search for blank lines.
int index = commentLines.size() - 1;
while (index >= 0 && commentLines.get(index).length() > 0)
{
index--;
}
setHeader(new ArrayList(commentLines.subList(0, index + 1)));
return index + 1;
}
else
{
return 0;
}
}
/**
* 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
*/
static boolean isCommentLine(String line) {
String s = line.trim();
// blank lines are also treated as comment lines
return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
}
/**
* Checks if the value is in the given array.
*
* The method returns false
if a null
array is passed in.
*
* @param array the array to search through
* @param valueToFind the value to find
* @return true
if the array contains the object
*/
public static boolean contains(char[] array, char valueToFind) {
if (array == null) {
return false;
}
for (int i = 0; i < array.length; i++) {
if (valueToFind == array[i]) {
return true;
}
}
return false;
}
private static final char ESCAPE_CHAR = '\\';
private static final String DELIM_START = "${";
private static final String DELIM_STOP = "}";
private static final String CHECKSUM_SUFFIX = ".checksum";
/**
* Perform substitution on a property set
*
* @param properties the property set to perform substitution on
*/
public static void performSubstitution(Map properties)
{
for (String name : properties.keySet())
{
String value = properties.get(name);
properties.put(name, substVars(value, name, null, properties));
}
}
/**
*
* This method performs property variable substitution on the
* specified value. If the specified value contains the syntax
* ${<prop-name>}, where <prop-name>
* refers to either a configuration property or a system property,
* then the corresponding property value is substituted for the variable
* placeholder. Multiple variable placeholders may exist in the
* specified value as well as nested variable placeholders, which
* are substituted from inner most to outer most. Configuration
* properties override system properties.
*
* @param val The string on which to perform property substitution.
* @param currentKey The key of the property being evaluated used to
* detect cycles.
* @param cycleMap Map of variable references used to detect nested cycles.
* @param configProps Set of configuration properties.
* @return The value of the specified string after system property substitution.
* @throws IllegalArgumentException If there was a syntax error in the
* property placeholder syntax or a recursive variable reference.
**/
public static String substVars(String val, String currentKey, Map cycleMap, Map configProps)
throws IllegalArgumentException
{
if (cycleMap == null)
{
cycleMap = new HashMap();
}
// Put the current key in the cycle map.
cycleMap.put(currentKey, currentKey);
// Assume we have a value that is something like:
// "leading ${foo.${bar}} middle ${baz} trailing"
// Find the first ending '}' variable delimiter, which
// will correspond to the first deepest nested variable
// placeholder.
int stopDelim = val.indexOf(DELIM_STOP);
while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR)
{
stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
}
// Find the matching starting "${" variable delimiter
// by looping until we find a start delimiter that is
// greater than the stop delimiter we have found.
int startDelim = val.indexOf(DELIM_START);
while (stopDelim >= 0)
{
int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
if ((idx < 0) || (idx > stopDelim))
{
break;
}
else if (idx < stopDelim)
{
startDelim = idx;
}
}
// If we do not have a start or stop delimiter, then just
// return the existing value.
if ((startDelim < 0) || (stopDelim < 0))
{
return unescape(val);
}
// At this point, we have found a variable placeholder so
// we must perform a variable substitution on it.
// Using the start and stop delimiter indices, extract
// the first, deepest nested variable placeholder.
String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
// Verify that this is not a recursive variable reference.
if (cycleMap.get(variable) != null)
{
throw new IllegalArgumentException("recursive variable reference: " + variable);
}
// Get the value of the deepest nested variable placeholder.
// Try to configuration properties first.
String substValue = (String) ((configProps != null) ? configProps.get(variable) : null);
if (substValue == null)
{
// Ignore unknown property values.
substValue = variable.length() > 0 ? System.getProperty(variable, "") : "";
}
// Remove the found variable from the cycle map, since
// it may appear more than once in the value and we don't
// want such situations to appear as a recursive reference.
cycleMap.remove(variable);
// Append the leading characters, the substituted value of
// the variable, and the trailing characters to get the new
// value.
val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length(), val.length());
// Now perform substitution again, since there could still
// be substitutions to make.
val = substVars(val, currentKey, cycleMap, configProps);
// Remove escape characters preceding {, } and \
val = unescape(val);
// Return the value.
return val;
}
private static String unescape(String val) {
int escape = val.indexOf(ESCAPE_CHAR);
while (escape >= 0 && escape < val.length() - 1)
{
char c = val.charAt(escape + 1);
if (c == '{' || c == '}' || c == ESCAPE_CHAR)
{
val = val.substring(0, escape) + val.substring(escape + 1);
}
escape = val.indexOf(ESCAPE_CHAR, escape + 1);
}
return val;
}
/**
* 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
{
/** Stores the comment lines for the currently processed property.*/
private List commentLines;
/** Stores the value lines for the currently processed property.*/
private List valueLines;
/** Stores the name of the last read property.*/
private String propertyName;
/** Stores the value of the last read property.*/
private String propertyValue;
/**
* Creates a new instance of PropertiesReader
and sets
* the underlaying reader and the list delimiter.
*
* @param reader the reader
*/
public PropertiesReader(Reader reader)
{
super(reader);
commentLines = new ArrayList();
valueLines = new ArrayList();
}
/**
* 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 (<name>
* = <value>
)
*
* @return A string containing a property value or null
*
* @throws java.io.IOException in case of an I/O error
*/
public String readProperty() throws IOException
{
commentLines.clear();
valueLines.clear();
StringBuffer buffer = new StringBuffer();
while (true)
{
String line = readLine();
if (line == null)
{
// EOF
return null;
}
if (isCommentLine(line))
{
commentLines.add(line);
continue;
}
valueLines.add(line);
line = line.trim();
if (checkCombineLines(line))
{
line = line.substring(0, line.length() - 1);
buffer.append(line);
}
else
{
buffer.append(line);
break;
}
}
return buffer.toString();
}
/**
* 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 (false) or whether further properties are
* available (true).
*
* @return a flag if further properties are available
* @throws java.io.IOException if an error occurs
*/
public boolean nextProperty() throws IOException
{
String line = readProperty();
if (line == null)
{
return false; // EOF
}
// parse the line
String[] property = parseProperty(line);
propertyName = StringEscapeUtils.unescapeJava(property[0]);
propertyValue = StringEscapeUtils.unescapeJava(property[1]);
return true;
}
/**
* Returns the comment lines that have been read for the last property.
*
* @return the comment lines for the last property returned by
* readProperty()
*/
public List getCommentLines()
{
return commentLines;
}
/**
* Returns the value lines that have been read for the last property.
*
* @return the raw value lines for the last property returned by
* readProperty()
*/
public List getValueLines()
{
return valueLines;
}
/**
* Returns the name of the last read property. This method can be called
* after {@link #nextProperty()}
was invoked and its
* return value was true.
*
* @return the name of the last read property
*/
public String getPropertyName()
{
return propertyName;
}
/**
* Returns the value of the last read property. This method can be
* called after {@link #nextProperty()}
was invoked and
* its return value was true.
*
* @return the value of the last read property
*/
public String getPropertyValue()
{
return propertyValue;
}
/**
* 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
*/
private static boolean checkCombineLines(String line)
{
int bsCount = 0;
for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
{
bsCount++;
}
return bsCount % 2 != 0;
}
/**
* Parse a property line and return the key and the value in an array.
*
* @param line the line to parse
* @return an array with the property's key and value
*/
private static String[] parseProperty(String line)
{
// sorry for this spaghetti code, please replace it as soon as
// possible with a regexp when the Java 1.3 requirement is dropped
String[] result = new String[2];
StringBuffer key = new StringBuffer();
StringBuffer value = new StringBuffer();
// state of the automaton:
// 0: key parsing
// 1: antislash found while parsing the key
// 2: separator crossing
// 3: value parsing
int state = 0;
for (int pos = 0; pos < line.length(); pos++)
{
char c = line.charAt(pos);
switch (state)
{
case 0:
if (c == '\\')
{
state = 1;
}
else if (contains(WHITE_SPACE, c))
{
// switch to the separator crossing state
state = 2;
}
else if (contains(SEPARATORS, c))
{
// switch to the value parsing state
state = 3;
}
else
{
key.append(c);
}
break;
case 1:
if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c))
{
// this is an escaped separator or white space
key.append(c);
}
else
{
// another escaped character, the '\' is preserved
key.append('\\');
key.append(c);
}
// return to the key parsing state
state = 0;
break;
case 2:
if (contains(WHITE_SPACE, c))
{
// do nothing, eat all white spaces
state = 2;
}
else if (contains(SEPARATORS, c))
{
// switch to the value parsing state
state = 3;
}
else
{
// any other character indicates we encoutered the beginning of the value
value.append(c);
// switch to the value parsing state
state = 3;
}
break;
case 3:
value.append(c);
break;
}
}
result[0] = key.toString().trim();
result[1] = value.toString().trim();
return result;
}
} // 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)
{
super(writer);
}
/**
* Writes the given property and its value.
*
* @param key the property key
* @param value the property value
* @throws java.io.IOException if an error occurs
*/
public void writeProperty(String key, String value) throws IOException
{
write(escapeKey(key));
write(" = ");
write(StringEscapeUtils.escapeJava(value));
writeln(null);
}
/**
* Escape the separators in the key.
*
* @param key the key
* @return the escaped key
*/
private String escapeKey(String key)
{
StringBuffer newkey = new StringBuffer();
for (int i = 0; i < key.length(); i++)
{
char c = key.charAt(i);
if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c))
{
// escape the separator
newkey.append('\\');
newkey.append(c);
}
else
{
newkey.append(c);
}
}
return newkey.toString();
}
/**
* Helper method for writing a line with the platform specific line
* ending.
*
* @param s the content of the line (may be null)
* @throws java.io.IOException if an error occurs
*/
public void writeln(String s) throws IOException
{
if (s != null)
{
write(s);
}
write(LINE_SEPARATOR);
}
} // class PropertiesWriter
/**
* TODO
*/
protected static class Layout {
private List commentLines;
private List valueLines;
public Layout() {
}
public Layout(List commentLines, List valueLines) {
this.commentLines = commentLines;
this.valueLines = valueLines;
}
public List getCommentLines() {
return commentLines;
}
public void setCommentLines(List commentLines) {
this.commentLines = commentLines;
}
public List getValueLines() {
return valueLines;
}
public void setValueLines(List valueLines) {
this.valueLines = valueLines;
}
public void clearValue() {
this.valueLines = null;
}
} // class Layout
}