/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.system;
import java.util.Properties;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;
import java.io.InputStream;
import java.io.IOException;
/**
* Extension of {@link Properties} that provides automatic support for
* substitutions of variables. For example: <p>
*
* PROP1=value<br>
* PROP2=${PROP1}<br><br>
*
* When loading the above set of properties from a file, PROP2 will
* automatically substitute PROP1 for its value. <p>
*
* Additional usage requirements:
* <ul>
* <li>When properties are provided from a file or {@code InputStream} of some
* sort, properties can appear in any order and all values will be
* interpolated as expected.</li>
* <li>If a property is set using the
* {@link #setProperty(java.lang.String, java.lang.String)} method, it will
* <em>not</em> automatically cause its value to be interpolated in other
* properties where it is included.</li>
* <li>An automatically interpolated value of another property can be included
* in a property by surrounding the key of the value with the start delimiter of
* '${' and the end delimiter of '}'.</li>
* <li>Property keys can contain any characters except for the end delimiter
* of '}'. Inclusion of this character via an escape sequence is not
* supported.</li>
* </ul>
*/
public class SubstitutionProperties extends Properties {
private static final long serialVersionUID = 1L;
private static final String START_KEY = "${";
private static final String END_KEY = "}";
/**
* Creates an empty property list with no defaults.
*/
public SubstitutionProperties() {
super();
}
/**
* Creates an empty property list with a default backing of properties.
* When initialized with this constructor, any properties in the
* default backing that contain substitutable values (i.e. ${PROPNAME})
* are replaced and set with their interpolated value in the new
* {@code SubstitutionProperties} object.
*
* @param p default properties
*/
public SubstitutionProperties(Properties p) {
super(p);
replaceAll();
}
/**
* Load properties from an {@code InputStream}. The properties are loaded
* by calling {@code super.load(inStream);} Additionally, the loaded
* properties are filtered and any properties that contain substitutable
* variables (i.e. ${PROPNAME}) are replaced and set with their
* interpolated value.
*
* @param inStream the input stream to load the properties from
* @throws IOException if an error occurred while reading from the stream
* @see java.util.Properties#load(java.io.InputStream)
*/
public void load(InputStream inStream)
throws IOException {
super.load(inStream);
replaceAll();
}
/**
* Load properties from an XML file specified by the given
* {@code InputStream}. The properties are loaded
* by calling {@code super.loadFromXML(inStream);} Additionally, the loaded
* properties are filtered and any properties that contain substitutable
* variables (i.e. ${PROPNAME}) are replaced and set with their
* interpolated value.
*
* @param inStream the input stream to load the properties from
* @throws IOException if an error occurred while reading from the stream
* @see java.util.Properties#loadFromXML(java.io.InputStream)
*/
public void loadFromXML(InputStream inStream)
throws IOException {
super.load(inStream);
replaceAll();
}
/**
* Sets the given property by calling the
* {@code super.setProperty(name, value)} method. Additionally, the
* property is filtered and if it contains subtitutable variables
* (i.e. ${PROPNAME}), they are replaced and set with their interpolated
* value.
*
* @param name the name of the property
* @param value the value of the property
* @return the previous value of the property, or {@code null} if it was
* not set
* @see java.util.Properties#setProperty(java.lang.String, java.lang.String)
*/
public Object setProperty(String name, String value) {
String prev = super.getProperty(name);
super.setProperty(name, value);
replace(name, new HashSet<String>());
return prev;
}
/**
* Walks through each of the properties and replaces any instances
* of ${PROPNAME} with the value of PROPNAME if it exists. If such
* a string is found in a property and and the property to lookup does
* not exist, it is replaced with the empty string.
*/
private void replaceAll() {
for (Enumeration<?> e = this.propertyNames();
e.hasMoreElements(); ) {
String property = (String) e.nextElement();
replace(property, new HashSet<String>());
}
}
/**
* Replaces any instances of ${PROPNAME} in the property with key
* propName with the value of PROPNAME if it exists.
*
* @param propName property name to replace
* @param alreadyUsed set of properties currently in the state of
* being interpolated so that we can detect loops
* @throws IllegalStateException if a loop is detected during substitution
* or if a value contains an opening key
* for substitution '${' but not a closing
* key '}'
*/
private String replace(String propName, Set<String> beingInterpolated) {
beingInterpolated.add(propName);
String propValue = super.getProperty(propName);
if (propValue == null || propValue.equals("")) {
return "";
}
//walk through the value, building a new string and replacing
//properties as we go
StringBuilder newValue = new StringBuilder(propValue.length());
int currentIndex = 0;
for (int startIndex = propValue.indexOf(START_KEY); startIndex != -1;
startIndex = propValue.indexOf(START_KEY, currentIndex)) {
newValue.append(propValue.substring(currentIndex, startIndex));
currentIndex = propValue.indexOf(END_KEY, startIndex);
if (currentIndex != -1) {
String subPropName = propValue.substring(startIndex + 2,
currentIndex++);
if (beingInterpolated.contains(subPropName)) {
throw new IllegalStateException(
"loop detected when interpolating property : " +
propName);
}
newValue.append(replace(subPropName, beingInterpolated));
} else {
throw new IllegalArgumentException(
"illegal property name, '" + END_KEY + "' not found " +
"when interpolating property : " + propName);
}
}
newValue.append(propValue.substring(currentIndex, propValue.length()));
beingInterpolated.remove(propName);
super.setProperty(propName, newValue.toString());
return newValue.toString();
}
}