blob: 66be3bf950ac5f653ee7147e65ac4af9efa8bbd2 [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.ambari.server.state.kerberos;
import com.google.inject.Singleton;
import org.apache.ambari.server.AmbariException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper class to provide variable replacement services
*/
@Singleton
public class VariableReplacementHelper {
/**
* 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());
}
};
/**
* 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);
}
// 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
* @return a new string generated by appling the function
*/
private String applyReplacementFunction(String function, String replacement) {
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);
}
}
}
}
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
* @return the resulting string
*/
String perform(String[] args, String data);
}
/**
* 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) {
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 "";
}
}
/**
* 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) {
if (data != null) {
return data.toLowerCase();
}
return "";
}
}
}