/*
 * 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.serveraction.kerberos;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.AmbariRuntimeException;
import org.apache.ambari.server.actionmanager.HostRoleStatus;
import org.apache.ambari.server.agent.CommandReport;
import org.apache.ambari.server.agent.ExecutionCommand;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.KerberosHelper;
import org.apache.ambari.server.controller.UpdateConfigurationPolicy;
import org.apache.ambari.server.orm.dao.HostDAO;
import org.apache.ambari.server.orm.entities.HostEntity;
import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
import org.apache.ambari.server.serveraction.AbstractServerAction;
import org.apache.ambari.server.serveraction.kerberos.stageutils.KerberosKeytabController;
import org.apache.ambari.server.serveraction.kerberos.stageutils.ResolvedKerberosPrincipal;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.Config;
import org.apache.ambari.server.state.kerberos.KerberosIdentityDescriptor;
import org.apache.ambari.server.utils.StageUtils;
import org.apache.ambari.server.utils.ThreadPools;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.reflect.TypeToken;
import com.google.inject.Inject;

/**
 * KerberosServerAction is an abstract class to be implemented by Kerberos-related
 * {@link org.apache.ambari.server.serveraction.ServerAction} implementations.
 * <p/>
 * This class provides helper methods used to get common properties from the command parameters map
 * and iterate through the Kerberos identity metadata file
 * (see {@link org.apache.ambari.server.serveraction.kerberos.KerberosIdentityDataFileReader}).
 */
public abstract class KerberosServerAction extends AbstractServerAction {
  /**
   * A (command parameter) property name used to hold the authenticated user's name for use in
   * operations that record the acting user.
   */
  public static final String AUTHENTICATED_USER_NAME = "authenticated_user_name";

  /**
   * A (command parameter) property name used to hold the absolute path to the directory that is to
   * be used to store transient data while the request is being processed.  This is expected to be
   * a temporary directory.
   */
  public static final String DATA_DIRECTORY = "data_directory";

  /**
   * A (command parameter) property name used to hold the default Kerberos realm value.
   */
  public static final String DEFAULT_REALM = "default_realm";

  /**
   * A (command parameter) property name used to hold the (serialized) service/component filter map.
   */
  public static final String SERVICE_COMPONENT_FILTER = "service_component_filter";

  /**
   * A (command parameter) property name used to hold the (serialized) host filter list.
   */
  public static final String HOST_FILTER = "host_filter";

  /**
   * A (command parameter) property name used to hold the (serialized) identity filter list.
   */
  public static final String IDENTITY_FILTER = "identity_filter";

  /**
   * A (command parameter) property name used to hold the relevant KDC type value.  See
   * {@link org.apache.ambari.server.serveraction.kerberos.KDCType} for valid values
   */
  public static final String KDC_TYPE = "kdc_type";

  /**
   * A (command parameter) property name used to hold the value indicating how to process
   * configurations updates. One of the of the following values is expected:
   * <dl>
   * <dt>none</dt>
   * <dd>No configurations will be updated</dd>
   * <dt>identities_only</dt>
   * <dd>New and updated configurations related to Kerberos identity information - principal, keytab file, and auth-to-local rule properties</dd>
   * <dt>new_and_identities</dt>
   * <dd>Only new configurations declared by the Kerberos descriptor and stack advisor as well as the identity-related changes</dd>
   * <dt>all</dt>
   * <dd>All configuration changes (default)</dd>
   * </dl>
   */
  public static final String UPDATE_CONFIGURATION_POLICY = "update_configuration_policy";

  /**
   * A (command parameter) property name used to hold the note to set when applying any
   * configuration changes
   */
  public static final String UPDATE_CONFIGURATION_NOTE = "update_configuration_note";

  /**
   * The prefix to use for the data directory name.
   */
  public static final String DATA_DIRECTORY_PREFIX = ".ambari_";

  /**
   * Kerberos action shared data entry name for the principal-to-password map
   */
  private static final String PRINCIPAL_PASSWORD_MAP = "principal_password_map";

  /**
   * Kerberos action shared data entry name for the principal-to-key_number map
   */
  private static final String PRINCIPAL_KEY_NUMBER_MAP = "principal_key_number_map";

