blob: f18144cc3e45c97f34e7423d4046c5c533b50a9c [file] [log] [blame]
/*
* 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.sling.launcher.app;
import static org.apache.felix.framework.util.FelixConstants.EMBEDDED_EXECUTION_PROP;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;
import org.apache.felix.framework.Felix;
import org.apache.felix.framework.Logger;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.url.URLConstants;
import org.osgi.service.url.URLStreamHandlerService;
/**
* The <code>Sling</code> serves as the starting point for Sling.
* <ul>
* <li>The {@link #Sling(Logger, ResourceProvider, Map)} method launches
* Apache <code>Felix</code> as the OSGi framework implementation we use.
* </ul>
* <p>
* <b>Launch Configuration</b>
* <p>
* The Apache <code>Felix</code> framework requires configuration parameters
* to be specified for startup. This servlet builds the list of parameters from
* three locations:
* <ol>
* <li>The <code>com/day/osgi/servlet/Sling.properties</code> is read from
* the servlet class path. This properties file contains default settings.</li>
* <li>Extensions of this servlet may provide additional properties to be
* loaded overwriting the {@link #loadPropertiesOverride(Map)} method.
* <li>Finally, web application init parameters are added to the properties and
* may overwrite existing properties of the same name(s).
* </ol>
* <p>
* After loading all properties, variable substitution takes place on the
* property values. A variable is indicated as <code>${&lt;prop-name&gt;}</code>
* where <code>&lt;prop-name&gt;</code> is the name of a system or
* configuration property (configuration properties override system properties).
* Variables may be nested and are resolved from inner-most to outer-most. For
* example, the property value <code>${outer-${inner}}</code> is resolved by
* first resolving <code>${inner}</code> and then resolving the property whose
* name is the catenation of <code>outer-</code> and the result of resolving
* <code>${inner}</code>.
* <p>
*/
public class Sling implements BundleActivator {
/** Pseduo class version ID to keep the IDE quite. */
private static final long serialVersionUID = 1L;
/**
* The name of the configuration property defining the Sling home directory
* (value is "sling.home"). This is a Platform file system directory below
* which all runtime data, such as the Felix bundle archives, logfiles, CRX
* repository, etc., is located.
* <p>
* The value of this property, if not set as a system property defaults to
* the <i>sling</i> directory in the current working directory.
*
* @see #SLING_HOME_URL
*/
public static final String SLING_HOME = "sling.home";
/**
* The name of the configuration property defining the Sling home directory
* as an URL (value is "sling.home.url").
* <p>
* The value of this property is assigned the value of
* <code>new File(${sling.home}).toURI().toString()</code> before
* resolving the property variables.
*
* @see #SLING_HOME
*/
public static final String SLING_HOME_URL = "sling.home.url";
/**
* The name of the configuration property defining a properties file
* defining a list of bundles, which are installed into the framework when
* it has been launched (value is "org.apache.osgi.bundles").
* <p>
* This configuration property is generally set in the web application
* configuration and may be referenced in all property files (default, user
* supplied and web application parameters) used to build the framework
* configuration.
*/
public static final String OSGI_FRAMEWORK_BUNDLES = "org.apache.osgi.bundles";
/**
* The property to be set to ignore the system properties when building the
* Felix framework properties (value is "sling.ignoreSystemProperties"). If
* this is property is set to <code>true</code> (case does not matter),
* the system properties will not be used by
* {@link #loadConfigProperties(Map)}.
*/
public static final String SLING_IGNORE_SYSTEM_PROPERTIES = "sling.ignoreSystemProperties";
/**
* The name of the default launcher properties file to setup the environment
* for the <code>Felix</code> framework (value is "sling.properties").
* <p>
* Extensions of this class may overwrite some or all properties in this
* file through Web Application parameters or other properties files.
*/
public static final String CONFIG_PROPERTIES = "sling.properties";
public static final String PROP_SYSTEM_PACKAGES = "org.apache.sling.launcher.system.packages";
/**
* The simple logger to log messages during startup and shutdown to
*/
protected final Logger logger;
private ResourceProvider resourceProvider;
/**
* The <code>Felix</code> instance loaded on {@link #init()} and stopped
* on {@link #destroy()}.
*/
private Felix felix;
/**
* The <code>BundleContext</code> of the OSGi framework system bundle.
* This is used for service registration and service access to get at the
* delegatee servlet.
*/
private BundleContext bundleContext;
/**
* Initializes this servlet by loading the framework configuration
* properties, starting the OSGi framework (Apache Felix) and exposing the
* system bundle context and the <code>Felix</code> instance as servlet
* context attributes.
*
* @throws BundleException if the framework cannot be initialized.
*/
public Sling(Logger logger, ResourceProvider resourceProvider,
Map<String, String> propOverwrite) throws BundleException {
this.logger = logger;
this.resourceProvider = resourceProvider;
this.logger.log(Logger.LOG_INFO, "Starting Sling");
// read the default parameters
Map<String, String> props = this.loadConfigProperties(propOverwrite);
// check for auto-start bundles
this.setInstallBundles(props);
// ensure execution environment
this.setExecutionEnvironment(props);
// make sure Felix does not exit the VM when terminating ...
props.put(EMBEDDED_EXECUTION_PROP, "true");
// the custom activator list just contains this servlet
List<BundleActivator> activators = new ArrayList<BundleActivator>();
activators.add(this);
activators.add(new BootstrapInstaller(logger, resourceProvider));
// create the framework and start it
Felix tmpFelix = new Felix(logger, props, activators);
tmpFelix.start();
// only assign field if start succeeds
this.felix = tmpFelix;
// log sucess message
this.logger.log(Logger.LOG_INFO, "Sling started");
}
/**
* Destroys this servlet by shutting down the OSGi framework and hence the
* delegatee servlet if one is set at all.
*/
public final void destroy() {
// shutdown the Felix container
if (felix != null) {
logger.log(Logger.LOG_INFO, "Shutting down Sling");
felix.stopAndWait();
logger.log(Logger.LOG_INFO, "Sling stopped");
felix = null;
}
}
// ---------- BundleActivator ----------------------------------------------
/**
* Called when the OSGi framework is being started. This implementation
* registers as a service listener for the
* <code>javax.servlet.Servlet</code> class and calls the
* {@link #doStartBundle()} method for implementations to execute more
* startup tasks. Additionally the <code>context</code> URL protocol
* handler is registered.
*
* @param bundleContext The <code>BundleContext</code> of the system
* bundle of the OSGi framework.
* @throws Exception May be thrown if the {@link #doStartBundle()} throws.
*/
public final void start(BundleContext bundleContext) throws Exception {
this.bundleContext = bundleContext;
// register the context URL handler
Hashtable<String, Object> props = new Hashtable<String, Object>();
props.put(URLConstants.URL_HANDLER_PROTOCOL, new String[] { "context" });
ContextProtocolHandler contextHandler = new ContextProtocolHandler(
this.resourceProvider);
bundleContext.registerService(URLStreamHandlerService.class.getName(),
contextHandler, props);
// execute optional bundle startup tasks of an extension
this.doStartBundle();
}
/**
* Called when the OSGi framework is being shut down. This implementation
* first calls the {@link #doStopBundle()} method method before
* unregistering as a service listener and ungetting an servlet delegatee if
* one has been acquired.
*
* @param bundleContext The <code>BundleContext</code> of the system
* bundle of the OSGi framework.
*/
public final void stop(BundleContext bundleContext) {
// execute optional bundle stop tasks of an extension
try {
this.doStopBundle();
} catch (Exception e) {
this.logger.log(Logger.LOG_ERROR, "Unexpected exception caught", e);
}
// drop bundle context reference
this.bundleContext = null;
}
// ---------- Configuration Loading ----------------------------------------
/**
* Loads the configuration properties in the configuration property file
* associated with the framework installation; these properties are
* accessible to the framework and to bundles and are intended for
* configuration purposes. By default, the configuration property file is
* located in the <tt>conf/</tt> directory of the Felix installation
* directory and is called "<tt>config.properties</tt>". The
* installation directory of Felix is assumed to be the parent directory of
* the <tt>felix.jar</tt> file as found on the system class path property.
* The precise file from which to load configuration properties can be set
* by initializing the "<tt>felix.config.properties</tt>" system
* property to an arbitrary URL.
*
* @return A <tt>Properties</tt> instance or <tt>null</tt> if there was
* an error.
*/
private Map<String, String> loadConfigProperties(
Map<String, String> propOverwrite) throws BundleException {
// The config properties file is either specified by a system
// property or it is in the same directory as the Felix JAR file.
// Try to load it from one of these places.
Map<String, String> props = new HashMap<String, String>();
// Read the properties file.
this.load(props, CONFIG_PROPERTIES);
// resolve inclusions (and remove property)
this.loadIncludes(props, null);
// overwrite default properties with initial overwrites
if (propOverwrite != null) {
props.putAll(propOverwrite);
}
// check whether sling.home is overwritten by system property
String slingHome = System.getProperty(SLING_HOME);
if (slingHome == null || slingHome.length() == 0) {
// no system property, ensure default setting
slingHome = props.get(SLING_HOME);
if (slingHome == null || slingHome.length() == 0) {
slingHome = "sling";
this.logger.log(Logger.LOG_INFO,
"sling.home is not defined. Using '" + slingHome + "'");
}
}
// resolve variables and ensure sling.home is an absolute path
slingHome = substVars(slingHome, SLING_HOME, null, props);
File slingHomeFile = new File(slingHome).getAbsoluteFile();
slingHome = slingHomeFile.getAbsolutePath();
// overlay with ${sling.home}/sling.properties
this.logger.log(Logger.LOG_INFO, "Starting sling in " + slingHome);
File propFile = new File(slingHome, CONFIG_PROPERTIES);
this.load(props, propFile);
// create a copy of the properties to perform variable substitution
Map<String, String> origProps = props;
props = new HashMap<String, String>();
props.putAll(origProps);
// check system properties for any overrides (except sling.home !)
String ignoreSystemProperties = props.get(SLING_IGNORE_SYSTEM_PROPERTIES);
if (!"true".equalsIgnoreCase(ignoreSystemProperties)) {
for (String name : props.keySet()) {
String sysProp = System.getProperty(name);
if (sysProp != null) {
props.put(name, sysProp);
}
}
}
// resolve inclusions again
this.loadIncludes(props, slingHome);
// overwrite properties, this is not persisted as such
this.loadPropertiesOverride(props);
// resolve boot delegation and system packages
this.resolve(props, "org.osgi.framework.bootdelegation",
"sling.bootdelegation.");
this.resolve(props, "org.osgi.framework.system.packages",
"sling.system.packages.");
// reset back the sling home property
// might have been overwritten by system properties, included
// files or the sling.properties file
origProps.put(SLING_HOME, slingHome);
props.put(SLING_HOME, slingHome);
props.put(SLING_HOME_URL, slingHomeFile.toURI().toString());
// Perform variable substitution for system properties.
for (Entry<String, String> entry : props.entrySet()) {
entry.setValue(substVars(entry.getValue(), entry.getKey(), null,
props));
}
// look for context:/ URLs to substitute
for (Entry<String, String> entry : props.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if (value != null && value.startsWith("context:/")) {
String path = value.substring("context:/".length() - 1);
InputStream src = this.resourceProvider.getResourceAsStream(path);
if (src != null) {
File target = new File(slingHome, path);
OutputStream dest = null;
try {
// only copy file if not existing
if (!target.exists()) {
target.getParentFile().mkdirs();
dest = new FileOutputStream(target);
byte[] buf = new byte[2048];
int rd;
while ((rd = src.read(buf)) >= 0) {
dest.write(buf, 0, rd);
}
}
// after copying replace property and add url property
entry.setValue(target.getAbsolutePath());
// also set the new property on the unsubstituted props
origProps.put(name, "${sling.home}" + path);
} catch (IOException ioe) {
this.logger.log(Logger.LOG_ERROR, "Cannot copy file "
+ value + " to " + target, ioe);
} finally {
if (dest != null) {
try {
dest.close();
} catch (IOException ignore) {
}
}
try {
src.close();
} catch (IOException ignore) {
}
}
}
}
}
// write the unsubstituted properties back to the overlay file
OutputStream os = null;
try {
// ensure parent folder(s)
propFile.getParentFile().mkdirs();
os = new FileOutputStream(propFile);
// copy the values into a temporary properties structure to store
Properties tmp = new Properties();
tmp.putAll(origProps);
tmp.store(os, "Overlay properties for configuration");
} catch (Exception ex) {
this.logger.log(Logger.LOG_ERROR,
"Error loading overlay properties from " + propFile, ex);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ex2) {
// Nothing we can do.
}
}
}
return props;
}
/**
* Scans the properties for any properties starting with the given
* <code>prefix</code> (e.g. <code>sling.bootdelegation.</code>).
* <ol>
* <li>Each such property is checked, whether it actually starts with
* <code>prefix<b>class.</b></code>. If so, the rest of the property
* name is assumed to be a fully qualified class name which is check,
* whether it is visible. If so, the value of the property is appended to
* the value of the <code>osgiProp</code>. If the class cannot be loaded,
* the property is ignored.
* <li>Otherwise, if the property does not contain a fully qualified class
* name, the value of the property is simply appended to the
* <code>osgiProp</code>.
* </ol>
*
* @param props The <code>Properties</code> to be scanned.
* @param osgiProp The name of the property in <code>props</code> to which
* any matching property values are appended.
* @param prefix The prefix of properties to handle.
*/
private void resolve(Map<String, String> props, String osgiProp,
String prefix) {
final String propVal = props.get(osgiProp);
StringBuffer prop = new StringBuffer(propVal == null ? "" : propVal);
boolean mod = false;
for (Entry<String, String> pEntry : props.entrySet()) {
String key = pEntry.getKey();
if (key.startsWith(prefix)) {
if (key.indexOf("class.") == prefix.length()) {
// prefix is followed by checker class name
String className = key.substring(prefix.length()
+ "class.".length());
try {
Class.forName(className, true,
this.getClass().getClassLoader());
} catch (Throwable t) {
// don't really care, but class checking failed, so we
// do not add
this.logger.log(Logger.LOG_DEBUG, "Class " + className
+ " not found. Ignoring '" + pEntry.getValue()
+ "' for property " + osgiProp);
continue;
}
}
// get here if class is known or no checker class
this.logger.log(Logger.LOG_DEBUG, "Adding '"
+ pEntry.getValue() + "' to property " + osgiProp);
if (prop.length() > 0) {
prop.append(',');
}
prop.append(pEntry.getValue());
mod = true;
}
}
// replace the property with the modified property
if (mod) {
this.logger.log(Logger.LOG_DEBUG, "Setting property " + osgiProp
+ " to " + prop.toString());
props.put(osgiProp, prop.toString());
}
}
private void setInstallBundles(Map<String, String> props) {
String prefix = "sling.install.";
Set<String> levels = new TreeSet<String>();
for (String key : props.keySet()) {
if (key.startsWith(prefix)) {
levels.add(key.substring(prefix.length()));
}
}
StringBuffer buf = new StringBuffer();
for (String level : levels) {
if (buf.length() > 0) {
buf.append(',');
}
buf.append(level);
}
props.put(prefix + "bundles", buf.toString());
}
/**
* Ensures sensible Execution Environment setting. If the
* <code>org.osgi.framework.executionenvironment</code> property is set in
* the configured properties or the system properties, we ensure that older
* settings for J2SE-1.2, J2SE-1.3 and J2SE-1.4 are included. If the
* property is neither set in the configuration properties nor in the system
* properties, the property is not set.
*
* @param props The configuration properties to check and optionally ammend.
*/
private void setExecutionEnvironment(Map<String, String> props) {
// get the current Execution Environment setting
String ee = props.get(Constants.FRAMEWORK_EXECUTIONENVIRONMENT);
if (ee == null) {
ee = System.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT);
}
// if there is a setting, ensure J2SE-1.2/3/4/5 is included in the list
if (ee != null) {
int javaMinor;
try {
String specVString = System.getProperty("java.specification.version");
javaMinor = Version.parseVersion(specVString).getMinor();
} catch (IllegalArgumentException iae) {
// don't care, assume minimal sling version (1.5)
javaMinor = 5;
}
for (int i = 2; i <= javaMinor; i++) {
String exEnv = "J2SE-1." + i;
if (ee.indexOf(exEnv) < 0) {
ee += "," + exEnv;
}
}
this.logger.log(Logger.LOG_INFO,
"Using Execution Environment setting: " + ee);
props.put(Constants.FRAMEWORK_EXECUTIONENVIRONMENT, ee);
} else {
this.logger.log(Logger.LOG_INFO,
"Not using Execution Environment setting");
}
}
// ---------- Extension support --------------------------------------------
/**
* Loads additional properties into the <code>properties</code> object.
* <p>
* This implementation does nothing and may be overwritten by extensions
* requiring additional properties to be set.
* <p>
* This method is called when the servlet is initialized to prepare the
* configuration for <code>Felix</code>. Implementations may add
* properties from implementation specific sources. Properties added here
* overwrite properties loaded from the default properties file and may be
* overwritten by parameters set in the web application.
* <p>
* The <code>properties</code> object has not undergone variable
* substition and properties added by this method may also contain values
* refererring to other properties.
* <p>
* The properties added in this method will not be persisted in the
* <code>sling.properties</code> file in the <code>sling.home</code>
* directory.
*
* @param properties The <code>Properties</code> object to which custom
* properties may be added.
*/
protected void loadPropertiesOverride(Map<String, String> properties) {
}
/**
* Returns the <code>BundleContext</code> of the system bundle of the OSGi
* framework launched by this servlet. This method only returns a non-<code>null</code>
* object after the system bundle of the framework has been started and
* before it is being stopped.
*/
protected final BundleContext getBundleContext() {
return this.bundleContext;
}
/**
* Executes additional startup tasks and is called by the
* {@link #start(BundleContext)} method.
* <p>
* This implementation does nothing and may be overwritten by extensions
* requiring additional startup tasks.
*
* @throws Exception May be thrown in case of problems.
*/
protected void doStartBundle() throws Exception {
}
/**
* Executes additional shutdown tasks and is called by the
* {@link #stop(BundleContext)} method.
* <p>
* This implementation does nothing and may be overwritten by extensions
* requiring additional shutdown tasks.
* <p>
* When overwriting this method, it must be made sure, that no exception may
* be thrown, otherwise unexpected behaviour may result.
*/
protected void doStopBundle() {
}
// ---------- Property file support ----------------------------------------
/**
* Looks for <code>sling.include</code> and <code>sling.include.*</code>
* properties in the <code>props</code> and loads properties form the
* respective locations.
* <p>
* Each <code>sling.include</code> (or <code>sling.include.*</code>)
* property may contain a comma-separated list of resource and/or file names
* to be loaded from. The includes are loaded in alphabetical order of the
* property names.
* <p>
* Each reasource path is first tried to be loaded through the
* {@link #resourceProvider}. If that fails, the resource path is tested as
* a file. If relative <code>slingHome</code> is used as the parent if not
* <code>null</code>, otherwise the current working directory is used as
* the parent.
* <p>
* Any non-existing resource is silently ignored.
* <p>
* When the method returns, the <code>sling.include</code> and
* <code>sling.include.*</code> properties are not contained in the
* <code>props</code> any more.
*
* @param props The <code>Properties</code> containing the
* <code>sling.include</code> and <code>sling.include.*</code>
* properties. This is also the destination for the new
* properties loaded.
* @param slingHome The parent directory used to resolve relative path names
* if loading from a file. This may be <code>null</code> in
* which case the current working directory is used as the
* parent.
*/
private void loadIncludes(Map<String, String> props, String slingHome) {
// Build the sort map of include properties first
// and remove include elements from the properties
SortedMap<String, String> includes = new TreeMap<String, String>();
for (Iterator<Entry<String, String>> pi = props.entrySet().iterator(); pi.hasNext();) {
Entry<String, String> entry = pi.next();
if (entry.getKey().startsWith("sling.include.")
|| entry.getKey().equals("sling.include")) {
includes.put(entry.getKey(), entry.getValue());
pi.remove();
}
}
for (Iterator<Entry<String, String>> ii = includes.entrySet().iterator(); ii.hasNext();) {
Map.Entry<String, String> entry = ii.next();
String key = entry.getKey();
String include = entry.getValue();
// ensure variable resolution on this property
include = substVars(include, key, null, props);
StringTokenizer tokener = new StringTokenizer(include, ",");
while (tokener.hasMoreTokens()) {
String file = tokener.nextToken().trim();
InputStream is = this.resourceProvider.getResourceAsStream(file);
try {
if (is == null && slingHome != null) {
File resFile = new File(file);
if (!resFile.isAbsolute()) {
resFile = new File(slingHome, file);
}
if (resFile.canRead()) {
is = new FileInputStream(resFile);
file = resFile.getAbsolutePath(); // for logging
}
}
if (is != null) {
this.load(props, is);
}
} catch (IOException ioe) {
this.logger.log(Logger.LOG_ERROR,
"Error loading config properties from " + file, ioe);
}
}
}
}
/**
* Load properties from the given resource file, which is accessed through
* the {@link #resourceProvider}. If the resource does not exist, nothing
* is loaded.
*
* @param props The <code>Properties</code> into which the loaded
* properties are loaded
* @param resource The resource from which to load the resources
*/
private void load(Map<String, String> props, String resource) {
InputStream is = this.resourceProvider.getResourceAsStream(resource);
if (is != null) {
try {
this.load(props, is);
} catch (IOException ioe) {
this.logger.log(Logger.LOG_ERROR,
"Error loading config properties from " + resource, ioe);
}
}
}
/**
* Load properties from the given file. If the resource cannot be read from
* (e.g. because it does not exist), nothing is loaded.
*
* @param props The <code>Properties</code> into which the loaded
* properties are loaded
* @param file The <code>File</code> to load the properties from
*/
private void load(Map<String, String> props, File file) {
if (file != null && file.canRead()) {
try {
this.load(props, new FileInputStream(file));
} catch (IOException ioe) {
this.logger.log(Logger.LOG_ERROR,
"Error loading config properties from "
+ file.getAbsolutePath(), ioe);
}
}
}
private void load(Map<String, String> props, InputStream ins)
throws IOException {
try {
Properties tmp = new Properties();
tmp.load(ins);
for (Map.Entry<Object, Object> entry : tmp.entrySet()) {
props.put((String) entry.getKey(), (String) entry.getValue());
}
} finally {
try {
ins.close();
} catch (IOException ioe2) {
// ignore
}
}
}
// ---------- Property file variable substition support --------------------
/**
* The starting delimiter of variable names (value is "${").
*/
private static final String DELIM_START = "${";
/**
* The ending delimiter of variable names (value is "}").
*/
private static final String DELIM_STOP = "}";
/**
* This method performs property variable substitution on the specified
* value. If the specified value contains the syntax
* <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
* 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.
*/
private static String substVars(String val, String currentKey,
Map<String, String> cycleMap, Map<String, String> configProps)
throws IllegalArgumentException {
// If there is currently no cycle map, then create
// one for detecting cycles for this invocation.
if (cycleMap == null) {
cycleMap = new HashMap<String, String>();
}
// 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);
// 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 val;
}
// At this point, we found a stop delimiter without a start,
// so throw an exception.
else if (((startDelim < 0) || (startDelim > stopDelim))
&& (stopDelim >= 0)) {
throw new IllegalArgumentException(
"stop delimiter with no start delimiter: " + 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 = (configProps != null)
? configProps.get(variable)
: null;
if (substValue == null) {
// Ignore unknown property values.
substValue = 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);
// Return the value.
return val;
}
}