All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.nn.adapterframework.util.StringResolver Maven / Gradle / Ivy

/*
   Copyright 2013, 2014 Nationale-Nederlanden, 2020 - 2023 WeAreFrank!

   Licensed 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 nl.nn.adapterframework.util;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * Provide functionality to resolve ${property.key} to the value of the property key, recursively.
 *
 * @author Johan Verrips
 */
public class StringResolver {
	/**
	 * Static field to access all instances via the {@link ServiceLoader}.
	 */
	private static final ServiceLoader serviceLoader = ServiceLoader.load(AdditionalStringResolver.class);
	// Not allowed to use a static reference to the logger in this class.
	// Log4j2 uses StringResolver during instantiation.

	private static final String VALUE_SEPARATOR=":-";

	public static final String DELIM_START = "${";
	public static final String DELIM_STOP = "}";

	private static Collection additionalStringResolvers = null;

	/**
	 * Do variable substitution on a string to resolve ${x2} to the value of the
	 * property x2. This is done recursive, so that 
*
	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @param propsToHide Optional collection of property names to hide from the output. If not null, then * all credentials will also be hidden, in addition to properties named in the collection. * * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2, Set propsToHide) throws IllegalArgumentException { return substVars(val, props1, props2, propsToHide, DELIM_START, DELIM_STOP, false); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @param propsToHide Optional collection of property names to hide from the output. If not null, then * all credentials will also be hidden, in addition to properties named in the collection. * @param delimStart Start of substitution pattern delimiter * @param delimStop End of substitution pattern delimiter * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2, Set propsToHide, String delimStart, String delimStop) throws IllegalArgumentException { return substVars(val, props1, props2, propsToHide, delimStart, delimStop, false); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @param propsToHide Optional collection of property names to hide from the output. If not null, then * all credentials will also be hidden, in addition to properties named in the collection. * @param delimStart Start of substitution pattern delimiter * @param delimStop End of substitution pattern delimiter * @param resolveWithPropertyName Flag indicating if property names should also be part of the output, for debugging of * configurations. * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2, Set propsToHide, String delimStart, String delimStop, boolean resolveWithPropertyName) throws IllegalArgumentException { if (delimStart.equals(delimStop)) { throw new IllegalArgumentException("Start and End delimiters of substitution variables cannot be the same: both are '" + delimStart + "'"); } StringBuilder sb = new StringBuilder(); SubstitutionContext ctx = new SubstitutionContext(propsToHide, delimStart, delimStop, resolveWithPropertyName); while (true) { findNextVariable(val, ctx); if (ctx.isNoDelimiterFound()) { // no delimiter return produceFinalOutputResult(val, sb, ctx); } appendFromInput(sb, val, ctx); String expression = extractNextExpression(val, ctx); String key = extractNextKey(val, props1, props2, ctx); Optional replacement = resolveReplacement(key, props1, props2, ctx); appendReplacement(sb, key, replacement, props1, props2, expression, ctx); } } private static String extractNextKey(String val, Map props1, Map props2, SubstitutionContext ctx) { String key = val.substring(ctx.pointer, ctx.tail); ctx.propertyComposer = key; if (key.contains(ctx.delimStart)) { key = substVars(key, props1, props2, ctx.resolveWithPropertyName); if(key.contains(VALUE_SEPARATOR) && ctx.resolveWithPropertyName) { ctx.propertyComposer = key; key = extractKeyValue(key, ctx); } } return key; } private static String extractNextExpression(String val, SubstitutionContext ctx) { if(val.contains(VALUE_SEPARATOR)) { ctx.tail = val.indexOf(VALUE_SEPARATOR); ctx.providedDefaultValue = val.substring(ctx.tail+VALUE_SEPARATOR.length(), indexOfDelimStop(val, ctx.pointer, ctx.delimStart, ctx.delimStop)); ctx.containsDefault=true; } else { ctx.tail = indexOfDelimStop(val, ctx.pointer, ctx.delimStart, ctx.delimStop); } if (ctx.tail == -1) { throw new IllegalArgumentException('[' + val + "] has no closing brace. Opening brace at position [" + ctx.pointer + "]"); } String expression = val.substring(ctx.pointer, ctx.tail + ctx.delimStop.length()); ctx.pointer += ctx.delimStart.length(); return expression; } private static void appendFromInput(StringBuilder sb, String val, SubstitutionContext ctx) { sb.append(val, ctx.head, ctx.resolveWithPropertyName ? ctx.pointer + ctx.delimStart.length() : ctx.pointer); } private static String produceFinalOutputResult(String val, StringBuilder sb, SubstitutionContext ctx) { // no more variables if (ctx.head == 0) { // this is a simple string return val; } // add the tail string which contains no variables and return the result. sb.append(val.substring(ctx.head)); return sb.toString(); } private static void findNextVariable(String val, SubstitutionContext ctx) { if (ctx.tail > 0) { // Not the first variable, so move head & tail beyond previous tail. if(ctx.containsDefault) { // tail points to index of ':-' update tail to point delimStop ctx.tail = indexOfDelimStop(val, ctx.pointer, ctx.delimStart, ctx.delimStop); } ctx.head = ctx.tail + ctx.delimStop.length(); } ctx.pointer = val.indexOf(ctx.delimStart, ctx.head); // index delimiter } private static void appendReplacement(StringBuilder sb, String key, Optional replacement, Map props1, Map props2, String expression, SubstitutionContext ctx) { if(ctx.resolveWithPropertyName) { sb.append(ctx.propertyComposer).append(VALUE_SEPARATOR); } if (replacement.isPresent()) { String replacementValue; if (ctx.propsToHide != null && ctx.propsToHide.contains(key)) { replacementValue = StringUtil.hide(replacement.get()); } else { replacementValue = replacement.get(); } // Do variable substitution on the replacement string // such that we can solve "Hello ${x1}" as "Hello p2" // the where the properties are // x1=${x2} // x2=p2 if (!replacementValue.equals(expression) && !replacementValue.contains(ctx.delimStart + key + ctx.delimStop)) { String recursiveReplacement = substVars(replacementValue, props1, props2, ctx.resolveWithPropertyName); sb.append(recursiveReplacement); } else { sb.append(replacementValue); } } else { if(ctx.providedDefaultValue != null) { // use default value of property if missing actual sb.append(ctx.providedDefaultValue); } } if(ctx.resolveWithPropertyName) { sb.append(ctx.delimStop); } } private static Optional resolveReplacement(String key, Map props1, Map props2, SubstitutionContext ctx) { // TODO: In Java9 and later this can be done a bit nicer using Optional.or() return Environment.getSystemProperty(key, null) .map(Optional::of) .orElseGet(() -> findInAdditionalResolvers(key, props1, props2, ctx)) .map(Optional::of) .orElseGet((()-> getReplacementFromProps(key, props1))) .map(Optional::of) .orElseGet((()-> getReplacementFromProps(key, props2))) ; } private static Optional findInAdditionalResolvers(String key, Map props1, Map props2, SubstitutionContext ctx) { Optional replacement; replacement = getAdditionalStringResolvers().stream() .map(resolver -> resolver.resolve(key, props1, props2, ctx.propsToHide, ctx.delimStart, ctx.delimStop, ctx.resolveWithPropertyName)) .filter(Optional::isPresent) .findFirst() .orElse(Optional.empty()); return replacement; } private static Optional getReplacementFromProps(String key, Map props) { if (props == null) { return Optional.empty(); } else if (props instanceof Properties) { return Optional.ofNullable(((Properties) props).getProperty(key)); } else { Object replacementSource = props.get(key); if (replacementSource != null) { return Optional.of(replacementSource.toString()); } } return Optional.empty(); } /** * Resolves just the values of the properties in case a property key depends on other keys * e.g. System.getProperty("prefix_${key:-value}") will find no matching data, this method extracts the 'value' for property lookup prefix_value */ private static String extractKeyValue(String key, SubstitutionContext ctx) { StringBuilder sb = new StringBuilder(); int pointer = 0; int delimStartIndex = key.indexOf(ctx.delimStart, pointer); if(delimStartIndex != -1) { sb.append(key, pointer, delimStartIndex); int valueSeparator = key.indexOf(VALUE_SEPARATOR, delimStartIndex); if(valueSeparator != -1) { int delimStopIndex = indexOfDelimStop(key, delimStartIndex, ctx.delimStart, ctx.delimStop); String valueOfKey = key.substring(valueSeparator+VALUE_SEPARATOR.length(), delimStopIndex); if(valueOfKey.contains(ctx.delimStart)) { sb.append(extractKeyValue(valueOfKey, ctx)); } else { sb.append(valueOfKey); } } } return sb.toString(); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2) throws IllegalArgumentException { return substVars(val, props1, props2, null); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. * * @param val Value in which to provide string substitutions * @param props Property object in which to find substitutions * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props) throws IllegalArgumentException { return substVars(val, props, null); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. * * @param val Value in which to provide string substitutions * @param props Property object in which to find substitutions * @param resolveWithPropertyName Flag indicating if property names should also be part of the output, for debugging of * configurations. * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props, boolean resolveWithPropertyName) { return substVars(val, props, null, resolveWithPropertyName); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @param resolveWithPropertyName Flag indicating if property names should also be part of the output, for debugging of * configurations. * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2, boolean resolveWithPropertyName) throws IllegalArgumentException { return substVars(val, props1, props2, null, DELIM_START, DELIM_STOP, resolveWithPropertyName); } /** * Do variable substitution on a string to resolve ${x2} to the value of the * property x2. This is done recursive, so that
*

	 * Properties prop = new Properties();
	 * prop.put("test.name", "this is a name with ${test.xx}");
	 * prop.put("test.xx", "again");
	 * System.out.println(prop.get("test.name"));
	 * 
will print this is a name with again *

* First it looks in the System environment and System properties, if none is found * then all installed {@link AdditionalStringResolver}s are scanned for providing a replacement. * If no replacement is found still and a {@link Map} (or {@link Properties}) object is specified, it looks in the specified * object. If two {@link Map} or {@link Properties} objects are supplied, it looks first in the first and if none found * then in the second object. * * @param val Value in which to provide string substitutions * @param props1 First property object in which to find substitutions * @param props2 Second property object in which to find substitutions * @param propsToHide Optional collection of property names to hide from the output. If not null, then * all credentials will also be hidden, in addition to properties named in the collection. * @param resolveWithPropertyName Flag indicating if property names should also be part of the output, for debugging of * configurations. * @return Input string with all property reference patterns resolved to either a property value, or empty. * @throws IllegalArgumentException if there were invalid input arguments. */ public static String substVars(String val, Map props1, Map props2, Set propsToHide, boolean resolveWithPropertyName) throws IllegalArgumentException { return substVars(val, props1, props2, propsToHide, DELIM_START, DELIM_STOP, resolveWithPropertyName); } /** * Check if the input string needs property substitution applied. * @param string String to check * @return {@code true} if the input string contains the default start and end delimiters in consecutive * order, otherwise {@code false}. * The default delimiters are

"${"
and
"}"
respectively. */ public static boolean needsResolution(String string) { int j = string.indexOf(DELIM_START); return j>=0 && string.indexOf(DELIM_STOP, j) > j; } private static int indexOfDelimStop(String val, int startPos, String delimStart, String delimStop) { // if variable in variable then find the correct stop delimiter int stopPos = startPos - delimStop.length(); int numEmbeddedStart = 0; int numEmbeddedStop = 0; do { startPos += delimStart.length(); stopPos = val.indexOf(delimStop, stopPos + delimStop.length()); if (stopPos > 0) { String key = val.substring(startPos, stopPos); numEmbeddedStart = StringUtils.countMatches(key, delimStart); numEmbeddedStop = StringUtils.countMatches(key, delimStop); } } while (stopPos > 0 && numEmbeddedStart != numEmbeddedStop); return stopPos; } private static Collection getAdditionalStringResolvers() { if (additionalStringResolvers == null) { try { additionalStringResolvers = CollectionUtils.collect(serviceLoader.iterator(), input -> input); } catch (Throwable t) { t.printStackTrace(); //Cannot log this because it's used before Log4j2 initializes. additionalStringResolvers = Collections.emptyList(); } } return additionalStringResolvers; } private static class SubstitutionContext { final Set propsToHide; final String delimStart; final String delimStop; final boolean resolveWithPropertyName; String providedDefaultValue=null; boolean containsDefault = false; int head = 0; int pointer; int tail; String propertyComposer = ""; private SubstitutionContext(Set propsToHide, String delimStart, String delimStop, boolean resolveWithPropertyName) { this.propsToHide = propsToHide; this.delimStart = delimStart; this.delimStop = delimStop; this.resolveWithPropertyName = resolveWithPropertyName; } boolean isNoDelimiterFound() { return pointer == -1; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy