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;
  
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.StackAccessException;
  import org.apache.ambari.server.api.services.AmbariMetaInfo;
  import org.apache.commons.collections4.CollectionUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.inject.Inject;
  import com.google.inject.Provider;
  import com.google.inject.Singleton;
  
  /**
   * Class that assists with merging configuration values across stacks.
   */
  @Singleton
  public class ConfigMergeHelper {
  
    private static final Pattern HEAP_PATTERN = Pattern.compile("(\\d+)([mgMG])");
    private static final Logger LOG = LoggerFactory.getLogger(ConfigMergeHelper.class);
  
    @Inject
    private Provider<Clusters> m_clusters;
  
    @Inject
    private Provider<AmbariMetaInfo> m_ambariMetaInfo;
  
    @SuppressWarnings("unchecked")
    public Map<String, Map<String, ThreeWayValue>> getConflicts(String clusterName, StackId targetStack) throws AmbariException {
      Cluster cluster = m_clusters.get().getCluster(clusterName);
      StackId oldStack = cluster.getCurrentStackVersion();
  
      Map<String, Map<String, String>> oldMap = new HashMap<>();
      Map<String, Map<String, String>> newMap = new HashMap<>();
  
      // Collect service-level properties for old and new stack
      for (String serviceName : cluster.getServices().keySet()) {
        try {
          Set<PropertyInfo> newStackProperties = m_ambariMetaInfo.get().getServiceProperties(
            targetStack.getStackName(), targetStack.getStackVersion(), serviceName);
          addToMap(newMap, newStackProperties);
        } catch (StackAccessException e) {
          LOG.info("Skipping service {} which is currently installed but does not exist in the target stack {}", serviceName, targetStack);
          continue;
        }
        Set<PropertyInfo> oldStackProperties = m_ambariMetaInfo.get().getServiceProperties(
            oldStack.getStackName(), oldStack.getStackVersion(), serviceName);
        addToMap(oldMap, oldStackProperties);
    }
  
      // Collect stack-level properties defined for old and new stack
      Set<PropertyInfo> set = m_ambariMetaInfo.get().getStackProperties(
          oldStack.getStackName(), oldStack.getStackVersion());
      addToMap(oldMap, set);
  
      set = m_ambariMetaInfo.get().getStackProperties(
          targetStack.getStackName(), targetStack.getStackVersion());
      addToMap(newMap, set);
  
      // Final result after merging.
      Map<String, Map<String, ThreeWayValue>> result =
        new HashMap<>();
  
      // For every property of every config type, see if it is going to be merged:
      for (Entry<String, Map<String, String>> entry : oldMap.entrySet()) {
        if (!newMap.containsKey(entry.getKey())) {
          LOG.info("Stack {} does not have an equivalent config type {} in {}",
              oldStack.getStackId(), entry.getKey(), targetStack.getStackId());
          continue;
        }
  
        Map<String, String> oldPairs = entry.getValue();
        Map<String, String> newPairs = newMap.get(entry.getKey());
        Collection<String> customValueKeys = null;
  
        Config config = cluster.getDesiredConfigByType(entry.getKey());
  
        // The conflict occurs, if current property is not defined at stack
        // metainfo, but is present in current cluster configs,
        // and a new value is different from current value
        if (null != config) {
          Set<String> valueKeys = config.getProperties().keySet();
  
          customValueKeys = CollectionUtils.subtract(valueKeys, oldPairs.keySet());
        }
  
        // Keep properties with custom values (i.e., changed from default value in old stack)
        if (null != customValueKeys) {
          for (String prop : customValueKeys) {
            String newVal = newPairs.get(prop);
            String savedVal = config.getProperties().get(prop);
            if (null != newVal && null != savedVal && !newVal.equals(savedVal)) {
              ThreeWayValue twv = new ThreeWayValue();
              twv.oldStackValue = null;
              twv.newStackValue = normalizeValue(savedVal, newVal.trim());
              twv.savedValue = savedVal.trim();
  
              if (!result.containsKey(entry.getKey())) {
                result.put(entry.getKey(), new HashMap<>());
              }
  
              result.get(entry.getKey()).put(prop, twv);
            }
          }
        }
  
        // Also, conflict occurs if all four coditions are met:
        // a) property is defined both in old and in new stack
        // b) property value in new stack differs from current
        // the property value
        // c) current property value is not the same as in new stack
        // d) current property value is not the same as in old stack
  
        Collection<String> common = CollectionUtils.intersection(newPairs.keySet(),
            oldPairs.keySet());
  
        for (String prop : common) {
          String oldStackVal = oldPairs.get(prop);
          String newStackVal = newPairs.get(prop);
          String savedVal = "";
          if (null != config) {
            savedVal = config.getProperties().get(prop);
          }
  
          if (!(newStackVal == null || oldStackVal == null)
                  && !newStackVal.equals(savedVal) &&
              (!oldStackVal.equals(newStackVal) || !oldStackVal.equals(savedVal))) {
            ThreeWayValue twv = new ThreeWayValue();
            twv.oldStackValue = normalizeValue(savedVal, oldStackVal.trim());
            twv.newStackValue = normalizeValue(savedVal, newStackVal.trim());
            twv.savedValue = (null == savedVal) ? null : savedVal.trim();
  
            if (!result.containsKey(entry.getKey())) {
              result.put(entry.getKey(), new HashMap<>());
            }
  
            result.get(entry.getKey()).put(prop, twv);
          }
        }
      }
  
      return result;
    }
  
    private void addToMap(Map<String, Map<String, String>> map, Set<PropertyInfo> stackProperties) {
      for (PropertyInfo pi : stackProperties) {
        String type = ConfigHelper.fileNameToConfigType(pi.getFilename());
  
        if (!map.containsKey(type)) {
          map.put(type, new HashMap<>());
        }
        map.get(type).put(pi.getName(), pi.getValue());
      }
  
    }
  
    /**
     * Represents the three different config values for merging.
     */
    public static class ThreeWayValue {
      /**
       * The previous stack-defined value.
       */
      public String oldStackValue;
      /**
       * The new stack-defined value.
       */
      public String newStackValue;
      /**
       * The saved stack value, possibly changed from previous stack-defined value.
       */
      public String savedValue;
    }
  
    /**
     * If template looks like Java heap size, then format raw value to comply
     * with size unit specified in template
     * @param templateValue
     * @param newRawValue
     * @return raw value formatted like template (if template looks like Java
     * heap size), raw value as is otherwise.
     */
    static String normalizeValue(String templateValue, String newRawValue) {
      if (null == templateValue) {
        return newRawValue;
      }
  
      Matcher m = HEAP_PATTERN.matcher(templateValue);
  
      if (m.matches()) {
        return newRawValue + m.group(2);
      }
  
      return newRawValue;
    }
  
  }
  