content: /*
   * 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.AmbariRuntimeException;
  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());
        put("principalPrimary", new PrincipalPrimary());
        put("stripPort", new StripPort());
      }
    };
  
    /**
     * 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 AmbariRuntimeException {
      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 AmbariRuntimeException(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);
  
            if ((name != null) && !name.isEmpty()) {
              Map<String, String> replacements = (type == null) ? replacementsMap.get("") : replacementsMap.get(type);
              if (replacements == null || replacements.get(name) == null) {
                continue;
              }
  
              String replacement = replacements.get(name);
              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;
      }
    }
  
    /**
     * Get the primary part of a Kerberos principal.
     * The format of a typical Kerberos principal is primary/instance@REALM.
     */
    private static class PrincipalPrimary implements Function {
      @Override
      public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
        if (data == null) {
          return null;
        }
        if (data.contains("/")) {
          return data.split("/")[0];
        } else if (data.contains("@")) {
          return data.split("@")[0];
        } else {
          return data;
        }
      }
    }
  
    /**
     * Strips out the port (if any) from a URL assuming the following input data layout
     * <code>host[:port]</code>
     */
    private static class StripPort implements Function {
      @Override
      public String perform(String[] args, String data, Map<String, Map<String, String>> replacementsMap) {
        if (data == null) {
          return null;
        }
        final int semicolonIndex = data.indexOf(":");
        return semicolonIndex == -1 ? data : data.substring(0, semicolonIndex);
      }
    }
  }
  