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.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  import java.util.SortedMap;
  import java.util.TreeMap;
  import java.util.concurrent.TimeUnit;
  
  import javax.annotation.Nullable;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.AmbariRuntimeException;
  import org.apache.ambari.server.HostNotFoundException;
  import org.apache.ambari.server.agent.stomp.AgentConfigsHolder;
  import org.apache.ambari.server.agent.stomp.MetadataHolder;
  import org.apache.ambari.server.agent.stomp.dto.ClusterConfigs;
  import org.apache.ambari.server.api.services.AmbariMetaInfo;
  import org.apache.ambari.server.configuration.Configuration;
  import org.apache.ambari.server.controller.AmbariManagementController;
  import org.apache.ambari.server.controller.AmbariManagementControllerImpl;
  import org.apache.ambari.server.events.AgentConfigsUpdateEvent;
  import org.apache.ambari.server.events.HostComponentUpdate;
  import org.apache.ambari.server.events.HostComponentsUpdateEvent;
  import org.apache.ambari.server.events.publishers.STOMPUpdatePublisher;
  import org.apache.ambari.server.orm.dao.ClusterDAO;
  import org.apache.ambari.server.orm.dao.ServiceConfigDAO;
  import org.apache.ambari.server.orm.entities.ClusterConfigEntity;
  import org.apache.ambari.server.orm.entities.HostComponentDesiredStateEntity;
  import org.apache.ambari.server.orm.entities.ServiceConfigEntity;
  import org.apache.ambari.server.stack.StackDirectory;
  import org.apache.ambari.server.state.PropertyInfo.PropertyType;
  import org.apache.ambari.server.state.configgroup.ConfigGroup;
  import org.apache.ambari.server.utils.SecretReference;
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.collections4.MapUtils;
  import org.apache.commons.lang.StringUtils;
  import org.apache.commons.lang.math.NumberUtils;
  import org.apache.commons.lang3.StringEscapeUtils;
  import org.apache.commons.lang3.tuple.Pair;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.common.base.Objects;
  import com.google.common.cache.Cache;
  import com.google.common.cache.CacheBuilder;
  import com.google.common.collect.Maps;
  import com.google.inject.Inject;
  import com.google.inject.Provider;
  import com.google.inject.Singleton;
  import com.google.inject.persist.Transactional;
  
  /**
   * Helper class that works with config traversals.
   */
  @Singleton
  public class ConfigHelper {
  
    private Clusters clusters = null;
    private AmbariMetaInfo ambariMetaInfo = null;
    private ClusterDAO clusterDAO = null;
    private static final String DELETED = "DELETED_";
    public static final String CLUSTER_DEFAULT_TAG = "tag";
    private final boolean STALE_CONFIGS_CACHE_ENABLED;
    private final int STALE_CONFIGS_CACHE_EXPIRATION_TIME;
  
    /**
     * Cache for storing stale config flags. Key for cache is hash of [actualConfigs, desiredConfigs, hostName, serviceName,
     * componentName].
     */
    private final Cache<Integer, Boolean> staleConfigsCache;
  
    /**
     * clusterId -> hostId -> serviceName -> serviceComponentName -> state map to reduce redundant updates sending.
     */
    private final Map<Long, Map<Long, Map<String, Map<String, Boolean>>>> stateCache = new HashMap<>();
  
    private final Cache<Integer, String> refreshConfigCommandCache;
  
    private static final Logger LOG =
        LoggerFactory.getLogger(ConfigHelper.class);
  
    /**
     * List of property prefixes and names. Please keep in alphabetical order.
     */
    public static final String HBASE_SITE = "hbase-site";
    public static final String HDFS_SITE = "hdfs-site";
    public static final String HIVE_SITE = "hive-site";
    public static final String YARN_SITE = "yarn-site";
    public static final String CLUSTER_ENV = "cluster-env";
    public static final String CLUSTER_ENV_ALERT_REPEAT_TOLERANCE = "alerts_repeat_tolerance";
    public static final String CLUSTER_ENV_RETRY_ENABLED = "command_retry_enabled";
    public static final String SERVICE_CHECK_TYPE = "service_check_type";
    public static final String CLUSTER_ENV_RETRY_COMMANDS = "commands_to_retry";
    public static final String CLUSTER_ENV_RETRY_MAX_TIME_IN_SEC = "command_retry_max_time_in_sec";
    public static final String COMMAND_RETRY_MAX_TIME_IN_SEC_DEFAULT = "600";
    public static final String CLUSTER_ENV_STACK_NAME_PROPERTY = "stack_name";
    public static final String CLUSTER_ENV_STACK_FEATURES_PROPERTY = "stack_features";
    public static final String CLUSTER_ENV_STACK_TOOLS_PROPERTY = "stack_tools";
    public static final String CLUSTER_ENV_STACK_ROOT_PROPERTY = "stack_root";
    public static final String CLUSTER_ENV_STACK_PACKAGES_PROPERTY = "stack_packages";
  
    public static final String HTTP_ONLY = "HTTP_ONLY";
    public static final String HTTPS_ONLY = "HTTPS_ONLY";
    public static final String SERVICE_CHECK_MINIMAL = "minimal";
  
    /**
     * The tag given to newly created versions.
     */
    public static final String FIRST_VERSION_TAG = "version1";
  
    @Inject
    private Provider<MetadataHolder> m_metadataHolder;
  
    @Inject
    private Provider<AgentConfigsHolder> m_agentConfigsHolder;
  
    @Inject
    private Provider<AmbariManagementControllerImpl> m_ambariManagementController;
  
    @Inject
    private STOMPUpdatePublisher STOMPUpdatePublisher;
  
    @Inject
    private ServiceConfigDAO serviceConfigDAO;
  
    @Inject
    public ConfigHelper(Clusters c, AmbariMetaInfo metaInfo, Configuration configuration, ClusterDAO clusterDAO) {
      clusters = c;
      ambariMetaInfo = metaInfo;
      this.clusterDAO = clusterDAO;
      STALE_CONFIGS_CACHE_ENABLED = configuration.isStaleConfigCacheEnabled();
      STALE_CONFIGS_CACHE_EXPIRATION_TIME = configuration.staleConfigCacheExpiration();
      staleConfigsCache = CacheBuilder.newBuilder().
          expireAfterWrite(STALE_CONFIGS_CACHE_EXPIRATION_TIME, TimeUnit.SECONDS).build();
  
      refreshConfigCommandCache = CacheBuilder.newBuilder().
              expireAfterWrite(STALE_CONFIGS_CACHE_EXPIRATION_TIME, TimeUnit.SECONDS).build();
    }
  
    /**
     * Gets the desired tags for a cluster and host
     *
     * @param cluster  the cluster
     * @param hostName the host name
     * @return a map of tag type to tag names with overrides
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> getEffectiveDesiredTags(
            Cluster cluster, String hostName) throws AmbariException {
  
      return getEffectiveDesiredTags(cluster, hostName, null);
    }
  
    /**
     * Gets the desired tags for a cluster and host
     *
     * @param cluster
     *          the cluster
     * @param hostName
     *          the host name
     * @return a map of tag type to tag names with overrides
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> getEffectiveDesiredTags(Cluster cluster, String hostName,
                                                                    @Nullable Map<String, DesiredConfig> desiredConfigs) throws AmbariException {
      Host host = null;
      if (hostName != null) {
        try {
          host = clusters.getHost(hostName);
        } catch (HostNotFoundException e) {
          LOG.error("Cannot get desired config for unknown host {}", hostName, e);
        }
      }
      Map<String, HostConfig> desiredHostConfigs = (host == null) ? null
          : host.getDesiredHostConfigs(cluster, desiredConfigs);
  
      return getEffectiveDesiredTags(cluster, desiredConfigs, desiredHostConfigs);
    }
  
    /**
     * Gets the desired tags for a cluster and overrides for a host
     *
     * @param cluster
     *          the cluster
     * @param hostConfigOverrides
     *          the host overrides applied using config groups
     * @param clusterDesired
     *          the desired configurations for the cluster. Obtaining these can be
     *          expensive, ans since this method could be called 10,000's of times
     *          when generating cluster/host responses. Therefore, the caller
     *          should build these once and pass them in. If {@code null}, then
     *          this method will retrieve them at runtime, incurring a performance
     *          penality.
     * @return a map of tag type to tag names with overrides
     */
    private Map<String, Map<String, String>> getEffectiveDesiredTags(Cluster cluster, Map<String,
            DesiredConfig> clusterDesired, Map<String, HostConfig> hostConfigOverrides) {
  
      if (null == cluster) {
        clusterDesired = new HashMap<>();
      }
  
      // per method contract, lookup if not supplied
      if (null == clusterDesired) {
        clusterDesired = cluster.getDesiredConfigs();
      }
  
      if (null == clusterDesired) {
        clusterDesired = new HashMap<>();
      }
  
      Map<String, Map<String, String>> resolved = new TreeMap<>();
  
      // Do not use host component config mappings.  Instead, the rules are:
      // 1) Use the cluster desired config
      // 2) override (1) with config-group overrides
  
      for (Entry<String, DesiredConfig> clusterEntry : clusterDesired.entrySet()) {
        String type = clusterEntry.getKey();
        String tag = clusterEntry.getValue().getTag();
  
        // 1) start with cluster config
        if (cluster != null) {
          Config config = cluster.getConfig(type, tag);
          if (null == config) {
            continue;
          }
  
          Map<String, String> tags = new LinkedHashMap<>();
  
          tags.put(CLUSTER_DEFAULT_TAG, config.getTag());
  
          // AMBARI-3672. Only consider Config groups for override tags
          // tags -> (configGroupId, versionTag)
          if (hostConfigOverrides != null) {
            HostConfig hostConfig = hostConfigOverrides.get(config.getType());
            if (hostConfig != null) {
              for (Entry<Long, String> tagEntry : hostConfig
                      .getConfigGroupOverrides().entrySet()) {
                tags.put(tagEntry.getKey().toString(), tagEntry.getValue());
              }
            }
          }
  
          resolved.put(type, tags);
        }
      }
  
      return resolved;
    }
  
  
    public Set<String> filterInvalidPropertyValues(Map<PropertyInfo, String> properties, String filteredListName) {
      Set<String> resultSet = new HashSet<>();
      for (Iterator<Entry<PropertyInfo, String>> iterator = properties.entrySet().iterator(); iterator.hasNext();) {
        Entry<PropertyInfo, String> property = iterator.next();
        PropertyInfo propertyInfo = property.getKey();
        String propertyValue = property.getValue();
        if (propertyValue == null || propertyValue.toLowerCase().equals("null") || propertyValue.isEmpty()) {
          LOG.error(String.format("Excluding property %s from %s, because of invalid or empty value!", propertyInfo.getName(), filteredListName));
          iterator.remove();
        } else {
          resultSet.add(propertyValue);
        }
      }
      return resultSet;
    }
  
    /**
     * Get all config properties for a cluster given a set of configType to
     * versionTags map. This helper method merges all the override tags with a
     * the properties from parent cluster config properties
     *
     * @param cluster
     * @param desiredTags
     * @return {type : {key, value}}
     */
    public Map<String, Map<String, String>> getEffectiveConfigProperties(
        Cluster cluster, Map<String, Map<String, String>> desiredTags) {
  
      Map<String, Map<String, String>> properties = new HashMap<>();
  
      if (desiredTags != null) {
        for (Entry<String, Map<String, String>> entry : desiredTags.entrySet()) {
          String type = entry.getKey();
          Map<String, String> propertyMap = properties.get(type);
          if (propertyMap == null) {
            propertyMap = new HashMap<>();
          }
  
          Map<String, String> tags = new HashMap<>(entry.getValue());
          String clusterTag = tags.get(CLUSTER_DEFAULT_TAG);
  
          // Overrides is only supported if the config type exists at cluster
          // level
          if (clusterTag != null) {
            Config config = cluster.getConfig(type, clusterTag);
            if (config != null) {
              propertyMap.putAll(config.getProperties());
            }
            tags.remove(CLUSTER_DEFAULT_TAG);
            // Now merge overrides
            for (Entry<String, String> overrideEntry : tags.entrySet()) {
              Config overrideConfig = cluster.getConfig(type,
                  overrideEntry.getValue());
  
              if (overrideConfig != null) {
                propertyMap = getMergedConfig(propertyMap, overrideConfig.getProperties());
              }
            }
          }
          properties.put(type, propertyMap);
        }
      }
  
      return properties;
    }
  
    public Map<String, Map<String, String>> getEffectiveConfigProperties(String clusterName, String hostName) throws AmbariException {
      Cluster cluster = clusters.getCluster(clusterName);
      return getEffectiveConfigProperties(cluster, getEffectiveDesiredTags(cluster, hostName));
    }
  
    /**
     * Get all config attributes for a cluster given a set of configType to
     * versionTags map. This helper method merges all the override tags with a
     * the attributes from parent cluster config properties
     *
     * @param cluster
     * @param desiredTags
     * @return {type : {attribute : {property, attributeValue}}
     */
    public Map<String, Map<String, Map<String, String>>> getEffectiveConfigAttributes(
        Cluster cluster, Map<String, Map<String, String>> desiredTags) {
  
      Map<String, Map<String, Map<String, String>>> attributes = new HashMap<>();
  
      if (desiredTags != null) {
        for (Entry<String, Map<String, String>> entry : desiredTags.entrySet()) {
  
          String type = entry.getKey();
          Map<String, Map<String, String>> attributesMap = null;
  
          Map<String, String> tags = new HashMap<>(entry.getValue());
          String clusterTag = tags.get(CLUSTER_DEFAULT_TAG);
  
          if (clusterTag != null) {
            Config config = cluster.getConfig(type, clusterTag);
            if (config != null) {
              attributesMap = new TreeMap<>();
              cloneAttributesMap(config.getPropertiesAttributes(), attributesMap);
            }
            tags.remove(CLUSTER_DEFAULT_TAG);
          }
          for (Entry<String, String> overrideEntry : tags.entrySet()) {
            Config overrideConfig = cluster.getConfig(type,
                overrideEntry.getValue());
            overrideAttributes(overrideConfig, attributesMap);
          }
          if (attributesMap != null) {
            attributes.put(type, attributesMap);
          }
        }
      }
  
      return attributes;
    }
  
    /**
     * Merge override with original, if original property doesn't exist,
     * add it to the properties
     *
     * @param persistedClusterConfig
     * @param override
     * @return
     */
    public Map<String, String> getMergedConfig(Map<String,
        String> persistedClusterConfig, Map<String, String> override) {
  
      Map<String, String> finalConfig = new HashMap<>(persistedClusterConfig);
  
      if (override != null && override.size() > 0) {
        for (Entry<String, String> entry : override.entrySet()) {
          Boolean deleted = 0 == entry.getKey().indexOf(DELETED);
          String nameToUse = deleted ?
              entry.getKey().substring(DELETED.length()) : entry.getKey();
          if (finalConfig.containsKey(nameToUse)) {
            finalConfig.remove(nameToUse);
          }
          if (!deleted) {
            finalConfig.put(nameToUse, entry.getValue());
          }
        }
      }
  
      return finalConfig;
    }
  
    /**
     * Retrieves effective configurations for specified cluster and tags and merge them with
     * present before in {@code configurations}
     * @param configurations configurations will be merged with effective cluster configurations
     * @param configurationTags configuration tags for cluster's desired configs
     * @param cluster cluster to configs retrieving
     */
    public void getAndMergeHostConfigs(Map<String, Map<String, String>> configurations,
                                       Map<String, Map<String, String>> configurationTags,
                                       Cluster cluster) {
      if (null != configurationTags && !configurationTags.isEmpty()) {
        Map<String, Map<String, String>> configProperties =
            getEffectiveConfigProperties(cluster, configurationTags);
  
        // Apply the configurations present before on top of derived configs
        for (Map.Entry<String, Map<String, String>> entry : configProperties.entrySet()) {
          String type = entry.getKey();
          Map<String, String> allLevelMergedConfig = entry.getValue();
  
          if (configurations.containsKey(type)) {
            Map<String, String> mergedConfig = getMergedConfig(allLevelMergedConfig,
                configurations.get(type));
  
            configurations.get(type).clear();
            configurations.get(type).putAll(mergedConfig);
  
          } else {
            configurations.put(type, new HashMap<>());
            configurations.get(type).putAll(allLevelMergedConfig);
          }
        }
      }
    }
  
    /**
     * Retrieves effective configuration attributes for specified cluster and tags and merge them with
     * present before in {@code configurationAttributes}
     * @param configurationAttributes configuration attributes will be merged with effective ones on the cluster
     * @param configurationTags  configuration tags for cluster's desired configs
     * @param cluster cluster to config attributes retrieving
     */
    public void getAndMergeHostConfigAttributes(Map<String, Map<String, Map<String, String>>> configurationAttributes,
                                       Map<String, Map<String, String>> configurationTags,
                                       Cluster cluster) {
      if (null != configurationTags && !configurationTags.isEmpty()) {
        Map<String, Map<String, Map<String, String>>> configAttributes =
            getEffectiveConfigAttributes(cluster, configurationTags);
  
        for (Map.Entry<String, Map<String, Map<String, String>>> attributesOccurrence : configAttributes.entrySet()) {
          String type = attributesOccurrence.getKey();
          Map<String, Map<String, String>> attributes = attributesOccurrence.getValue();
  
          if (configurationAttributes != null) {
            if (!configurationAttributes.containsKey(type)) {
              configurationAttributes.put(type,
                  new TreeMap<>());
            }
            cloneAttributesMap(attributes,
                configurationAttributes.get(type));
          }
        }
      }
    }
  
    /**
     * Merge override attributes with original ones.
     * If overrideConfig#getPropertiesAttributes does not contain occurrence of override for any of
     * properties from overrideConfig#getProperties then persisted attribute should be removed.
     */
    public Map<String, Map<String, String>> overrideAttributes(Config overrideConfig,
                                                               Map<String, Map<String, String>> persistedAttributes) {
      if (overrideConfig != null && persistedAttributes != null) {
        Map<String, Map<String, String>> overrideAttributes = overrideConfig.getPropertiesAttributes();
        if (overrideAttributes != null) {
          cloneAttributesMap(overrideAttributes, persistedAttributes);
          Map<String, String> overrideProperties = overrideConfig.getProperties();
          if (overrideProperties != null) {
            Set<String> overriddenProperties = overrideProperties.keySet();
            for (String overriddenProperty : overriddenProperties) {
              for (Entry<String, Map<String, String>> persistedAttribute : persistedAttributes.entrySet()) {
                String attributeName = persistedAttribute.getKey();
                Map<String, String> persistedAttributeValues = persistedAttribute.getValue();
                Map<String, String> overrideAttributeValues = overrideAttributes.get(attributeName);
                if (overrideAttributeValues == null || !overrideAttributeValues.containsKey(overriddenProperty)) {
                  persistedAttributeValues.remove(overriddenProperty);
                }
              }
            }
          }
        }
      }
      return persistedAttributes;
    }
  
    public void cloneAttributesMap(Map<String, Map<String, String>> sourceAttributesMap,
                                   Map<String, Map<String, String>> targetAttributesMap) {
      if (sourceAttributesMap != null && targetAttributesMap != null) {
        for (Entry<String, Map<String, String>> attributesEntry : sourceAttributesMap.entrySet()) {
          String attributeName = attributesEntry.getKey();
          if (!targetAttributesMap.containsKey(attributeName)) {
            targetAttributesMap.put(attributeName, new TreeMap<>());
          }
          for (Entry<String, String> attributesValue : attributesEntry.getValue().entrySet()) {
            targetAttributesMap.get(attributeName).put(attributesValue.getKey(), attributesValue.getValue());
          }
        }
      }
    }
  
    public void applyCustomConfig(Map<String, Map<String, String>> configurations,
                                  String type, String name, String value, Boolean deleted) {
      if (!configurations.containsKey(type)) {
        configurations.put(type, new HashMap<>());
      }
      String nameToUse = deleted ? DELETED + name : name;
      Map<String, String> properties = configurations.get(type);
      if (properties.containsKey(nameToUse)) {
        properties.remove(nameToUse);
      }
      properties.put(nameToUse, value);
    }
  
    /**
     * The purpose of this method is to determine if a
     * {@link ServiceComponentHost}'s known actual configs are different than what
     * is set on the cluster (the desired). The following logic is applied:
     * <ul>
     * <li>Desired type does not exist on the SCH (actual)
     * <ul>
     * <li>Type does not exist on the stack: <code>false</code></li>
     * <li>Type exists on the stack: <code>true</code> if the config key is on the
     * stack. otherwise <code>false</code></li>
     * </ul>
     * </li>
     * <li>Desired type exists for the SCH
     * <ul>
     * <li>Desired tags already set for the SCH (actual): <code>false</code></li>
     * <li>Desired tags DO NOT match SCH: <code>true</code> if the changed keys
     * exist on the stack, otherwise <code>false</code></li>
     * </ul>
     * </li>
     * </ul>
     *
     * @param sch
     *          the SCH to calcualte config staleness for (not {@code null}).
     * @param requestDesiredConfigs
     *          the desired configurations for the cluster. Obtaining these can be
     *          expensive and since this method operates on SCH's, it could be
     *          called 10,000's of times when generating cluster/host responses.
     *          Therefore, the caller should build these once and pass them in. If
     *          {@code null}, then this method will retrieve them at runtime,
     *          incurring a performance penality.
     *
     * @return <code>true</code> if the actual configs are stale
     */
    public boolean isStaleConfigs(ServiceComponentHost sch, Map<String, DesiredConfig> requestDesiredConfigs)
        throws AmbariException {
      HostComponentDesiredStateEntity hostComponentDesiredStateEntity = sch.getDesiredStateEntity();
      return isStaleConfigs(sch, requestDesiredConfigs, hostComponentDesiredStateEntity);
    }
  
    public boolean isStaleConfigs(ServiceComponentHost sch, Map<String, DesiredConfig> requestDesiredConfigs,
                                  HostComponentDesiredStateEntity hostComponentDesiredStateEntity)
            throws AmbariException {
      boolean stale = calculateIsStaleConfigs(sch, requestDesiredConfigs, hostComponentDesiredStateEntity);
      if (LOG.isDebugEnabled()) {
        LOG.debug("Cache configuration staleness for host {} and component {} as {}",
                sch.getHostName(), sch.getServiceComponentName(), stale);
      }
      return stale;
    }
  
    /**
     * Remove configs by type
     *
     * @param type config Type
     */
    @Transactional
    public void removeConfigsByType(Cluster cluster, String type) {
      Set<String> globalVersions = cluster.getConfigsByType(type).keySet();
  
      for (String version : globalVersions) {
        ClusterConfigEntity clusterConfigEntity = clusterDAO.findConfig
            (cluster.getClusterId(), type, version);
  
        clusterDAO.removeConfig(clusterConfigEntity);
      }
    }
  
    /**
     * Gets all the config dictionary where property with the given name is present in stack definitions
     *
     * @param stackId
     * @param propertyName
     */
    public Set<String> findConfigTypesByPropertyName(StackId stackId, String propertyName, String clusterName) throws AmbariException {
      StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(),
                                                stackId.getStackVersion());
  
      Set<String> result = new HashSet<>();
  
      for (Service service : clusters.getCluster(clusterName).getServices().values()) {
        Set<PropertyInfo> stackProperties = ambariMetaInfo.getServiceProperties(stack.getName(), stack.getVersion(), service.getName());
        Set<PropertyInfo> stackLevelProperties = ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion());
        stackProperties.addAll(stackLevelProperties);
  
        for (PropertyInfo stackProperty : stackProperties) {
          if (stackProperty.getName().equals(propertyName)) {
            String configType = fileNameToConfigType(stackProperty.getFilename());
  
            result.add(configType);
          }
        }
      }
  
      return result;
    }
  
    /**
     * Gets a map of config types to password property names to password property value names,
     * that are credential store enabled.
     *
     * @param stackId
     * @param service
     * @return
     * @throws AmbariException
       */
    public Map<String, Map<String, String>> getCredentialStoreEnabledProperties(StackId stackId, Service service)
            throws AmbariException {
      PropertyType propertyType = PropertyType.PASSWORD;
      StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
      Map<String, Map<String, String>> result = new HashMap<>();
      Map<String, String> passwordProperties;
      Set<PropertyInfo> serviceProperties = ambariMetaInfo.getServiceProperties(stack.getName(), stack.getVersion(), service.getName());
      for (PropertyInfo serviceProperty : serviceProperties) {
        if (serviceProperty.getPropertyTypes().contains(propertyType)) {
          if (!serviceProperty.getPropertyValueAttributes().isKeyStore()) {
            continue;
          }
          String stackPropertyConfigType = fileNameToConfigType(serviceProperty.getFilename());
          passwordProperties = result.get(stackPropertyConfigType);
          if (passwordProperties == null) {
            passwordProperties = new HashMap<>();
            result.put(stackPropertyConfigType, passwordProperties);
          }
          // If the password property is used by another property, it means the password property
          // is a password value name while the use is the password alias name. If the user property
          // is from another config type, include that in the password alias name as name:type.
          if (serviceProperty.getUsedByProperties().size() > 0) {
            for (PropertyDependencyInfo usedByProperty : serviceProperty.getUsedByProperties()) {
              String propertyName = usedByProperty.getName();
              if (!StringUtils.isEmpty(usedByProperty.getType())) {
                propertyName += ':' + usedByProperty.getType();
              }
              passwordProperties.put(propertyName, serviceProperty.getName());
            }
          }
          else {
            passwordProperties.put(serviceProperty.getName(), serviceProperty.getName());
          }
        }
      }
  
      return result;
    }
  
    /***
     * Fetch user to group mapping from the cluster configs. UserGroupEntries contain information regarding the group that the user is associated to.
     * @param stackId
     * @param cluster
     * @param desiredConfigs
     * @return
     * @throws AmbariException
     */
    public Map<String, Set<String>> createUserGroupsMap(StackId stackId,
                                                        Cluster cluster, Map<String, DesiredConfig> desiredConfigs) throws AmbariException {
      StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
      Map<String, ServiceInfo> servicesMap = ambariMetaInfo.getServices(stack.getName(), stack.getVersion());
      Set<PropertyInfo> stackProperties = ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion());
      return createUserGroupsMap(cluster, desiredConfigs, servicesMap, stackProperties);
    }
  
    /***
     * Fetch user to group mapping from the cluster configs. UserGroupEntries contain information regarding the group that the user is associated to.
     * @param cluster
     * @param desiredConfigs
     * @param servicesMap
     * @param stackProperties
     * @return
     * @throws AmbariException
     */
    public Map<String, Set<String>> createUserGroupsMap(
      Cluster cluster, Map<String, DesiredConfig> desiredConfigs,
      Map<String, ServiceInfo> servicesMap, Set<PropertyInfo> stackProperties) throws AmbariException {
  
      Map<String, Set<String>> userGroupsMap = new HashMap<>();
      Map<PropertyInfo, String> userProperties = getPropertiesWithPropertyType(
        PropertyType.USER, cluster, desiredConfigs, servicesMap, stackProperties);
      Map<PropertyInfo, String> groupProperties = getPropertiesWithPropertyType(
        PropertyType.GROUP, cluster, desiredConfigs, servicesMap, stackProperties);
  
      if(userProperties != null && groupProperties != null) {
        for(Map.Entry<PropertyInfo, String> userProperty : userProperties.entrySet()) {
          PropertyInfo userPropertyInfo = userProperty.getKey();
          String userPropertyValue = userProperty.getValue();
          if(userPropertyInfo.getPropertyValueAttributes() != null
            && userPropertyInfo.getPropertyValueAttributes().getUserGroupEntries() != null) {
            Set<String> groupPropertyValues = new HashSet<>();
            Collection<UserGroupInfo> userGroupEntries = userPropertyInfo.getPropertyValueAttributes().getUserGroupEntries();
            for (UserGroupInfo userGroupInfo : userGroupEntries) {
              boolean found = false;
              for(Map.Entry<PropertyInfo, String> groupProperty : groupProperties.entrySet()) {
                PropertyInfo groupPropertyInfo = groupProperty.getKey();
                String groupPropertyValue = groupProperty.getValue();
                if(StringUtils.equals(userGroupInfo.getType(),
                  ConfigHelper.fileNameToConfigType(groupPropertyInfo.getFilename()))
                  && StringUtils.equals(userGroupInfo.getName(), groupPropertyInfo.getName())) {
                  groupPropertyValues.add(groupPropertyValue);
                  found = true;
                }
              }
  
              if(!found) {
                //Log error if the user-group mapping is not found
                LOG.error("User group mapping property {" + userGroupInfo.getType() + "/" + userGroupInfo.getName() + "} is missing for user property {" + ConfigHelper.fileNameToConfigType(userPropertyInfo.getFilename()) + "/" + userPropertyInfo.getName() + "} (username = " + userPropertyInfo.getValue() +")");
              }
            }
            userGroupsMap.put(userPropertyValue, groupPropertyValues);
          }
        }
      }
      return userGroupsMap;
    }
  
    /***
     * Fetch all the properties of a given PropertyType. For eg: Fetch all cluster configs that are of type "user"
     * @param stackId
     * @param propertyType
     * @param cluster
     * @param desiredConfigs
     * @return
     * @throws AmbariException
     */
    public Map<PropertyInfo, String> getPropertiesWithPropertyType(StackId stackId, PropertyType propertyType,
                                                                   Cluster cluster, Map<String, DesiredConfig> desiredConfigs) throws AmbariException {
      StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
      Map<String, ServiceInfo> servicesMap = ambariMetaInfo.getServices(stack.getName(), stack.getVersion());
      Set<PropertyInfo> stackProperties = ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion());
  
      return getPropertiesWithPropertyType(propertyType, cluster, desiredConfigs, servicesMap, stackProperties);
    }
  
    /***
     * Fetch all the properties of a given PropertyType. For eg: Fetch all cluster configs that are of type "user"
     * @param propertyType
     * @param cluster
     * @param desiredConfigs
     * @param servicesMap
     * @param stackProperties
     * @return
     */
    public Map<PropertyInfo, String> getPropertiesWithPropertyType(PropertyType propertyType, Cluster cluster,
                                                                   Map<String, DesiredConfig> desiredConfigs, Map<String, ServiceInfo> servicesMap,
                                                                   Set<PropertyInfo> stackProperties) throws AmbariException {
      Map<String, Config> actualConfigs = new HashMap<>();
      Map<PropertyInfo, String> result = new HashMap<>();
  
      for (Map.Entry<String, DesiredConfig> desiredConfigEntry : desiredConfigs.entrySet()) {
        String configType = desiredConfigEntry.getKey();
        DesiredConfig desiredConfig = desiredConfigEntry.getValue();
        actualConfigs.put(configType, cluster.getConfig(configType, desiredConfig.getTag()));
      }
  
      for (Service service : cluster.getServices().values()) {
        ServiceInfo serviceInfo = servicesMap.get(service.getName());
        if (serviceInfo == null) {
          continue;
        }
        Set<PropertyInfo> serviceProperties = new HashSet<>(serviceInfo.getProperties());
        for (PropertyInfo serviceProperty : serviceProperties) {
          if (serviceProperty.getPropertyTypes().contains(propertyType)) {
            String stackPropertyConfigType = fileNameToConfigType(serviceProperty.getFilename());
            try {
              String property = actualConfigs.get(stackPropertyConfigType).getProperties().get(serviceProperty.getName());
              if (null == property){
                LOG.error(String.format("Unable to obtain property values for %s with property attribute %s. "
                    + "The property does not exist in version %s of %s configuration.",
                  serviceProperty.getName(),
                  propertyType,
                  desiredConfigs.get(stackPropertyConfigType),
                  stackPropertyConfigType
                ));
              } else {
                result.put(serviceProperty, property);
              }
            } catch (Exception ignored) {
            }
          }
        }
      }
  
  
      for (PropertyInfo stackProperty : stackProperties) {
        if (stackProperty.getPropertyTypes().contains(propertyType)) {
          String stackPropertyConfigType = fileNameToConfigType(stackProperty.getFilename());
          if (actualConfigs.containsKey(stackPropertyConfigType)) {
            result.put(stackProperty, actualConfigs.get(stackPropertyConfigType).getProperties().get(stackProperty.getName()));
          }
        }
      }
  
      return result;
    }
  
    /***
     * Fetch all the property values of a given PropertyType. For eg: Fetch all cluster configs that are of type "user"
     * @param stackId
     * @param propertyType
     * @param cluster
     * @param desiredConfigs
     * @return
     * @throws AmbariException
     */
    public Set<String> getPropertyValuesWithPropertyType(StackId stackId, PropertyType propertyType,
                                                         Cluster cluster, Map<String,
                                                         DesiredConfig> desiredConfigs) throws AmbariException {
      StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
      Map<String, ServiceInfo> servicesMap = ambariMetaInfo.getServices(stack.getName(), stack.getVersion());
      Set<PropertyInfo> stackProperties = ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion());
  
      return getPropertyValuesWithPropertyType(propertyType, cluster, desiredConfigs, servicesMap, stackProperties);
    }
  
    /***
     * Fetch all the property values of a given PropertyType. For eg: Fetch all cluster configs that are of type "user"
     * @param propertyType
     * @param cluster
     * @param desiredConfigs
     * @param servicesMap
     * @param stackProperties
     * @return
     * @throws AmbariException
     */
    public Set<String> getPropertyValuesWithPropertyType(PropertyType propertyType,
                                                         Cluster cluster, Map<String, DesiredConfig> desiredConfigs,
                                                         Map<String, ServiceInfo> servicesMap,
                                                         Set<PropertyInfo> stackProperties) throws AmbariException {
      Map<String, Config> actualConfigs = new HashMap<>();
      Set<String> result = new HashSet<>();
  
      for (Map.Entry<String, DesiredConfig> desiredConfigEntry : desiredConfigs.entrySet()) {
        String configType = desiredConfigEntry.getKey();
        DesiredConfig desiredConfig = desiredConfigEntry.getValue();
        actualConfigs.put(configType, cluster.getConfig(configType, desiredConfig.getTag()));
      }
  
      for (Service service : cluster.getServices().values()) {
        // !!! the services map is from the stack, which may not contain services in the cluster.
        // This is the case for upgrades where the target stack may remove services
        if (!servicesMap.containsKey(service.getName())) {
          continue;
        }
  
        Set<PropertyInfo> serviceProperties = new HashSet<>(servicesMap.get(service.getName()).getProperties());
        for (PropertyInfo serviceProperty : serviceProperties) {
          if (serviceProperty.getPropertyTypes().contains(propertyType)) {
            String stackPropertyConfigType = fileNameToConfigType(serviceProperty.getFilename());
            try {
              String property = actualConfigs.get(stackPropertyConfigType).getProperties().get(serviceProperty.getName());
              if (null == property){
                LOG.error(String.format("Unable to obtain property values for %s with property attribute %s. "
                    + "The property does not exist in version %s of %s configuration.",
                    serviceProperty.getName(),
                    propertyType,
                    desiredConfigs.get(stackPropertyConfigType),
                    stackPropertyConfigType
                    ));
              } else {
                result.add(property);
              }
            } catch (Exception ignored) {
            }
          }
        }
      }
  
  
      for (PropertyInfo stackProperty : stackProperties) {
        if (stackProperty.getPropertyTypes().contains(propertyType)) {
          String stackPropertyConfigType = fileNameToConfigType(stackProperty.getFilename());
          if (actualConfigs.containsKey(stackPropertyConfigType)) {
            result.add(actualConfigs.get(stackPropertyConfigType).getProperties().get(stackProperty.getName()));
          }
        }
      }
  
      return result;
    }
  
    public void checkAllStageConfigsPresentInDesiredConfigs(Cluster cluster) throws AmbariException {
      StackId stackId = cluster.getDesiredStackVersion();
      Set<String> stackConfigTypes = ambariMetaInfo.getStack(stackId.getStackName(),
              stackId.getStackVersion()).getConfigTypeAttributes().keySet();
      Map<String, Config> actualConfigs = new HashMap<>();
      Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs();
  
      for (Map.Entry<String, DesiredConfig> desiredConfigEntry : desiredConfigs.entrySet()) {
        String configType = desiredConfigEntry.getKey();
        DesiredConfig desiredConfig = desiredConfigEntry.getValue();
        actualConfigs.put(configType, cluster.getConfig(configType, desiredConfig.getTag()));
      }
  
      for (String stackConfigType : stackConfigTypes) {
        if (!actualConfigs.containsKey(stackConfigType)) {
          LOG.error(String.format("Unable to find stack configuration %s in ambari configs!", stackConfigType));
        }
      }
  
    }
  
    /***
     * Fetch all the config values of a given PropertyType. For eg: Fetch all stack configs that are of type "user"
     * @param cluster
     * @param configType
     * @param propertyName
     * @return
     * @throws AmbariException
     */
    public String getPropertyValueFromStackDefinitions(Cluster cluster, String configType, String propertyName) throws AmbariException {
  
      Set<StackId> stackIds = new HashSet<>();
  
      for (Service service : cluster.getServices().values()) {
        stackIds.add(service.getDesiredStackId());
      }
  
      for (StackId stackId : stackIds) {
  
        StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(),
            stackId.getStackVersion());
  
        for (ServiceInfo serviceInfo : stack.getServices()) {
          Set<PropertyInfo> serviceProperties = ambariMetaInfo.getServiceProperties(stack.getName(), stack.getVersion(), serviceInfo.getName());
          Set<PropertyInfo> stackProperties = ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion());
          serviceProperties.addAll(stackProperties);
  
          for (PropertyInfo stackProperty : serviceProperties) {
            String stackPropertyConfigType = fileNameToConfigType(stackProperty.getFilename());
  
            if (stackProperty.getName().equals(propertyName) && stackPropertyConfigType.equals(configType)) {
              return stackProperty.getValue();
            }
          }
        }
      }
  
      return null;
    }
  
    /**
     * Gets the configuration value referenced by the specified placeholder from
     * the cluster configuration. This will take a configuration placeholder such
     * as {{hdfs-site/foo}} and return the value of {@code foo} defined in
     * {@code hdfs-site}.
     *
     * @param cluster     the cluster to use when rendering the placeholder value (not
     *                    {@code null}).
     * @param placeholder the placeholder value, such as {{hdfs-site/foobar}} (not
     *                    {@code null} )
     * @return the configuration value, or {@code null} if none.
     * @throws AmbariException if there was a problem parsing the placeholder or retrieving the
     *                         referenced value.
     */
    public String getPlaceholderValueFromDesiredConfigurations(Cluster cluster,
                                                               String placeholder) {
      // remove the {{ and }} from the placholder
      if (placeholder.startsWith("{{") && placeholder.endsWith("}}")) {
        placeholder = placeholder.substring(2, placeholder.length() - 2).trim();
      }
  
      // break up hdfs-site/foobar into hdfs-site and foobar
      int delimiterPosition = placeholder.indexOf("/");
      if (delimiterPosition < 0) {
        return placeholder;
      }
  
      String configType = placeholder.substring(0, delimiterPosition);
      String propertyName = placeholder.substring(delimiterPosition + 1,
          placeholder.length());
  
      // return the value if it exists, otherwise return the placeholder
      String value = getValueFromDesiredConfigurations(cluster, configType, propertyName);
      return value != null ? value : placeholder;
    }
  
    public String getValueFromDesiredConfigurations(Cluster cluster, String configType, String propertyName) {
      Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs();
      DesiredConfig desiredConfig = desiredConfigs.get(configType);
      if(desiredConfig != null) {
        Config config = cluster.getConfig(configType, desiredConfig.getTag());
        Map<String, String> configurationProperties = config.getProperties();
        if (null != configurationProperties) {
          String value = configurationProperties.get(propertyName);
          if (null != value) {
            return value;
          }
        }
      }
      return null;
    }
  
    public ServiceInfo getPropertyOwnerService(Cluster cluster, String configType, String propertyName) throws AmbariException {
  
      for (Service service : cluster.getServices().values()) {
        StackId stackId = service.getDesiredStackId();
        StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
  
        for (ServiceInfo serviceInfo : stack.getServices()) {
          Set<PropertyInfo> serviceProperties = ambariMetaInfo.getServiceProperties(stack.getName(), stack.getVersion(), serviceInfo.getName());
  
          for (PropertyInfo stackProperty : serviceProperties) {
            String stackPropertyConfigType = fileNameToConfigType(stackProperty.getFilename());
  
            if (stackProperty.getName().equals(propertyName) && stackPropertyConfigType.equals(configType)) {
              return serviceInfo;
            }
          }
        }
      }
  
      return null;
    }
  
    public Set<PropertyInfo> getServiceProperties(Cluster cluster, String serviceName) throws AmbariException {
      // The original implementation of this method is to return all properties regardless of whether
      // they should be excluded or not.  By setting removeExcluded to false in the method invocation
      // below, no attempt will be made to remove properties that exist in excluded types.
      Service service = cluster.getService(serviceName);
  
      return getServiceProperties(service.getDesiredStackId(), serviceName, false);
    }
  
    /**
     * Retrieves a Set of PropertyInfo objects containing the relevant properties for the requested
     * service.
     * <p/>
     * If <code>removeExcluded</code> is <code>true</code>, the service's excluded configuration types
     * are used to prune off PropertyInfos that should be ignored; else if <code>false</code>, all
     * PropertyInfos will be returned.
     *
     * @param stackId        a StackId declaring the relevant stack
     * @param serviceName    a String containing the requested service's name
     * @param removeExcluded a boolean value indicating whether to remove properties from excluded
     *                       configuration types (<code>true</code>) or return the complete set of properties regardless of exclusions (<code>false</code>)
     * @return a Set of PropertyInfo objects for the requested service
     * @throws AmbariException if the requested stack or the requested service is not found
     */
    public Set<PropertyInfo> getServiceProperties(StackId stackId, String serviceName, boolean removeExcluded)
        throws AmbariException {
      ServiceInfo service = ambariMetaInfo.getService(stackId.getStackName(), stackId.getStackVersion(), serviceName);
      Set<PropertyInfo> properties = new HashSet<>(service.getProperties());
  
      if (removeExcluded) {
        Set<String> excludedConfigTypes = service.getExcludedConfigTypes();
  
        // excludedConfigTypes can be null since org.apache.ambari.server.state.ServiceInfo.setExcludedConfigTypes()
        // allows for null values
        if ((excludedConfigTypes != null) && !excludedConfigTypes.isEmpty()) {
          // Iterate through the set of found PropertyInfo instances and remove ones that should be
          // excluded.
          Iterator<PropertyInfo> iterator = properties.iterator();
  
          while (iterator.hasNext()) {
            PropertyInfo propertyInfo = iterator.next();
  
            // If the config type for the current PropertyInfo is containing within an excluded type,
            // remove it from the set of properties being returned
            if (excludedConfigTypes.contains(ConfigHelper.fileNameToConfigType(propertyInfo.getFilename()))) {
              iterator.remove();
            }
          }
        }
      }
  
      return properties;
    }
  
    public Set<PropertyInfo> getStackProperties(Cluster cluster) throws AmbariException {
  
      Set<StackId> stackIds = new HashSet<>();
      for (Service service : cluster.getServices().values()) {
        stackIds.add(service.getDesiredStackId());
      }
  
      Set<PropertyInfo> propertySets = new HashSet<>();
  
      for (StackId stackId : stackIds) {
        StackInfo stack = ambariMetaInfo.getStack(stackId.getStackName(), stackId.getStackVersion());
        propertySets.addAll(ambariMetaInfo.getStackProperties(stack.getName(), stack.getVersion()));
      }
  
      return propertySets;
    }
  
    public void updateConfigType(Cluster cluster, StackId stackId, AmbariManagementController controller,
                                 String configType, Map<String, String> updates,
                                 Collection<String> removals,
                                 String authenticatedUserName,
                                 String serviceVersionNote)  throws AmbariException {
      updateConfigType(cluster, stackId, controller, configType, updates, removals, authenticatedUserName,
              serviceVersionNote, null, true);
    }
  
    public void updateBulkConfigType(Cluster cluster, StackId stackId, AmbariManagementController controller,
                                     Iterable<String> configTypes,
                                     Map<String, Map<String, String>> updates,
                                     Map<String, Collection<String>> removals,
                                     String authenticatedUserName,
                                     String serviceVersionNote) throws AmbariException, AmbariRuntimeException {
  
      Map<String, DesiredConfig> desiredConfig =  cluster.getDesiredConfigs();
  
      Boolean[] doUpdateAgentConfigs = {false};
      LOG.info("Bulk config update. Starting...");
      configTypes.forEach(configType -> {
        try {
          Boolean updated = updateConfigType(
                  cluster, stackId, controller,
                  configType,
                  updates.get(configType),
                  removals.get(configType),
                  authenticatedUserName,
                  serviceVersionNote,
                  desiredConfig,
                  false
          );
          LOG.info("Bulk config update. Working with {}...{}", configType, updated ? "updated" : "not updated");
          doUpdateAgentConfigs[0] = doUpdateAgentConfigs[0] || updated;
        } catch (AmbariException e) {
          throw new AmbariRuntimeException(e);
        }
      });
  
      LOG.info("Bulk config update, agent update is {} required", (doUpdateAgentConfigs[0]) ? "" : "not");
  
      if (doUpdateAgentConfigs[0]) {
        updateAgentConfigs(Collections.singleton(cluster.getClusterName()));
      }
    }
  
    /**
     * A helper method to create a new {@link Config} for a given configuration
     * type and updates to the current values, if any. This method will perform the following tasks:
     * <ul>
     * <li>Merge the specified updates with the properties of the current version of the
     * configuration</li>
     * <li>Create a {@link Config} in the cluster for the specified type. This
     * will have the proper versions and tags set automatically.</li>
     * <li>Set the cluster's {@link DesiredConfig} to the new configuration</li>
     * <li>Create an entry in the configuration history with a note and username.</li>
     * <ul>
     *
     * @throws AmbariException
     */
    public Boolean updateConfigType(Cluster cluster, StackId stackId, AmbariManagementController controller,
                                    String configType, Map<String, String> updates,
                                    Collection<String> removals,
                                    String authenticatedUserName,
                                    String serviceVersionNote,
                                    @Nullable Map<String, DesiredConfig> desiredConfig,
                                    Boolean doUpdateAgentConfigs) throws AmbariException {
  
      // Nothing to update or remove
      if (configType == null ||
        (updates == null || updates.isEmpty()) &&
        (removals == null || removals.isEmpty())) {
        return false;
      }
  
      Config oldConfig = (desiredConfig != null && desiredConfig.containsKey(configType))
              ? cluster.getConfig(configType, desiredConfig.get(configType).getTag())
              : cluster.getDesiredConfigByType(configType);
      Map<String, String> oldConfigProperties;
      Map<String, String> properties = new HashMap<>();
      Map<String, Map<String, String>> propertiesAttributes = new HashMap<>();
  
      if (oldConfig == null) {
        oldConfigProperties = null;
      } else {
        oldConfigProperties = oldConfig.getProperties();
        if (oldConfigProperties != null) {
          properties.putAll(oldConfigProperties);
        }
        if (oldConfig.getPropertiesAttributes() != null) {
          propertiesAttributes.putAll(oldConfig.getPropertiesAttributes());
        }
      }
  
      if (updates != null) {
        properties.putAll(updates);
      }
  
      // Remove properties that need to be removed.
      if (removals != null) {
        for (String propertyName : removals) {
          properties.remove(propertyName);
          for (Map<String, String> attributesMap: propertiesAttributes.values()) {
            attributesMap.remove(propertyName);
          }
        }
      }
  
      if ((oldConfigProperties == null || !Maps.difference(oldConfigProperties, properties).areEqual())
              && createConfigType(cluster, stackId, controller, configType, properties, propertiesAttributes,
              authenticatedUserName, serviceVersionNote)) {
        if (doUpdateAgentConfigs) {
          updateAgentConfigs(Collections.singleton(cluster.getClusterName()));
        }
  
        return true;
      }
  
      return false;
    }
  
    public void createConfigType(Cluster cluster, StackId stackId,
        AmbariManagementController controller, String configType, Map<String, String> properties,
        String authenticatedUserName, String serviceVersionNote) throws AmbariException {
  
      if (createConfigType(cluster, stackId, controller, configType, properties,
        new HashMap<>(), authenticatedUserName, serviceVersionNote)) {
  
        updateAgentConfigs(Collections.singleton(cluster.getClusterName()));
      }
    }
  
    public boolean createConfigType(Cluster cluster, StackId stackId,
        AmbariManagementController controller, String configType, Map<String, String> properties,
        Map<String, Map<String, String>> propertyAttributes, String authenticatedUserName,
        String serviceVersionNote) throws AmbariException {
  
      // create the configuration history entry
      Config baseConfig = createConfig(cluster, stackId, controller, configType, FIRST_VERSION_TAG,
          properties, propertyAttributes);
  
      if (baseConfig != null) {
        cluster.addDesiredConfig(authenticatedUserName,
            Collections.singleton(baseConfig), serviceVersionNote);
        return true;
      }
      return false;
    }
  
    /**
     * Create configurations and assign them for services.
     * @param cluster               the cluster
     * @param controller            the controller
     * @param stackId               the stack to create the new properties for
     * @param batchProperties       the type->config map batch of properties
     * @param authenticatedUserName the user that initiated the change
     * @param serviceVersionNote    the service version note
     * @return true if configs were created
     * @throws AmbariException
     */
    public boolean createConfigTypes(Cluster cluster, StackId stackId,
        AmbariManagementController controller, Map<String, Map<String, String>> batchProperties,
        String authenticatedUserName, String serviceVersionNote) throws AmbariException {
  
      Map<String, Set<Config>> serviceMapped = new HashMap<>();
  
      for (Map.Entry<String, Map<String, String>> entry : batchProperties.entrySet()) {
        String type = entry.getKey();
        Map<String, String> properties = entry.getValue();
  
        Config baseConfig = createConfig(cluster, stackId, controller, type, FIRST_VERSION_TAG,
            properties, Collections.emptyMap());
  
        if (null != baseConfig) {
          try {
            String service = cluster.getServiceByConfigType(type);
            if (!serviceMapped.containsKey(service)) {
              serviceMapped.put(service, new HashSet<>());
            }
            serviceMapped.get(service).add(baseConfig);
          } catch (Exception e) {
            // !!! ignore
          }
        }
      }
  
      // create the configuration history entries
      boolean added = false;
      for (Set<Config> configs : serviceMapped.values()) {
        if (!configs.isEmpty()) {
          cluster.addDesiredConfig(authenticatedUserName, configs, serviceVersionNote);
          added = true;
        }
      }
      return added;
    }
  
    /**
     * Creates a new configuration using the specified tag as the first version
     * tag. Otherwise, the configuration will be created with {@literal version}
     * along with the current timestamp.
     *
     * @param cluster
     *          the cluster (not {@code null}).
     * @param stackId
     *          the stack to create the new properties for
     * @param controller
     *          the controller which actually creates the configuration (not
     *          {@code null}).
     * @param stackId
     *          the stack to create the new properties for
     * @param type
     *          the new configuration type (not {@code null}).
     * @param tag
     *          the initial tag; if this configuration already exists, it will use
     *          the timestamp along with {@literal version}.
     * @param properties
     *          the properties to persist (not {@code null}).
     * @param propertyAttributes
     *          the attributes to persist, or {@code null} for none.
     * @return
     * @throws AmbariException
     */
    Config createConfig(Cluster cluster, StackId stackId, AmbariManagementController controller,
        String type, String tag, Map<String, String> properties,
        Map<String, Map<String, String>> propertyAttributes) throws AmbariException {
  
      // if the configuration is not new, then create a timestamp tag
      if (cluster.getConfigsByType(type) != null) {
        tag = "version" + System.currentTimeMillis();
      }
  
      Map<PropertyType, Set<String>> propertiesTypes = cluster.getConfigPropertiesTypes(type);
      if(propertiesTypes.containsKey(PropertyType.PASSWORD)) {
        for(String passwordProperty : propertiesTypes.get(PropertyType.PASSWORD)) {
          if(properties.containsKey(passwordProperty)) {
            String passwordPropertyValue = properties.get(passwordProperty);
            if (!SecretReference.isSecret(passwordPropertyValue)) {
              continue;
            }
            SecretReference ref = new SecretReference(passwordPropertyValue, cluster);
            String refValue = ref.getValue();
            properties.put(passwordProperty, refValue);
          }
        }
      }
  
      return controller.createConfig(cluster, stackId, type, properties, tag, propertyAttributes);
    }
  
    /**
     * Gets the default properties for the specified service. These properties
     * represent those which would be used when a service is first installed.
     *
     * @param stack
     *          the stack to pull stack-values from (not {@code null})
     * @return a mapping of configuration type to map of key/value pairs for the
     *         default configurations.
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> getDefaultStackProperties(StackId stack)
        throws AmbariException {
      Map<String, Map<String, String>> defaultPropertiesByType = new HashMap<>();
  
      // populate the stack (non-service related) properties
      Set<org.apache.ambari.server.state.PropertyInfo> stackConfigurationProperties = ambariMetaInfo.getStackProperties(
          stack.getStackName(), stack.getStackVersion());
  
      for (PropertyInfo stackDefaultProperty : stackConfigurationProperties) {
        String type = ConfigHelper.fileNameToConfigType(stackDefaultProperty.getFilename());
  
        if (!defaultPropertiesByType.containsKey(type)) {
          defaultPropertiesByType.put(type, new HashMap<>());
        }
  
        defaultPropertiesByType.get(type).put(stackDefaultProperty.getName(),
            stackDefaultProperty.getValue());
      }
  
      return defaultPropertiesByType;
    }
  
    /**
     *
     * @param stack
     *          the stack to pull stack-values from (not {@code null})
     * @param serviceName
     *          the service name {@code null}).
     * @return a mapping of configuration type to map of key/value pairs for the
     *         default configurations.
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> getDefaultProperties(StackId stack, String serviceName)
        throws AmbariException {
      Map<String, Map<String, String>> defaultPropertiesByType = new HashMap<>();
  
      // populate the stack (non-service related) properties first
      Set<org.apache.ambari.server.state.PropertyInfo> stackConfigurationProperties = ambariMetaInfo.getStackProperties(
          stack.getStackName(), stack.getStackVersion());
  
      for (PropertyInfo stackDefaultProperty : stackConfigurationProperties) {
        String type = ConfigHelper.fileNameToConfigType(stackDefaultProperty.getFilename());
  
        if (!defaultPropertiesByType.containsKey(type)) {
          defaultPropertiesByType.put(type, new HashMap<>());
        }
  
        defaultPropertiesByType.get(type).put(stackDefaultProperty.getName(),
            stackDefaultProperty.getValue());
      }
  
      // for every installed service, populate the default service properties
      Set<org.apache.ambari.server.state.PropertyInfo> serviceConfigurationProperties = ambariMetaInfo.getServiceProperties(
          stack.getStackName(), stack.getStackVersion(), serviceName);
  
      // !!! use new stack as the basis
      for (PropertyInfo serviceDefaultProperty : serviceConfigurationProperties) {
        String type = ConfigHelper.fileNameToConfigType(serviceDefaultProperty.getFilename());
  
        if (!defaultPropertiesByType.containsKey(type)) {
          defaultPropertiesByType.put(type, new HashMap<>());
        }
  
        defaultPropertiesByType.get(type).put(serviceDefaultProperty.getName(),
            serviceDefaultProperty.getValue());
      }
  
      return defaultPropertiesByType;
    }
  
    //TODO remove after UI will start usage of stale configs flag from HostComponentState update event.
    private boolean calculateIsStaleConfigs(ServiceComponentHost sch, Map<String, DesiredConfig> desiredConfigs,
                                            HostComponentDesiredStateEntity hostComponentDesiredStateEntity) throws AmbariException {
  
      if (sch.isRestartRequired(hostComponentDesiredStateEntity)) {
        return true;
      }
  
      Map<String, HostConfig> actual = sch.getActualConfigs();
      if (null == actual || actual.isEmpty()) {
        return false;
      }
  
      Cluster cluster = clusters.getClusterById(sch.getClusterId());
  
      Map<String, Map<String, String>> desired = getEffectiveDesiredTags(cluster, sch.getHostName(),
              desiredConfigs);
  
      Boolean stale = null;
      int staleHash = 0;
      if (STALE_CONFIGS_CACHE_ENABLED){
        staleHash = Objects.hashCode(actual.hashCode(),
                desired.hashCode(),
                sch.getHostName(),
                sch.getServiceComponentName(),
                sch.getServiceName());
        stale = staleConfigsCache.getIfPresent(staleHash);
        if(stale != null) {
          return stale;
        }
      }
  
      stale = false;
  
      StackId stackId = sch.getServiceComponent().getDesiredStackId();
  
      StackInfo stackInfo = ambariMetaInfo.getStack(stackId);
  
      ServiceInfo serviceInfo = ambariMetaInfo.getService(stackId.getStackName(),
              stackId.getStackVersion(), sch.getServiceName());
  
      ComponentInfo componentInfo = serviceInfo.getComponentByName(sch.getServiceComponentName());
      // Configs are considered stale when:
      // - desired type DOES NOT exist in actual
      // --- desired type DOES NOT exist in stack: not_stale
      // --- desired type DOES exist in stack: check stack for any key: stale
      // - desired type DOES exist in actual
      // --- desired tags DO match actual tags: not_stale
      // --- desired tags DO NOT match actual tags
      // ---- merge values, determine changed keys, check stack: stale
  
      Iterator<Entry<String, Map<String, String>>> it = desired.entrySet().iterator();
      List<String> changedProperties = new LinkedList<>();
  
      while (it.hasNext()) {
        boolean staleEntry = false;
        Entry<String, Map<String, String>> desiredEntry = it.next();
  
        String type = desiredEntry.getKey();
        Map<String, String> tags = desiredEntry.getValue();
  
        if (!actual.containsKey(type)) {
          // desired is set, but actual is not
          staleEntry = (serviceInfo.hasConfigDependency(type) || componentInfo.hasConfigType(type));
        } else {
          // desired and actual both define the type
          HostConfig hc = actual.get(type);
          Map<String, String> actualTags = buildTags(hc);
  
          if (!isTagChanged(tags, actualTags, hasGroupSpecificConfigsForType(cluster, sch.getHostName(), type))) {
            staleEntry = false;
          } else {
            staleEntry = (serviceInfo.hasConfigDependency(type) || componentInfo.hasConfigType(type));
            if (staleEntry) {
              Collection<String> changedKeys = findChangedKeys(cluster, type, tags.values(), actualTags.values());
              changedProperties.addAll(changedKeys);
            }
          }
        }
        stale = stale | staleEntry;
      }
  
      String refreshCommand = calculateRefreshCommand(stackInfo.getRefreshCommandConfiguration(), sch, changedProperties);
  
      if (STALE_CONFIGS_CACHE_ENABLED) {
        staleConfigsCache.put(staleHash, stale);
        if (refreshCommand != null) {
          refreshConfigCommandCache.put(staleHash, refreshCommand);
        }
      }
  
      // gather all changed properties and see if we can find a common refreshConfigs command for this component
      if (LOG.isDebugEnabled()) {
        LOG.debug("Changed properties {} ({}) {} :  COMMAND: {}", stale, sch.getServiceComponentName(), sch.getHostName(), refreshCommand);
        for (String p : changedProperties) {
          LOG.debug(p);
        }
      }
  
      return stale;
    }
  
    /**
     * Checks populated services for staled configs and updates agent configs.
     * Method retrieves actual agent configs and compares them with just generated to identify stale configs.
     * Then config updates are sent to agents.
     * @param updatedClusters names of clusters with changed configs
     * @throws AmbariException
     */
    public void updateAgentConfigs(Set<String> updatedClusters) throws AmbariException {
  
      // get all used clusters in request
      List<Cluster> clustersInUse = new ArrayList<>();
      for (String clusterName : updatedClusters) {
        Cluster cluster;
        cluster = clusters.getCluster(clusterName);
        clustersInUse.add(cluster);
      }
  
      // get all current and previous host configs
      Map<Long, AgentConfigsUpdateEvent> currentConfigEvents = new HashMap<>();
      Map<Long, AgentConfigsUpdateEvent> previousConfigEvents = new HashMap<>();
      for (Cluster cluster : clustersInUse) {
        for (Host host : cluster.getHosts()) {
          Long hostId = host.getHostId();
          if (!currentConfigEvents.containsKey(hostId)) {
            currentConfigEvents.put(host.getHostId(), m_agentConfigsHolder.get().getCurrentData(hostId));
          }
          if (!previousConfigEvents.containsKey(host.getHostId())) {
            previousConfigEvents.put(host.getHostId(),
                m_agentConfigsHolder.get().initializeDataIfNeeded(hostId, true));
          }
        }
      }
  
      for (Cluster cluster : clustersInUse) {
        Map<Long, Map<String, Collection<String>>> changedConfigs = new HashMap<>();
        for (Host host : cluster.getHosts()) {
          AgentConfigsUpdateEvent currentConfigData = currentConfigEvents.get(host.getHostId());
          AgentConfigsUpdateEvent previousConfigsData = previousConfigEvents.get(host.getHostId());
  
          SortedMap<String, SortedMap<String, String>> currentConfigs =
              currentConfigData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
          SortedMap<String, SortedMap<String, String>> previousConfigs =
              previousConfigsData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
  
          Map<String, Collection<String>> changedConfigsHost = new HashMap<>();
          for (String currentConfigType : currentConfigs.keySet()) {
            if (previousConfigs.containsKey(currentConfigType)) {
              Set<String> changedKeys = new HashSet<>();
              Map<String, String> currentTypedConfigs = currentConfigs.get(currentConfigType);
              Map<String, String> previousTypedConfigs = previousConfigs.get(currentConfigType);
  
              for (String currentKey : currentTypedConfigs.keySet()) {
                if (!previousTypedConfigs.containsKey(currentKey)
                    || !currentTypedConfigs.get(currentKey).equals(previousTypedConfigs.get(currentKey))) {
                  changedKeys.add(currentKey);
                }
              }
              for (String previousKey : previousTypedConfigs.keySet()) {
                if (!currentTypedConfigs.containsKey(previousKey)) {
                  changedKeys.add(previousKey);
                }
              }
  
              if (!changedKeys.isEmpty()) {
                changedConfigsHost.put(currentConfigType, changedKeys);
              }
            } else {
              changedConfigsHost.put(currentConfigType, currentConfigs.get(currentConfigType).keySet());
            }
          }
          for (String previousConfigType : previousConfigs.keySet()) {
            if (!currentConfigs.containsKey(previousConfigType)) {
              changedConfigsHost.put(previousConfigType, previousConfigs.get(previousConfigType).keySet());
            }
          }
          changedConfigs.put(host.getHostId(), changedConfigsHost);
        }
        for (String serviceName : cluster.getServices().keySet()) {
          checkStaleConfigsStatusOnConfigsUpdate(cluster.getClusterId(), serviceName, changedConfigs);
        }
  
        m_metadataHolder.get().updateData(m_ambariManagementController.get().getClusterMetadataOnConfigsUpdate(cluster));
        m_agentConfigsHolder.get().updateData(cluster.getClusterId(), null);
      }
    }
  
    /**
     * Checks configs are stale after specified config changes for service's components.
     * @param clusterId cluster with changed config
     * @param serviceName service for changed config
     * @param changedConfigs map of config types to collections of changed properties' names.
     * @throws AmbariException
     */
    public void checkStaleConfigsStatusOnConfigsUpdate(Long clusterId, String serviceName,
                                                       Map<Long, Map<String, Collection<String>>> changedConfigs) throws AmbariException {
      if (MapUtils.isEmpty(changedConfigs)) {
        return;
      }
  
      if (!clusters.getCluster(clusterId).getServices().keySet().contains(serviceName)) {
        return;
      }
      Service service = clusters.getCluster(clusterId).getService(serviceName);
      for (ServiceComponent serviceComponent : service.getServiceComponents().values()) {
        String serviceComponentHostName = serviceComponent.getName();
        for (ServiceComponentHost serviceComponentHost : serviceComponent.getServiceComponentHosts().values()) {
          if (changedConfigs.keySet().contains(serviceComponentHost.getHost().getHostId())) {
            boolean staleConfigs = checkStaleConfigsStatusForHostComponent(serviceComponentHost,
                changedConfigs.get(serviceComponentHost.getHost().getHostId()));
  
            if (wasStaleConfigsStatusUpdated(clusterId, serviceComponentHost.getHost().getHostId(),
                serviceName, serviceComponentHostName, staleConfigs)) {
              serviceComponentHost.setRestartRequiredWithoutEventPublishing(staleConfigs);
              STOMPUpdatePublisher.publish(new HostComponentsUpdateEvent(Collections.singletonList(
                  HostComponentUpdate.createHostComponentStaleConfigsStatusUpdate(clusterId,
                      serviceName, serviceComponentHost.getHostName(),
                      serviceComponentHostName, staleConfigs))));
            }
          }
        }
      }
    }
  
    /**
     * Tries to change cached stale config with new value.
     * @param clusterId cluster id.
     * @param hostId host id.
     * @param serviceName service name.
     * @param hostComponentName component name.
     * @param staleConfigs value to check.
     * @return true if value from cache is different from {@param staleConfigs}.
     */
    public boolean wasStaleConfigsStatusUpdated(Long clusterId, Long hostId, String serviceName, String hostComponentName, Boolean staleConfigs) {
      if (!stateCache.containsKey(clusterId)) {
        stateCache.put(clusterId, new HashMap<>());
      }
      Map<Long, Map<String, Map<String, Boolean>>> hosts = stateCache.get(clusterId);
      if (!hosts.containsKey(hostId)) {
        hosts.put(hostId, new HashMap<>());
      }
      Map<String, Map<String, Boolean>> services = hosts.get(hostId);
      if (!services.containsKey(serviceName)) {
        services.put(serviceName, new HashMap<>());
      }
      Map<String, Boolean> hostComponents = services.get(serviceName);
      if (staleConfigs.equals(hostComponents.get(hostComponentName))) {
        return false;
      } else {
        hostComponents.put(hostComponentName, staleConfigs);
        return true;
      }
    }
  
    /**
     * Checks configs are stale for specified host component.
     * @param sch host component to check.
     * @param changedConfigs map of config types to collections of changed properties' names.
     * @return true if configs are stale.
     * @throws AmbariException
     */
    public boolean checkStaleConfigsStatusForHostComponent(ServiceComponentHost sch,
                                                           Map<String, Collection<String>> changedConfigs) throws AmbariException {
      HostComponentDesiredStateEntity hostComponentDesiredStateEntity = sch.getDesiredStateEntity();
      if (sch.isRestartRequired(hostComponentDesiredStateEntity)) {
        return true;
      }
      boolean stale = false;
  
      Cluster cluster = clusters.getClusterById(sch.getClusterId());
  
      StackId stackId = sch.getServiceComponent().getDesiredStackId();
  
      StackInfo stackInfo = ambariMetaInfo.getStack(stackId);
  
      ServiceInfo serviceInfo = ambariMetaInfo.getService(stackId.getStackName(),
          stackId.getStackVersion(), sch.getServiceName());
  
      ComponentInfo componentInfo = serviceInfo.getComponentByName(sch.getServiceComponentName());
  
      List<String> changedProperties = new LinkedList<>();
      for (Map.Entry<String, Collection<String>> changedConfigType : changedConfigs.entrySet()) {
        String type = changedConfigType.getKey();
        stale |= (serviceInfo.hasConfigDependency(type) || componentInfo.hasConfigType(type));
        if (stale) {
          for (String propertyName : changedConfigType.getValue()) {
            changedProperties.add(type + "/" + propertyName);
          }
        }
      }
  
      String refreshCommand = calculateRefreshCommand(stackInfo.getRefreshCommandConfiguration(), sch, changedProperties);
  
      Map<String, HostConfig> actual = sch.getActualConfigs();
  
      Map<String, Map<String, String>> desired = getEffectiveDesiredTags(cluster, sch.getHostName(),cluster.getDesiredConfigs());
  
      if (STALE_CONFIGS_CACHE_ENABLED) {
        if (refreshCommand != null) {
          int staleHash = Objects.hashCode(actual.hashCode(),
              desired.hashCode(),
              sch.getHostName(),
              sch.getServiceComponentName(),
              sch.getServiceName());
          refreshConfigCommandCache.put(staleHash, refreshCommand);
        }
      }
  
      return stale;
    }
  
    /**
     * Calculates config types and keys were changed during configs change.
     * @param currentServiceConfigEntity service config entity with populated current cluster configs
     * @param configGroupId id of config group contained changed configs. Can be null in case group is default
     * @param clusterId cluster id
     * @param serviceName service name configs were changed for
     * @return map of type names to collections of properties' names were changed.
     */
    public Map<String, Collection<String>> getChangedConfigTypes(Cluster cluster,
                                                                  ServiceConfigEntity currentServiceConfigEntity,
                                                                  Long configGroupId, Long clusterId, String serviceName) {
      ServiceConfigEntity previousServiceConfigEntity;
      List<ClusterConfigEntity> previousConfigEntities = new ArrayList<>();
      List<ClusterConfigEntity> currentConfigEntities = new ArrayList<>();
      currentConfigEntities.addAll(currentServiceConfigEntity.getClusterConfigEntities());
      // Retrieve group cluster configs
      if (configGroupId != null) {
        previousServiceConfigEntity =
            serviceConfigDAO.getLastServiceConfigVersionsForGroup(configGroupId);
        if (previousServiceConfigEntity != null) {
          previousConfigEntities.addAll(previousServiceConfigEntity.getClusterConfigEntities());
        }
      }
      // Service config with custom group contains not all config types, so it is needed
      // to complement it with configs from default group
      previousServiceConfigEntity =
          serviceConfigDAO.getLastServiceConfigForServiceDefaultGroup(clusterId, serviceName);
      if (previousServiceConfigEntity != null) {
        for (ClusterConfigEntity clusterConfigEntity : previousServiceConfigEntity.getClusterConfigEntities()) {
          // Add only configs not present yet
          ClusterConfigEntity exist =
              previousConfigEntities.stream()
                  .filter(c -> c.getType().equals(clusterConfigEntity.getType())).findAny().orElse(null);
          if (exist == null) {
            previousConfigEntities.add(clusterConfigEntity);
          }
          // Complement current custom group service config to correct comparing
          if (configGroupId != null) {
            exist = currentConfigEntities.stream()
                .filter(c -> c.getType().equals(clusterConfigEntity.getType())).findAny().orElse(null);
            if (exist == null) {
              currentConfigEntities.add(clusterConfigEntity);
            }
          }
        }
      }
      Map<String, String> previousConfigs = new HashMap<>();
      Map<String, String> currentConfigs = new HashMap<>();
  
      for (ClusterConfigEntity clusterConfigEntity : currentConfigEntities) {
        currentConfigs.put(clusterConfigEntity.getType(), clusterConfigEntity.getTag());
      }
      for (ClusterConfigEntity clusterConfigEntity : previousConfigEntities) {
        previousConfigs.put(clusterConfigEntity.getType(), clusterConfigEntity.getTag());
      }
  
      Map<String, Collection<String>> changedConfigs = new HashMap<>();
      for (Map.Entry<String, String> currentConfig : currentConfigs.entrySet()) {
        String type = currentConfig.getKey();
        String tag = currentConfig.getValue();
        Collection<String> changedKeys;
        if (previousConfigs.containsKey(type)) {
          changedKeys = findChangedKeys(cluster, type, Collections.singletonList(tag),
              Collections.singletonList(previousConfigs.get(type)));
        } else {
          changedKeys = cluster.getConfig(type, tag).getProperties().keySet();
        }
        if (CollectionUtils.isNotEmpty(changedKeys)) {
          changedConfigs.put(type, changedKeys);
        }
      }
      return changedConfigs;
    }
  
    public String getRefreshConfigsCommand(Cluster cluster, String hostName, String serviceName, String componentName) throws AmbariException {
      ServiceComponent serviceComponent = cluster.getService(serviceName).getServiceComponent(componentName);
      ServiceComponentHost sch = serviceComponent.getServiceComponentHost(hostName);
      return getRefreshConfigsCommand(cluster, sch);
    }
  
    public String getRefreshConfigsCommand(Cluster cluster, ServiceComponentHost sch) throws AmbariException {
      String refreshCommand = null;
  
      Map<String, HostConfig> actual = sch.getActualConfigs();
      if (STALE_CONFIGS_CACHE_ENABLED) {
        Map<String, Map<String, String>> desired = getEffectiveDesiredTags(cluster, sch.getHostName(),
                cluster.getDesiredConfigs());
        int staleHash = Objects.hashCode(actual.hashCode(),
                desired.hashCode(),
                sch.getHostName(),
                sch.getServiceComponentName(),
                sch.getServiceName());
        refreshCommand = refreshConfigCommandCache.getIfPresent(staleHash);
      }
      return refreshCommand;
    }
  
  
    /**
     * Calculates refresh command for a set of changed properties as follows:
     *  - if a property has no refresh command return null
     *  - in case of multiple refresh commands: as REFRESH_CONFIGS is executed by default in case of any other command as well,
     *  can be overriden by RELOAD_CONFIGS or any other custom command, however in case of any other different commands return null
     *  as it's not possible to refresh all properties with one command.
     *
     *  examples:
     *     {REFRESH_CONFIGS, REFRESH_CONFIGS, RELOAD_CONFIGS} ==> RELOAD_CONFIGS
     *     {REFRESH_CONFIGS, RELOADPROXYUSERS, RELOAD_CONFIGS} ==> null
     *
     * @param refreshCommandConfiguration
     * @param sch
     * @param changedProperties
     * @return
     */
    private String calculateRefreshCommand(RefreshCommandConfiguration refreshCommandConfiguration,
                                           ServiceComponentHost sch, List<String> changedProperties) {
  
      String finalRefreshCommand = null;
      for (String propertyName : changedProperties) {
        String refreshCommand = refreshCommandConfiguration.getRefreshCommandForComponent(sch, propertyName);
        if (refreshCommand == null) {
          return null;
        }
        if (finalRefreshCommand == null) {
          finalRefreshCommand = refreshCommand;
        }
        if (!finalRefreshCommand.equals(refreshCommand)) {
          if (finalRefreshCommand.equals(RefreshCommandConfiguration.REFRESH_CONFIGS)) {
            finalRefreshCommand = refreshCommand;
          } else if (!refreshCommand.equals(RefreshCommandConfiguration.REFRESH_CONFIGS)) {
            return null;
          }
        }
      }
      return finalRefreshCommand;
    }
  
    /**
     * Determines if the hostname has group specific configs for the type specified
     *
     * @param cluster
     * @param hostname of the host to look for
     * @param type     the type to look for (e.g. flume-conf)
     * @return <code>true</code> if the hostname has group specific configuration for the type
     */
    private boolean hasGroupSpecificConfigsForType(Cluster cluster, String hostname, String type) {
      try {
        Map<Long, ConfigGroup> configGroups = cluster.getConfigGroupsByHostname(hostname);
        if (configGroups != null && !configGroups.isEmpty()) {
          for (ConfigGroup configGroup : configGroups.values()) {
            Config config = configGroup.getConfigurations().get(type);
            if (config != null) {
              return true;
            }
          }
        }
      } catch (AmbariException ambariException) {
        LOG.warn("Could not determine group configuration for host. Details: " + ambariException.getMessage());
      }
      return false;
    }
  
    /**
     * @return the keys that have changed values
     */
    private Collection<String> findChangedKeys(Cluster cluster, String type,
                                               Collection<String> desiredTags, Collection<String> actualTags) {
  
      Map<String, String> desiredValues = new HashMap<>();
      Map<String, String> actualValues = new HashMap<>();
  
      for (String tag : desiredTags) {
        Config config = cluster.getConfig(type, tag);
        if (null != config) {
          desiredValues.putAll(config.getProperties());
        }
      }
  
      for (String tag : actualTags) {
        Config config = cluster.getConfig(type, tag);
        if (null != config) {
          actualValues.putAll(config.getProperties());
        }
      }
  
      List<String> keys = new ArrayList<>();
  
      for (Entry<String, String> entry : desiredValues.entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
  
        if (!actualValues.containsKey(key) || !valuesAreEqual(actualValues.get(key), value)) {
          keys.add(type + "/" + key);
        }
      }
  
      // check for case configs were removed
      for (String key : actualValues.keySet()) {
        if (!desiredValues.containsKey(key)) {
          keys.add(type + "/" + key);
        }
      }
  
      return keys;
    }
  
    /**
     * Checks for equality of parsed numbers if both values are numeric,
     * otherwise using regular equality.
     */
    static boolean valuesAreEqual(String value1, String value2) { // exposed for unit test
      if (NumberUtils.isNumber(value1) && NumberUtils.isNumber(value2)) {
        try {
          Number number1 = NumberUtils.createNumber(value1);
          Number number2 = NumberUtils.createNumber(value2);
          return Objects.equal(number1, number2) ||
            number1.doubleValue() == number2.doubleValue();
        } catch (NumberFormatException e) {
          // fall back to regular equality
        }
      }
  
      return Objects.equal(value1, value2);
    }
  
    /**
     * @return the map of tags for a desired config
     */
    private Map<String, String> buildTags(HostConfig hc) {
      Map<String, String> map = new LinkedHashMap<>();
      map.put(CLUSTER_DEFAULT_TAG, hc.getDefaultVersionTag());
      if (hc.getConfigGroupOverrides() != null) {
        for (Entry<Long, String> entry : hc.getConfigGroupOverrides().entrySet()) {
          map.put(entry.getKey().toString(), entry.getValue());
        }
      }
      return map;
    }
  
    /**
     * @return true if the tags are different in any way, even if not-specified
     */
    private boolean isTagChanged(Map<String, String> desiredTags, Map<String, String> actualTags, boolean groupSpecificConfigs) {
      if (!actualTags.get(CLUSTER_DEFAULT_TAG).equals(desiredTags.get(CLUSTER_DEFAULT_TAG))) {
        return true;
      }
  
      // cluster level configs are already compared for staleness, now they match
      // if the host has group specific configs for type we should ignore the cluster level configs and compare specifics
      if (groupSpecificConfigs) {
        actualTags.remove(CLUSTER_DEFAULT_TAG);
        desiredTags.remove(CLUSTER_DEFAULT_TAG);
      }
  
      Set<String> desiredSet = new HashSet<>(desiredTags.values());
      Set<String> actualSet = new HashSet<>(actualTags.values());
  
      // Both desired and actual should be exactly the same
      return !desiredSet.equals(actualSet);
    }
  
    public static String fileNameToConfigType(String filename) {
      int extIndex = filename.indexOf(StackDirectory.SERVICE_CONFIG_FILE_NAME_POSTFIX);
      return filename.substring(0, extIndex);
    }
  
    /**
     * Removes properties from configurations that marked as hidden for specified component.
     * @param configurations cluster configurations
     * @param attributes configuration attributes
     * @param componentName component name
     * @param configDownload indicates if config must be downloaded
     */
    public static void processHiddenAttribute(Map<String, Map<String, String>> configurations,
                                              Map<String, Map<String, Map<String, String>>> attributes,
                                              String componentName, boolean configDownload){
      if (configurations != null && attributes != null && componentName != null) {
        for(Map.Entry<String, Map<String,String>> confEntry : configurations.entrySet()){
          String configTag = confEntry.getKey();
          Map<String,String> confProperties = confEntry.getValue();
          if(attributes.containsKey(configTag)){
            Map<String, Map<String, String>> configAttributes = attributes.get(configTag);
            if(configAttributes.containsKey("hidden")){
              Map<String,String> hiddenProperties = configAttributes.get("hidden");
              if(hiddenProperties != null) {
                for (Map.Entry<String, String> hiddenEntry : hiddenProperties.entrySet()) {
                  String propertyName = hiddenEntry.getKey();
                  String components = hiddenEntry.getValue();
                  // hide property if we are downloading config & CONFIG_DOWNLOAD defined,
                  // otherwise - check if we have matching component name
                  if ((configDownload ? components.contains("CONFIG_DOWNLOAD") : components.contains(componentName))
                      && confProperties.containsKey(propertyName)) {
                    confProperties.remove(propertyName);
                  }
                }
              }
            }
          }
        }
      }
    }
  
    /**
     * Merge one attribute map to another.
     * @param attributes original map
     * @param additionalAttributes map with additional attributes
     */
    public static void mergeConfigAttributes(Map<String, Map<String, String>> attributes, Map<String, Map<String, String>> additionalAttributes){
      for(Map.Entry<String, Map<String, String>> attrEntry: additionalAttributes.entrySet()){
        String attributeName = attrEntry.getKey();
        Map<String, String> attributeProperties = attrEntry.getValue();
        if(!attributes.containsKey(attributeName)) {
          attributes.put(attributeName, attributeProperties);
        } else {
          attributes.get(attributeName).putAll(attributeProperties);
        }
      }
    }
  
    /**
     * Collects actual configurations and configuration attributes for specified host.
     * @param hostId host id to collect configurations and configuration attributes
     * @return event ready to send to agent
     * @throws AmbariException
     */
    public AgentConfigsUpdateEvent getHostActualConfigs(Long hostId) throws AmbariException {
      return getHostActualConfigsExcludeCluster(hostId, null);
    }
  
    public AgentConfigsUpdateEvent getHostActualConfigsExcludeCluster(Long hostId, Long clusterId) throws AmbariException {
      TreeMap<String, ClusterConfigs> clustersConfigs = new TreeMap<>();
  
      Host host = clusters.getHostById(hostId);
      for (Cluster cl : clusters.getClusters().values()) {
        if (clusterId != null && cl.getClusterId() == clusterId) {
          continue;
        }
        Map<String, Map<String, String>> configurations = new HashMap<>();
        Map<String, Map<String, Map<String, String>>> configurationAttributes = new HashMap<>();
        Map<String, DesiredConfig> clusterDesiredConfigs = cl.getDesiredConfigs(false);
        Map<String, Map<String, String>> configTags = getEffectiveDesiredTags(cl, host.getHostName(), clusterDesiredConfigs);
  
        // Logging below creating too much spam and slowing down operations
        if (LOG.isDebugEnabled()) {
          LOG.debug("For configs update on host {} will be used cluster entity {}", hostId, cl.getClusterEntity().toString());
          LOG.debug("For configs update on host {} will be used following cluster desired configs {}", hostId, clusterDesiredConfigs.toString());
          LOG.debug("For configs update on host {} will be used following effective desired tags {}", hostId, configTags.toString());
        }
  
        getAndMergeHostConfigs(configurations, configTags, cl);
        configurations = unescapeConfigNames(configurations);
        getAndMergeHostConfigAttributes(configurationAttributes, configTags, cl);
        configurationAttributes = unescapeConfigAttributeNames(configurationAttributes);
  
        SortedMap<String, SortedMap<String, String>> configurationsTreeMap = sortConfigutations(configurations);
        SortedMap<String, SortedMap<String, SortedMap<String, String>>> configurationAttributesTreeMap =
            sortConfigurationAttributes(configurationAttributes);
        clustersConfigs.put(Long.toString(cl.getClusterId()),
            new ClusterConfigs(configurationsTreeMap, configurationAttributesTreeMap));
      }
  
      return new AgentConfigsUpdateEvent(hostId, clustersConfigs);
    }
  
    private Map<String, Map<String, String>> unescapeConfigNames(Map<String, Map<String, String>> configurations) {
      Map<String, Map<String, String>> unescapedConfigs = new HashMap<>();
      for (Entry<String, Map<String, String>> configTypeEntry : configurations.entrySet()) {
        Map<String, String> unescapedTypeConfigs = new HashMap<>();
        for (Entry<String, String> config : configTypeEntry.getValue().entrySet()) {
          unescapedTypeConfigs.put(StringEscapeUtils.unescapeJava(config.getKey()), config.getValue());
        }
        unescapedConfigs.put(configTypeEntry.getKey(), unescapedTypeConfigs);
      }
  
      return unescapedConfigs;
    }
  
    private Map<String, Map<String, Map<String, String>>> unescapeConfigAttributeNames(
        Map<String, Map<String, Map<String, String>>> configurationAttributes) {
      Map<String, Map<String, Map<String, String>>> unescapedConfigAttributes = new HashMap<>();
  
      configurationAttributes.forEach((key, value) ->  unescapedConfigAttributes.put(key, unescapeConfigNames(value)));
      return unescapedConfigAttributes;
    }
  
    public SortedMap<String, SortedMap<String, String>> sortConfigutations(Map<String, Map<String, String>> configurations) {
      SortedMap<String, SortedMap<String, String>> configurationsTreeMap = new TreeMap<>();
      configurations.forEach((k, v) -> configurationsTreeMap.put(k, new TreeMap<>(v)));
      return configurationsTreeMap;
    }
  
    public SortedMap<String, SortedMap<String, SortedMap<String, String>>> sortConfigurationAttributes(
        Map<String, Map<String, Map<String, String>>> configurationAttributes) {
      SortedMap<String, SortedMap<String, SortedMap<String, String>>> configurationAttributesTreeMap = new TreeMap<>();
      configurationAttributes.forEach((k, v) -> {
        SortedMap<String, SortedMap<String, String>> c = new TreeMap<>();
        v.forEach((k1, v1) -> c.put(k1, new TreeMap<>(v1)));
        configurationAttributesTreeMap.put(k, c);
      });
  
      return configurationAttributesTreeMap;
    }
  
    /**
     * Determines the existing configurations for the cluster
     *
     * @param ambariManagementController
     *          the Ambari management controller
     * @param cluster
     *          the cluster
     * @return a map of the existing configurations
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> calculateExistingConfigurations(AmbariManagementController ambariManagementController, Cluster cluster) throws AmbariException {
      final Map<String, Map<String, String>> configurations = new HashMap<>();
      for (Host host : cluster.getHosts()) {
        configurations.putAll(calculateExistingConfigurations(
                ambariManagementController,
                cluster,
                host.getHostName(),
                null
        ));
      }
      return configurations;
    }
  
    /**
     * Determines the existing configurations for the cluster, related to a given hostname (if provided)
     *
     * @param ambariManagementController the Ambari management controller
     * @param cluster  the cluster
     * @param hostname a hostname
     * @param desiredConfigs desired configuration map
     * @return a map of the existing configurations
     * @throws AmbariException
     */
    public Map<String, Map<String, String>> calculateExistingConfigurations(
            AmbariManagementController ambariManagementController, Cluster cluster,  String hostname,
            @Nullable Map<String, DesiredConfig> desiredConfigs) throws AmbariException {
  
      // For a configuration type, both tag and an actual configuration can be stored
      // Configurations from the tag is always expanded and then over-written by the actual
      // global:version1:{a1:A1,b1:B1,d1:D1} + global:{a1:A2,c1:C1,DELETED_d1:x} ==>
      // global:{a1:A2,b1:B1,c1:C1}
      final Map<String, Map<String, String>> configurations = new HashMap<>();
      final Map<String, Map<String, String>> configurationTags = ambariManagementController
              .findConfigurationTagsWithOverrides(cluster, hostname, desiredConfigs);
      final Map<String, Map<String, String>> configProperties = getEffectiveConfigProperties(cluster, configurationTags);
  
      // Apply the configurations saved with the Execution Cmd on top of
      // derived configs - This will take care of all the hacks
      for (Map.Entry<String, Map<String, String>> entry : configProperties.entrySet()) {
        String type = entry.getKey();
        Map<String, String> allLevelMergedConfig = entry.getValue();
        Map<String, String> configuration = configurations.get(type);
  
        if (configuration == null) {
          configuration = new HashMap<>(allLevelMergedConfig);
        } else {
          Map<String, String> mergedConfig = getMergedConfig(allLevelMergedConfig, configuration);
          configuration.clear();
          configuration.putAll(mergedConfig);
        }
  
        configurations.put(type, configuration);
      }
  
      return configurations;
    }
  
    /**
     * Determines the existing configurations for the cluster, both properties and attributes.
     */
    public Pair<Map<String, Map<String, String>>, Map<String, Map<String, Map<String, String>>>> calculateExistingConfigs(Cluster cluster) throws AmbariException {
      Map<String, Map<String, String>> desiredConfigTags = getEffectiveDesiredTags(cluster, null);
      return Pair.of(
        getEffectiveConfigProperties(cluster, desiredConfigTags),
        getEffectiveConfigAttributes(cluster, desiredConfigTags)
      );
    }
  
  }
  