  /**
   * Key used in kerberosCommandParams in ExecutionCommand for base64 encoded keytab content
   */
  public static final String KEYTAB_CONTENT_BASE64 = "keytab_content_base64";

  /**
   * Key used in kerberosCommandParams in ExecutionCommand to indicate why type of creation operation to perform.
   *
   * @see OperationType
   */
  public static final String OPERATION_TYPE = "operation_type";

  /**
   * Key used in kerberosCommandParams in ExecutionCommand to indicate whether to include Ambari server indetity
   * ("true") or ignore it ("false")
   */
  public static final String INCLUDE_AMBARI_IDENTITY = "include_ambari_identity";

  /**
   * Keys used in CommandParams from ExecutionCommand to declare how to pre-configure services.
   * Expected values are, "ALL", "DEFAULT", and "NONE".
   */
  public static final String PRECONFIGURE_SERVICES = "preconfigure_services";

  private static final Logger LOG = LoggerFactory.getLogger(KerberosServerAction.class);

  /**
   * The Cluster that this ServerAction implementation is executing on
   */
  @Inject
  private Clusters clusters = null;

  /**
   * The KerberosOperationHandlerFactory to use to obtain KerberosOperationHandler instances
   * <p/>
   * This is needed to help with test cases to mock a KerberosOperationHandler
   */
  @Inject
  private KerberosOperationHandlerFactory kerberosOperationHandlerFactory;

  /**
   * KerberosHelper
   */
  @Inject
  private KerberosHelper kerberosHelper;

  @Inject
  private HostDAO hostDAO;

  @Inject
  private KerberosKeytabController kerberosKeytabController;

  @Inject
  private Configuration configuration;

  @Inject
  private ThreadPools threadPools;

  /**
   * Given a (command parameter) Map and a property name, attempts to safely retrieve the requested
   * data.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @param propertyName      a String declaring the name of the item from commandParameters to retrieve
   * @return a String or null, depending on the property value and if it existed in commandParameters
   */
  protected static String getCommandParameterValue(Map<String, String> commandParameters, String propertyName) {
    return ((commandParameters == null) || (propertyName == null)) ? null : commandParameters.get(propertyName);
  }

  /**
   * Given a (command parameter) Map, attempts to safely retrieve the "update_configuration_policy" property.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @return a UpdateConfigurationPolicy
   */
  protected static UpdateConfigurationPolicy getUpdateConfigurationPolicy(Map<String, String> commandParameters) {
    String stringValue = getCommandParameterValue(commandParameters, UPDATE_CONFIGURATION_POLICY);
    UpdateConfigurationPolicy value = UpdateConfigurationPolicy.translate(stringValue);

    // Return UpdateConfigurationPolicy.ALL as a default value
    return (value == null) ? UpdateConfigurationPolicy.ALL : value;
  }

  /**
   * Given a (command parameter) Map, attempts to safely retrieve the "default_realm" property.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @return a String indicating the default realm or null (if not found or set)
   */
  protected static String getDefaultRealm(Map<String, String> commandParameters) {
    return getCommandParameterValue(commandParameters, DEFAULT_REALM);
  }

  /**
   * Given a (command parameter) Map, attempts to safely retrieve the "kdc_type" property.
   * <p/>
   * If not found, {@link org.apache.ambari.server.serveraction.kerberos.KDCType#MIT_KDC} will be
   * returned as a default value.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @return a KDCType or null (if not found or set)
   */
  protected static KDCType getKDCType(Map<String, String> commandParameters) {
    String kdcType = getCommandParameterValue(commandParameters, KDC_TYPE);

    return ((kdcType == null) || kdcType.isEmpty())
        ? KDCType.NONE
        : KDCType.translate(kdcType);
  }

  /**
   * Given a (command parameter) Map, attempts to safely retrieve the "data_directory" property.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @return a String indicating the data directory or null (if not found or set)
   */
  protected static String getDataDirectoryPath(Map<String, String> commandParameters) {
    return getCommandParameterValue(commandParameters, DATA_DIRECTORY);
  }

  /**
   * Given a (command parameter) Map, attempts to safely retrieve the "operation_type" property.
   *
   * @param commandParameters a Map containing the dictionary of data to interrogate
   * @return an OperationType
   */
  protected static OperationType getOperationType(Map<String, String> commandParameters) {
    String value = getCommandParameterValue(commandParameters, OPERATION_TYPE);
    if (StringUtils.isEmpty(value)) {
      return OperationType.DEFAULT;
    } else {
      return OperationType.valueOf(value.toUpperCase());
    }
  }

