/* * 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.felix.utils.properties; import java.util.HashMap; import java.util.Map; import org.osgi.framework.BundleContext; /** * <p> * Enhancement of the standard <code>Properties</code> * managing the maintain of comments, etc. * </p> * * @author gnodet, jbonofre */ public class InterpolationHelper { private InterpolationHelper() { } private static final char ESCAPE_CHAR = '\\'; private static final String DELIM_START = "${"; private static final String DELIM_STOP = "}"; private static final String MARKER = "$__"; private static final String ENV_PREFIX = "env:"; /** * Callback for substitution */ public interface SubstitutionCallback { String getValue(String key); } /** * Perform substitution on a property set * * @param properties the property set to perform substitution on */ public static void performSubstitution(Map<String,String> properties) { performSubstitution(properties, (BundleContext) null); } /** * Perform substitution on a property set * * @param properties the property set to perform substitution on * @param context The bundle context */ public static void performSubstitution(Map<String,String> properties, BundleContext context) { performSubstitution(properties, new BundleContextSubstitutionCallback(context)); } /** * Perform substitution on a property set * * @param properties the property set to perform substitution on * @param callback Callback for substituion */ public static void performSubstitution(Map<String,String> properties, SubstitutionCallback callback) { performSubstitution(properties, callback, true, true, true); } /** * Perform substitution on a property set * * @param properties the property set to perform substitution on * @param callback the callback to obtain substitution values * @param substituteFromConfig If substitute from configuration * @param substituteFromSystemProperties If substitute from system properties * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise */ public static void performSubstitution(Map<String,String> properties, SubstitutionCallback callback, boolean substituteFromConfig, boolean substituteFromSystemProperties, boolean defaultsToEmptyString) { Map<String, String> org = new HashMap<String, String>(properties); for (String name : properties.keySet()) { String value = properties.get(name); properties.put(name, substVars(value, name, null, org, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString)); } } /** * <p> * This method performs property variable substitution on the * specified value. If the specified value contains the syntax * <tt>${<prop-name>}</tt>, where <tt><prop-name></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. * </p> * * @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<String,String> cycleMap, Map<String,String> configProps) throws IllegalArgumentException { return substVars(val, currentKey, cycleMap, configProps, (SubstitutionCallback) null); } /** * <p> * This method performs property variable substitution on the * specified value. If the specified value contains the syntax * <tt>${<prop-name>}</tt>, where <tt><prop-name></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. * </p> * * @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. * @param context the bundle context to retrieve properties from * @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<String,String> cycleMap, Map<String,String> configProps, BundleContext context) throws IllegalArgumentException { return substVars(val, currentKey, cycleMap, configProps, new BundleContextSubstitutionCallback(context)); } /** * <p> * This method performs property variable substitution on the * specified value. If the specified value contains the syntax * <tt>${<prop-name>}</tt>, where <tt><prop-name></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. * </p> * * @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. * @param callback the callback to obtain substitution values * @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<String,String> cycleMap, Map<String,String> configProps, SubstitutionCallback callback) throws IllegalArgumentException { return substVars(val, currentKey, cycleMap, configProps, callback, true, true, true); } /** * <p> * This method performs property variable substitution on the * specified value. If the specified value contains the syntax * <tt>${<prop-name>}</tt>, where <tt><prop-name></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. * </p> * * @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. * @param callback the callback to obtain substitution values * @param substituteFromConfig If substitute from configuration * @param substituteFromSystemProperties If substitute from system properties * @param defaultsToEmptyString sets an empty string if a replacement value is not found, leaves intact otherwise * @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<String,String> cycleMap, Map<String,String> configProps, SubstitutionCallback callback, boolean substituteFromConfig, boolean substituteFromSystemProperties, boolean defaultsToEmptyString) throws IllegalArgumentException { return unescape(doSubstVars(val, currentKey, cycleMap, configProps, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString)); } private static String doSubstVars(String val, String currentKey, Map<String,String> cycleMap, Map<String,String> configProps, SubstitutionCallback callback, boolean substituteFromConfig, boolean substituteFromSystemProperties, boolean defaultsToEmptyString) throws IllegalArgumentException { 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 startDelim; int stopDelim = -1; do { stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1); 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. 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; } } } while (startDelim >= 0 && stopDelim >= 0 && stopDelim < startDelim + DELIM_START.length()); // If we do not have a start or stop delimiter, then just // return the existing value. if ((startDelim < 0) || (stopDelim < 0)) { cycleMap.remove(currentKey); return 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); String org = variable; // Strip expansion modifiers int idx1 = variable.lastIndexOf(":-"); int idx2 = variable.lastIndexOf(":+"); int idx = idx1 >= 0 && idx2 >= 0 ? Math.min(idx1, idx2) : idx1 >= 0 ? idx1 : idx2; String op = null; if (idx >= 0 && idx < variable.length()) { op = variable.substring(idx); variable = variable.substring(0, idx); } // Verify that this is not a recursive variable reference. if (cycleMap.get(variable) != null) { throw new IllegalArgumentException("recursive variable reference: " + variable); } String substValue = null; // Get the value of the deepest nested variable placeholder. // Try to configuration properties first. if (substituteFromConfig && configProps != null) { substValue = configProps.get(variable); } if (substValue == null) { if (variable.length() > 0) { if (callback != null) { substValue = callback.getValue(variable); } if (substValue == null && substituteFromSystemProperties) { substValue = System.getProperty(variable); } } } if (op != null) { if (op.startsWith(":-")) { if (substValue == null || substValue.length() == 0 ) { substValue = op.substring(":-".length()); } } else if (op.startsWith(":+")) { if (substValue != null && substValue.length() != 0) { substValue = op.substring(":+".length()); } } else { throw new IllegalArgumentException("Bad substitution: ${" + org + "}"); } } if (substValue == null) { if (defaultsToEmptyString) { substValue = ""; } else { // alters the original token to avoid infinite recursion // altered tokens are reverted in substVarsPreserveUnresolved() substValue = MARKER + "{" + 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 = doSubstVars(val, currentKey, cycleMap, configProps, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString); cycleMap.remove(currentKey); // Return the value. return val; } private static String unescape(String val) { val = val.replaceAll("\\" + MARKER, "\\$"); 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; } public static class BundleContextSubstitutionCallback implements SubstitutionCallback { private final BundleContext context; public BundleContextSubstitutionCallback(BundleContext context) { this.context = context; } public String getValue(String key) { String value = null; if (key.startsWith(ENV_PREFIX)) { value = System.getenv(key.substring(ENV_PREFIX.length())); } else { if (context != null) { value = context.getProperty(key); } if (value == null) { value = System.getProperty(key); } } return value; } } }