/*
* 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.ambari.server.state.kerberos;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.ambari.server.AmbariException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Singleton;
/**
* Helper class to provide variable replacement services
*/
@Singleton
public class VariableReplacementHelper {
private static final Logger LOG = LoggerFactory.getLogger(VariableReplacementHelper.class);
/**
* a regular expression Pattern used to find "variable" placeholders in strings
*/
private static final Pattern PATTERN_VARIABLE = Pattern.compile("\\$\\{(?:([\\w\\-\\.]+)/)?([\\w\\-\\.]+)(?:\\s*\\|\\s*(.+?))?\\}");
/**
* a regular expression Pattern used to parse "function" declarations: name(arg1, arg2, ...)
*/
private static final Pattern PATTERN_FUNCTION = Pattern.compile("(\\w+)\\((.*?)\\)");
/**
* A map of "registered" functions
*/
private static final Map<String, Function> FUNCTIONS = new HashMap<String, Function>() {
{
put("each", new EachFunction());
put("toLower", new ToLowerFunction());
put("replace", new ReplaceValue());
put("append", new AppendFunction());
}
};
/**
* Performs variable replacement on the supplied String value using values from the replacementsMap.
* <p/>
* The value is a String containing one or more "variables" in the form of ${variable_name}, such
* that "variable_name" may indicate a group identifier; else "" is used as the group.
* For example:
* <p/>
* variable_name: group: ""; property: "variable_name"
* group1/variable_name: group: "group1"; property: "variable_name"
* root/group1/variable_name: Not Supported
* <p/>
* The replacementsMap is a Map of Maps creating a (small) hierarchy of data to traverse in order
* to resolve the variable.
* <p/>
* If a variable resolves to one or more variables, that new variable(s) will be processed and replaced.
* If variable exists after a set number of iterations it is assumed that a cycle has been created
* and the process will abort returning a String in a possibly unexpected state.
*
* @param value a String containing zero or more variables to be replaced
* @param replacementsMap a Map of data used to perform the variable replacements
* @return a new String
*/
public String replaceVariables(String value, Map<String, Map<String, String>> replacementsMap) throws AmbariException {
if ((value != null) && (replacementsMap != null) && !replacementsMap.isEmpty()) {
int count = 0; // Used to help prevent an infinite loop...
boolean replacementPerformed;
do {
if (++count > 1000) {
throw new AmbariException(String.format("Circular reference found while replacing variables in %s", value));
}
Matcher matcher = PATTERN_VARIABLE.matcher(value);
StringBuffer sb = new StringBuffer();
replacementPerformed = false;
while (matcher.find()) {
String type = matcher.group(1);
String name = matcher.group(2);
String function = matcher.group(3);
Map<String, String> replacements;
if ((name != null) && !name.isEmpty()) {
if (type == null) {
replacements = replacementsMap.get("");
} else {
replacements = replacementsMap.get(type);
}
if (replacements != null) {
String replacement = replacements.get(name);
if (replacement != null) {
if (function != null) {
replacement = applyReplacementFunction(function, replacement, replacementsMap);
}
// Escape '$' and '\' so they don't cause any issues.
matcher.appendReplacement(sb, replacement.replace("\\", "\\\\").replace("$", "\\$"));
replacementPerformed = true;
}
}
}
}
matcher.appendTail(sb);
value = sb.toString();
}
while (replacementPerformed); // Process the string again to make sure new variables were not introduced
}
return value;
}
/**
* Applies the specified replacement function to the supplied data.
* <p/>
* The function must be in the following format:
* <code>
* name(arg1,arg2,arg3...)
* </code>
* <p/>
* Commas in arguments should be escaped with a '/'.
*
* @param function the name and arguments of the function
* @param replacement the data to use in the function
* @param replacementsMap a Map of data used to perform variable replacements, if needed
* @return a new string generated by applying the function
*/
private String applyReplacementFunction(String function, String replacement, Map<String, Map<String, String>> replacementsMap) {
if (function != null) {
Matcher matcher = PATTERN_FUNCTION.matcher(function);
if (matcher.matches()) {
String name = matcher.group(1);
if (name != null) {
Function f = FUNCTIONS.get(name);
if (f != null) {
String args = matcher.group(2);
String[] argsList = args.split("(?<!\\\\),");
// Remove escape character from '\,'
for (int i = 0; i < argsList.length; i++) {
argsList[i] = argsList[i].trim().replace("\\,", ",");
}
return f.perform(argsList, replacement, replacementsMap);
}
}
}
}
return replacement;
}
/**
* Function is the interface to be implemented by replacement functions.
*/
private interface Function {
/**
* Perform the function to generate a new string by applying the logic of this function to the
* supplied data.
*
* @param args an array of arguments, specific to the function
* @param data the data to apply the function logic to
* @param replacementsMap a Map of data used to perform variable replacements, if needed
* @return the resulting string
*/
String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap);
}
/**
* EachFunction is a Function implementation that iterates over a list of values pulled from a
* delimited string to yield a new string.
* <p/>
* This function expects the following arguments (in order) within the args array:
* <ol>
* <li>pattern to use for each item, see {@link String#format(String, Object...)}</li>
* <li>delimiter to use when concatenating the resolved pattern per item</li>
* <li>regular expression used to split the original value</li>
* </ol>
*/
private static class EachFunction implements Function {
@Override
public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
if ((args == null) || (args.length != 3)) {
throw new IllegalArgumentException("Invalid number of arguments encountered");
}
if (data != null) {
StringBuilder builder = new StringBuilder();
String pattern = args[0];
String concatDelimiter = args[1];
String dataDelimiter = args[2];
String[] items = data.split(dataDelimiter);
for (String item : items) {
if (builder.length() > 0) {
builder.append(concatDelimiter);
}
builder.append(String.format(pattern, item));
}
return builder.toString();
}
return "";
}
}
/**
* ReplaceValue is a Function implementation that replaces the value in the string
* <p/>
* This function expects the following arguments (in order) within the args array:
* <ol>
* <li>regular expression that should be replaced</li>
* <li>replacement value for the string</li>
* </ol>
*/
private static class ReplaceValue implements Function {
@Override
public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
if ((args == null) || (args.length != 2)) {
throw new IllegalArgumentException("Invalid number of arguments encountered");
}
if (data != null) {
StringBuffer builder = new StringBuffer();
String regex = args[0];
String replacement = args[1];
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(data);
while (matcher.find()) {
matcher.appendReplacement(builder, replacement);
}
matcher.appendTail(builder);
return builder.toString();
}
return "";
}
}
/**
* ToLowerFunction is a Function implementation that converts a String to lowercase
*/
private static class ToLowerFunction implements Function {
@Override
public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
if (data != null) {
return data.toLowerCase();
}
return "";
}
}
/**
* AppendFunction is a Function implementation that appends the current value to the value of some
* configuration property using a specified delimited value.
* <p/>
* This function expects the following arguments (in order) within the args array:
* <ol>
* <li>configuration specification to use to find the value to append</li>
* <li>delimiter to use when concatenating the value</li>
* <li>boolean value used to determine of the value should be appended only it is unique (true) or unconditionally appended (false)</li>
* </ol>
*/
private static class AppendFunction implements Function {
@Override
public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
if ((args == null) || (args.length != 3)) {
String message = "Invalid number of arguments encountered while processing the 'append' variable replacement function. The following arguments are expected:" +
"\n\t- Configuration specification used to get the initial value" +
"\n\t- Delimiter used for parsing the initial value and appending new values" +
"\n\t- A flag to indicate whether values should be unique ('true') or not ('false')";
LOG.error(message);
throw new IllegalArgumentException(message);
}
String configurationSpec = args[0];
String concatDelimiter = args[1];
boolean uniqueOnly = Boolean.parseBoolean(args[2]);
String sourceData = getSourceData(replacementsMap, configurationSpec);
Collection<String> sourceItems = parseItems(sourceData, concatDelimiter);
Collection<String> dataItems = parseItems(data, concatDelimiter);
Collection<String> items = new ArrayList<>();
if (uniqueOnly) {
for (String item : sourceItems) {
if (!items.contains(item)) {
items.add(item);
}
}
for (String item : dataItems) {
if (!items.contains(item)) {
items.add(item);
}
}
} else {
items.addAll(sourceItems);
items.addAll(dataItems);
}
return StringUtils.join(items, concatDelimiter);
}
/**
* Parse the string using the specified delimiter to create a collection of parsed (and trimmed) Strings.
*
* @param delimitedString the String to parse
* @param concatDelimiter the delimiter used to split the String
* @return a Collection of Strings split from the original string
*/
private Collection<String> parseItems(String delimitedString, String concatDelimiter) {
Collection<String> items = new ArrayList<>();
if (!StringUtils.isEmpty(delimitedString)) {
for (String item : delimitedString.split(concatDelimiter)) {
item = item.trim();
if (!item.isEmpty()) {
items.add(item);
}
}
}
return items;
}
/**
* Retrieves the source data given a configuration specification and a Map of configurations, grouped by configuration types.
* <p>
* The configuration specification is expected to be in one of the following forms:
* <ul>
* <li>config-type/property-name</li>
* <li>property-name</li>
* </ul>
* <p>
* The replacementsMap is expected to be a Map of config-types to their name/value pairs.
*
* @param replacementsMap a Map of data used to perform variable replacements, if needed
* @param configurationSpec a configuration specification declaring the config-type (optional) and the relevant
* property name
* @return the found value
*/
private String getSourceData(Map<String, Map<String, String>> replacementsMap, String configurationSpec) {
String sourceData = null;
if ((replacementsMap != null) && !replacementsMap.isEmpty() && !StringUtils.isEmpty(configurationSpec)) {
// Parse the configuration specification to get the config-type and property-name.
// If only one "part" is found when splitting the String, assume that is it the property name
// where the config-type will be assumed to be "", which contains properties not set in service
// configurations
String[] parts = configurationSpec.split("/");
String type = null;
String name = null;
if (parts.length == 2) {
type = parts[0];
name = parts[1];
} else if (parts.length == 1) {
name = parts[0];
}
if (!StringUtils.isEmpty(name)) {
Map<String, String> replacements;
if (type == null) {
replacements = replacementsMap.get("");
} else {
replacements = replacementsMap.get(type);
}
if (replacements != null) {
sourceData = replacements.get(name);
}
}
}
return sourceData;
}
}
}