  /**
   * Sets the shared principal-to-password Map used to store principals and generated password for
   * use within the current request context.
   *
   * @param requestSharedDataContext a Map to be used as shared data among all ServerActions related
   *                                 to a given request
   * @param principalPasswordMap     A Map of principals and password to store
   */
  protected static void setPrincipalPasswordMap(Map<String, Object> requestSharedDataContext,
                                                Map<String, String> principalPasswordMap) {
    if (requestSharedDataContext != null) {
      requestSharedDataContext.put(PRINCIPAL_PASSWORD_MAP, principalPasswordMap);
    }
  }

  /**
   * Gets the shared principal-to-password Map used to store principals and generated password for
   * use within the current request context.
   * <p/>
   * If the requested Map is not found in requestSharedDataContext, one will be created and stored,
   * ensuring that a Map will always be returned, assuming requestSharedDataContext is not null.
   *
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request
   * @return A Map of principals-to-password
   */
  @SuppressWarnings("unchecked")
  protected static Map<String, String> getPrincipalPasswordMap(Map<String, Object> requestSharedDataContext) {
    if (requestSharedDataContext == null) {
      return null;
    }
    Object map = requestSharedDataContext.computeIfAbsent(PRINCIPAL_PASSWORD_MAP, k -> new HashMap<String, String>());
    return (Map<String, String>) map;
  }

  /**
   * Gets the shared principal-to-key_number Map used to store principals and key numbers for
   * use within the current request context.
   * <p/>
   * If the requested Map is not found in requestSharedDataContext, one will be created and stored,
   * ensuring that a Map will always be returned, assuming requestSharedDataContext is not null.
   *
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request
   * @return A Map of principals-to-key_numbers
   */
  @SuppressWarnings("unchecked")
  protected static Map<String, Integer> getPrincipalKeyNumberMap(Map<String, Object> requestSharedDataContext) {
    if (requestSharedDataContext == null) {
      return null;
    }
    Object map = requestSharedDataContext.computeIfAbsent(PRINCIPAL_KEY_NUMBER_MAP, k -> new HashMap<String, String>());
    return (Map<String, Integer>) map;
  }

  /**
   * Returns the relevant cluster's name
   * <p/>
   * Using the data from the execution command, retrieve the relevant cluster's name.
   *
   * @return a String declaring the relevant cluster's name
   * @throws AmbariException if the cluster's name is not available
   */
  protected String getClusterName() throws AmbariException {
    ExecutionCommand executionCommand = getExecutionCommand();
    String clusterName = (executionCommand == null) ? null : executionCommand.getClusterName();

    if ((clusterName == null) || clusterName.isEmpty()) {
      throw new AmbariException("Failed to retrieve the cluster name from the execution command");
    }

    return clusterName;
  }

  /**
   * Returns the relevant Cluster object
   *
   * @return the relevant Cluster
   * @throws AmbariException if the Cluster object cannot be retrieved
   */
  protected Cluster getCluster() throws AmbariException {
    Cluster cluster = clusters.getCluster(getClusterName());

    if (cluster == null) {
      throw new AmbariException(String.format("Failed to retrieve cluster for %s", getClusterName()));
    }

    return cluster;
  }

  /**
   * The Clusters object for this KerberosServerAction
   *
   * @return a Clusters object
   */
  protected Clusters getClusters() {
    return clusters;
  }

  /**
   * Attempts to safely retrieve the "data_directory" property from the this action's relevant
   * command parameters Map.
   *
   * @return a String indicating the data directory or null (if not found or set)
   */
  protected String getDataDirectoryPath() {
    return getDataDirectoryPath(getCommandParameters());
  }

  /**
   * Returns preconfigure type passed to current action.
   *
   * @return PreconfigureServiceType
   */
  protected PreconfigureServiceType getCommandPreconfigureType() {
    String preconfigureServices = getCommandParameterValue(getCommandParameters(), PRECONFIGURE_SERVICES);
    PreconfigureServiceType type = null;
    if (!StringUtils.isEmpty(preconfigureServices)) {
      try {
        type = PreconfigureServiceType.valueOf(preconfigureServices.toUpperCase());
      } catch (Throwable t) {
        LOG.warn("Invalid preconfigure_services value, assuming DEFAULT: {}", preconfigureServices);
        type = PreconfigureServiceType.DEFAULT;
      }
    }
    return type;
  }

  /**
   * Iterates through the Kerberos identity metadata from the
   * {@link org.apache.ambari.server.serveraction.kerberos.KerberosIdentityDataFileReader} and calls
   * the implementing class to handle each identity found.
   * <p/>
   * Using {@link #getHostFilter()}, {@link #getIdentityFilter()} and {@link #getServiceComponentFilter()} it retrieve
   * list of filtered keytabs and their principals and process each principal using
   * {@link #processIdentity(ResolvedKerberosPrincipal, KerberosOperationHandler, Map, boolean, Map)}.
   * The configuration option {@link Configuration#getKerberosServerActionThreadPoolSize()} defines
   * how many threads will handle {@link #processIdentity(ResolvedKerberosPrincipal, KerberosOperationHandler, Map, boolean, Map)}.
   * The default is {@code 1}, but this method must be thread-safe in the event that concurrent threads are used.
   *
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request
   * @return a CommandReport indicating the result of this operation
   * @throws AmbariException
   */
  protected CommandReport processIdentities(Map<String, Object> requestSharedDataContext)
      throws AmbariException {
    final CommandReport[] commandReport = {null};
    Map<String, String> commandParameters = getCommandParameters();

    actionLog.writeStdOut("Processing identities...");
    LOG.info("Processing identities...");

    if (commandParameters != null) {
      // Grab the relevant data from this action's command parameters map
      PrincipalKeyCredential administratorCredential = kerberosHelper.getKDCAdministratorCredentials(getClusterName());
      KDCType kdcType = getKDCType(commandParameters);
      String defaultRealm = getDefaultRealm(commandParameters);

      KerberosOperationHandler handler = kerberosOperationHandlerFactory.getKerberosOperationHandler(kdcType);
      Map<String, String> kerberosConfiguration = getConfigurationProperties("kerberos-env");

      try {
        handler.open(administratorCredential, defaultRealm, kerberosConfiguration);
      } catch (KerberosOperationException e) {
        String message = String.format("Failed to process the identities, could not properly open the KDC operation handler: %s",
            e.getMessage());
        actionLog.writeStdErr(message);
        LOG.error(message);
        throw new AmbariException(message, e);
      }

      try {
        // create the thread factory, executor, and completion service for
        // running the identity processing in parallel

        ExecutionCommand executionCommand = getExecutionCommand();
        int threadCount = configuration.getKerberosServerActionThreadPoolSize();
        String factoryName = (executionCommand == null)
          ? "process-identity-%d"
          : "process-identity-task-" + executionCommand.getTaskId() + "-thread-%d";


        Map<String, Collection<String>> serviceComponentFilter = getServiceComponentFilter();
        if (serviceComponentFilter != null && pruneServiceFilter()) {
          kerberosKeytabController.adjustServiceComponentFilter(clusters.getCluster(getClusterName()),
            true, serviceComponentFilter);
        }
        final Collection<KerberosIdentityDescriptor> serviceIdentities = (serviceComponentFilter == null)
          ? null
          : kerberosKeytabController.getServiceIdentities(getClusterName(), serviceComponentFilter.keySet(), null);

        threadPools.parallelOperation(factoryName, threadCount, "identities",
          kerberosKeytabController.getFilteredKeytabs(serviceIdentities, getHostFilter(), getIdentityFilter())
            .stream().flatMap(rkk -> rkk.getPrincipals().stream()).map(principal -> (Callable<CommandReport>) () -> {
              try {
                return processIdentity(principal, handler, kerberosConfiguration,
                  isRelevantIdentity(serviceIdentities, principal), requestSharedDataContext);
              } catch (AmbariException ambariException) {
                throw new AmbariRuntimeException(ambariException);
              }
            }
          ).collect(Collectors.toList()),
          (cr) -> { // processIdentity returns null if operation completed ok, else FAILED report generated
            boolean isFailed = Objects.isNull(cr);
            if (!isFailed) {
              commandReport[0] = cr;
            }
            return isFailed;
          }
        );
      } catch (Exception exception) {
        LOG.error("Unable to process identities asynchronously", exception);
        return createCommandReport(0, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
      } finally {
        // The KerberosOperationHandler needs to be closed, if it fails to close ignore the
        // exception since there is little we can or care to do about it now.
        try {
          handler.close();
        } catch (KerberosOperationException e) {
          // Ignore this...
        }
      }

    }

    actionLog.writeStdOut("Processing identities completed.");
    LOG.info("Processing identities completed.");

    // If commandReport is null, we can assume this operation was a success, so return a successful
    // CommandReport; else return the previously created CommandReport.
    return (commandReport[0] == null)
        ? createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr())
        : commandReport[0];
  }

  protected boolean pruneServiceFilter() {
    return true;
  }

  private boolean isRelevantIdentity(Collection<KerberosIdentityDescriptor> serviceIdentities, ResolvedKerberosPrincipal principal) {
    if (serviceIdentities != null) {
      boolean hasValidIdentity = false;
      for (KerberosIdentityDescriptor serviceIdentity : serviceIdentities) {
        if (principal.getPrincipal().equals(serviceIdentity.getPrincipalDescriptor().getName()) && StringUtils.isBlank(serviceIdentity.getReference())) {
          hasValidIdentity = true;
          break;
        }
      }
      return hasValidIdentity;
    }

    return true;
  }

  /**
   * Processes an identity as necessary.
   * <p/>
   * This method is called from {@link #processIdentities(Map)} for each
   * principal found by specified filter. After processing, it is expected
   * that the return value is null on success and a CommandReport (indicating the error) on failure.
   *
   * @param resolvedPrincipal        a ResolvedKerberosPrincipal object to process
   * @param operationHandler         a KerberosOperationHandler used to perform Kerberos-related
   *                                 tasks for specific Kerberos implementations
   *                                 (MIT, Active Directory, etc...)
   * @param kerberosConfiguration    a Map of configuration properties from kerberos-env
   * @param includedInFilter         a Boolean value indicating whather the principal is included in
   *                                 the current filter or not
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request  @return a CommandReport, indicating an error
   *                                 condition; or null, indicating a success condition
   * @throws AmbariException if an error occurs while processing the identity record
   */
  protected abstract CommandReport processIdentity(ResolvedKerberosPrincipal resolvedPrincipal,
                                                   KerberosOperationHandler operationHandler,
                                                   Map<String, String> kerberosConfiguration,
                                                   boolean includedInFilter,
                                                   Map<String, Object> requestSharedDataContext)
      throws AmbariException;

  protected void deleteDataDirectory(String dataDirectoryPath) {
    // Make sure this is a relevant directory. We don't want to accidentally allow _ANY_ directory
    // to be deleted.
    if ((dataDirectoryPath != null) && dataDirectoryPath.contains("/" + DATA_DIRECTORY_PREFIX)) {
      File dataDirectory = new File(dataDirectoryPath);
      File dataDirectoryParent = dataDirectory.getParentFile();

      // Make sure this directory has a parent and it is writeable, else we wont be able to
      // delete the directory
      if ((dataDirectoryParent != null) && dataDirectory.isDirectory() &&
          dataDirectoryParent.isDirectory() && dataDirectoryParent.canWrite()) {
        try {
          FileUtils.deleteDirectory(dataDirectory);
        } catch (IOException e) {
          // We should log this exception, but don't let it fail the process since if we got to this
          // KerberosServerAction it is expected that the the overall process was a success.
          String message = String.format("The data directory (%s) was not deleted due to an error condition - {%s}",
              dataDirectory.getAbsolutePath(), e.getMessage());
          LOG.warn(message, e);
        }
      }
    }
  }


  protected Set<String> getHostFilter() {
    String serializedValue = getCommandParameterValue(HOST_FILTER);

    if (serializedValue != null) {
      Type type = new TypeToken<Set<String>>() {
      }.getType();
      return StageUtils.getGson().fromJson(serializedValue, type);
    } else {
      return null;
    }
  }

  protected boolean hasHostFilters() {
    Set<String> hostFilers = getHostFilter();
    return hostFilers != null && hostFilers.size() > 0;
  }


  protected Map<String, Collection<String>> getServiceComponentFilter() {
    String serializedValue = getCommandParameterValue(SERVICE_COMPONENT_FILTER);

    if (serializedValue != null) {
      Type type = new TypeToken<Map<String, ? extends Collection<String>>>() {
      }.getType();
      return StageUtils.getGson().fromJson(serializedValue, type);
    } else {
      return null;
    }
  }

  protected Collection<String> getIdentityFilter() {
    String serializedValue = getCommandParameterValue(IDENTITY_FILTER);

    if (serializedValue != null) {
      Type type = new TypeToken<Collection<String>>() {
      }.getType();
      return StageUtils.getGson().fromJson(serializedValue, type);
    } else {
      return null;
    }
  }

  protected Long ambariServerHostID() {
    String ambariServerHostName = StageUtils.getHostName();
    HostEntity ambariServerHostEntity = hostDAO.findByName(ambariServerHostName);
    return (ambariServerHostEntity == null)
        ? null
        : ambariServerHostEntity.getHostId();
  }

  /**
   * Retrieve the current set of properties for the requested config type for the relevant cluster.
   *
   * @return a Map of property names to property values for the requested config type; or null if no data is found
   * @throws AmbariException if an error occurs retrieving the relevant cluster details
   */
  protected Map<String, String> getConfigurationProperties(String configType) throws AmbariException {
    if (StringUtils.isNotEmpty(configType)) {
      Cluster cluster = getCluster();
      Config config = (cluster == null) ? null : cluster.getDesiredConfigByType(configType);
      Map<String, String> properties = (config == null) ? null : config.getProperties();

      if (properties == null) {
        LOG.warn("The '{}' configuration data is not available:" +
                "\n\tcluster: {}" +
                "\n\tconfig: {}" +
                "\n\tproperties: null",
            configType,
            (cluster == null) ? "null" : "not null",
            (config == null) ? "null" : "not null");
      }

      return properties;
    } else {
      return null;
    }
  }

  public static class KerberosCommandParameters {
    private Map<String, String> params;

    public KerberosCommandParameters(ExecutionCommand ec) {
      params = ec.getCommandParams();
    }

    public KerberosCommandParameters(AbstractServerAction serverAction) {
      this(serverAction.getExecutionCommand());
    }

    public Set<String> getHostFilter() {
      String serializedValue = getCommandParameterValue(HOST_FILTER);

      if (serializedValue != null) {
        Type type = new TypeToken<Set<String>>() {
        }.getType();
        return StageUtils.getGson().fromJson(serializedValue, type);
      } else {
        return null;
      }
    }

    public boolean hasHostFilters() {
      Set<String> hostFilers = getHostFilter();
      return hostFilers != null && hostFilers.size() > 0;
    }

    public Map<String, ? extends Collection<String>> getServiceComponentFilter() {
      String serializedValue = getCommandParameterValue(SERVICE_COMPONENT_FILTER);

      if (serializedValue != null) {
        Type type = new TypeToken<Map<String, ? extends Collection<String>>>() {
        }.getType();
        return StageUtils.getGson().fromJson(serializedValue, type);
      } else {
        return null;
      }
    }

    public Collection<String> getIdentityFilter() {
      String serializedValue = getCommandParameterValue(IDENTITY_FILTER);

      if (serializedValue != null) {
        Type type = new TypeToken<Collection<String>>() {
        }.getType();
        return StageUtils.getGson().fromJson(serializedValue, type);
      } else {
        return null;
      }
    }

    public String getCommandParameterValue(String propertyName) {
      Map<String, String> commandParameters = params;
      return (commandParameters == null) ? null : commandParameters.get(propertyName);
    }
  }

  /**
   * A Kerberos operation type
   * <ul>
   * <li>RECREATE_ALL - regenerate keytabs for all principals</li>
   * <li>CREATE_MISSING - generate keytabs for only those that are missing</li>
   * <li>DEFAULT - generate needed keytabs for new components</li>
   * </ul>
   */
  public enum OperationType {
    /**
     * Regenerate keytabs for all principals
     */
    RECREATE_ALL,

    /**
     * Generate keytabs for only those that are missing
     */
    CREATE_MISSING,

    /**
     * Generate needed keytabs for new components
     */
    DEFAULT
  }
}
