/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2015 ForgeRock AS.
 */
package org.opends.server.loggers;

import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;

import static java.util.Arrays.asList;
import static org.opends.messages.LoggerMessages.*;
import static org.forgerock.audit.AuditServiceBuilder.newAuditService;
import static org.forgerock.audit.events.EventTopicsMetaDataBuilder.coreTopicSchemas;
import static org.forgerock.audit.json.AuditJsonConfig.registerHandlerToService;
import static org.opends.server.util.StaticUtils.getFileForPath;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

import org.forgerock.audit.AuditException;
import org.forgerock.audit.AuditService;
import org.forgerock.audit.AuditServiceBuilder;
import org.forgerock.audit.AuditServiceConfiguration;
import org.forgerock.audit.AuditServiceProxy;
import org.forgerock.audit.DependencyProvider;
import org.forgerock.audit.events.EventTopicsMetaData;
import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRetention;
import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRotation;
import org.forgerock.audit.filter.FilterPolicy;
import org.forgerock.audit.handlers.csv.CsvAuditEventHandler;
import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration;
import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvFormatting;
import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvSecurity;
import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.EventBufferingConfiguration;
import org.forgerock.audit.json.AuditJsonConfig;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.opendj.config.ConfigurationFramework;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.admin.server.ServerManagementContext;
import org.opends.server.admin.std.server.CsvFileAccessLogPublisherCfg;
import org.opends.server.admin.std.server.CsvFileHTTPAccessLogPublisherCfg;
import org.opends.server.admin.std.server.ExternalAccessLogPublisherCfg;
import org.opends.server.admin.std.server.ExternalHTTPAccessLogPublisherCfg;
import org.opends.server.admin.std.server.FileCountLogRetentionPolicyCfg;
import org.opends.server.admin.std.server.FixedTimeLogRotationPolicyCfg;
import org.opends.server.admin.std.server.FreeDiskSpaceLogRetentionPolicyCfg;
import org.opends.server.admin.std.server.LogPublisherCfg;
import org.opends.server.admin.std.server.LogRetentionPolicyCfg;
import org.opends.server.admin.std.server.LogRotationPolicyCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.admin.std.server.SizeLimitLogRetentionPolicyCfg;
import org.opends.server.admin.std.server.SizeLimitLogRotationPolicyCfg;
import org.opends.server.admin.std.server.TimeLimitLogRotationPolicyCfg;
import org.opends.server.config.ConfigEntry;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.DN;
import org.opends.server.util.StaticUtils;

/**
 * Entry point for the common audit facility.
 * <p>
 * This class manages the AuditService instances and Audit Event Handlers that correspond to the
 * publishers defined in OpenDJ configuration.
 * <p>
 * In theory there should be only one instance of AuditService for all the event handlers but
 * defining one service per handler allow to perform filtering at the DJ server level.
 */
public class CommonAudit
{
  /** Transaction id used when the incoming request does not contain a transaction id. */
  public static final String DEFAULT_TRANSACTION_ID = "0";

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private static final String AUDIT_SERVICE_JSON_CONFIGURATION_FILE = "audit-config.json";

  /** Dependency provider used to instantiate the handlers. */
  private final DependencyProvider dependencyProvider;

  /** Configuration framework is used to get an up-to-date class loader with any external library available. */
  private final ConfigurationFramework configurationFramework;

  /** Cache of audit services per configuration entry normalized name. */
  private final Map<String, AuditServiceProxy> auditServiceCache = new ConcurrentHashMap<>(10);

  /** Cache of PublisherConfig per http access configuration entry normalized name. */
  private final Map<String, PublisherConfig> httpAccessPublishers = new ConcurrentHashMap<>(5);

  /** Cache of PublisherConfig per access configuration entry normalized name. */
  private final Map<String, PublisherConfig> accessPublishers = new ConcurrentHashMap<>(5);

  /** Audit service shared by all HTTP access publishers. */
  private final AuditServiceProxy httpAccessAuditService;

  private final AtomicBoolean trustTransactionIds = new AtomicBoolean(false);

  /**
   * Creates the common audit.
   *
   * @throws ConfigException
   *           If an error occurs.
   */
  public CommonAudit() throws ConfigException
  {
    configurationFramework = ConfigurationFramework.getInstance();
    this.dependencyProvider = new CommonAuditDependencyProvider();
    this.httpAccessAuditService = createAuditServiceWithoutHandlers();
  }

  /**
   * Indicates if transactionIds received from requests should be trusted.
   *
   * @return {@code true} if transactionIds should be trusted, {@code false} otherwise
   */
  public boolean shouldTrustTransactionIds()
  {
    return trustTransactionIds.get();
  }

  /**
   * Sets the indicator for transactionIds trusting.
   *
   * @param shouldTrust
   *          {@code true} if transactionIds should be trusted, {@code false}
   *          otherwise
   */
  public void setTrustTransactionIds(boolean shouldTrust)
  {
    trustTransactionIds.set(shouldTrust);
  }

  private AuditServiceProxy createAuditServiceWithoutHandlers() throws ConfigException
  {
    try
    {
      return buildAuditService(new AuditServiceSetup()
      {
        @Override
        public void addHandlers(AuditServiceBuilder builder)
        {
          // no handler to add
        }
      });
    }
    catch (IOException | ConfigException | AuditException e)
    {
      throw new ConfigException(ERR_COMMON_AUDIT_CREATE.get(e), e);
    }
  }

  /**
   * Returns the Common Audit request handler for the provided configuration.
   *
   * @param config
   *            The log publisher configuration
   * @return the request handler associated to the log publisher
   * @throws ConfigException
   *            If an error occurs
   */
  public RequestHandler getRequestHandler(LogPublisherCfg config) throws ConfigException
  {
    if (new PublisherConfig(config).isHttpAccessLog())
    {
      return httpAccessAuditService;
    }
    return auditServiceCache.get(getConfigNormalizedName(config));
  }

  /**
   * Adds or updates the publisher corresponding to the provided configuration to common audit.
   *
   * @param newConfig
   *          Configuration of the publisher
   * @throws ConfigException
   *           If an error occurs.
   */
  public void addOrUpdatePublisher(final LogPublisherCfg newConfig) throws ConfigException
  {
    if (newConfig.isEnabled())
    {
      logger.trace(String.format("Setting up common audit for configuration entry: %s", newConfig.dn()));
      try
      {
        final PublisherConfig newPublisher = new PublisherConfig(newConfig);
        String normalizedName = getConfigNormalizedName(newConfig);
        if (newPublisher.isHttpAccessLog())
        {
          // if an old version exists, it is replaced by the new one
          httpAccessPublishers.put(normalizedName, newPublisher);
          buildAuditService(httpAccessAuditServiceSetup());
        }
        else // all other logs
        {
          final AuditServiceProxy existingService = auditServiceCache.get(normalizedName);
          AuditServiceProxy auditService = buildAuditService(new AuditServiceSetup(existingService)
          {
            @Override
            public void addHandlers(AuditServiceBuilder builder) throws ConfigException
            {
              registerHandlerName(newPublisher.getName());
              addHandlerToBuilder(newPublisher, builder);
            }
          });
          auditServiceCache.put(normalizedName, auditService);
          accessPublishers.put(normalizedName, newPublisher);
        }
      }
      catch (Exception e)
      {
        throw new ConfigException(ERR_COMMON_AUDIT_ADD_OR_UPDATE_LOG_PUBLISHER.get(newConfig.dn(), e), e);
      }
    }
  }

  /**
   * Removes the publisher corresponding to the provided configuration from common audit.
   *
   * @param config
   *          Configuration of publisher to remove
   * @throws ConfigException
   *            If an error occurs.
   */
  public void removePublisher(LogPublisherCfg config) throws ConfigException
  {
    logger.trace(String.format("Shutting down common audit for configuration entry:", config.dn()));
    String normalizedName = getConfigNormalizedName(config);
    try
    {
      if (httpAccessPublishers.containsKey(normalizedName))
      {
        httpAccessPublishers.remove(normalizedName);
        buildAuditService(httpAccessAuditServiceSetup());
      }
      else if (accessPublishers.containsKey(normalizedName))
      {
        accessPublishers.remove(normalizedName);
        AuditServiceProxy auditService = auditServiceCache.remove(normalizedName);
        if (auditService != null)
        {
          auditService.shutdown();
        }
      }
      // else it is not a registered publisher, nothing to do
    }
    catch (Exception e)
    {
      throw new ConfigException(ERR_COMMON_AUDIT_REMOVE_LOG_PUBLISHER.get(config.dn(), e), e);
    }
  }

  /** Shutdown common audit. */
  public void shutdown()
  {
    httpAccessAuditService.shutdown();
    for (AuditServiceProxy service : auditServiceCache.values())
    {
      service.shutdown();
    }
  }

  private AuditServiceSetup httpAccessAuditServiceSetup()
  {
    return new AuditServiceSetup(httpAccessAuditService)
    {
      @Override
      public void addHandlers(AuditServiceBuilder builder) throws ConfigException
      {
        for (PublisherConfig publisher : httpAccessPublishers.values())
        {
          registerHandlerName(publisher.getName());
          addHandlerToBuilder(publisher, builder);
        }
      }
    };
  }

  /**
   * Strategy for the setup of AuditService.
   * <p>
   * Unless no handler must be added, this class should be extended and
   * implementations should override the {@code addHandlers()} method.
   */
  static abstract class AuditServiceSetup
  {
    private final AuditServiceProxy existingAuditServiceProxy;
    private final List<String> names = new ArrayList<>();

    /** Creation with no existing audit service. */
    AuditServiceSetup()
    {
      this.existingAuditServiceProxy = null;
    }

    /** Creation with an existing audit service. */
    AuditServiceSetup(AuditServiceProxy existingAuditService)
    {
      this.existingAuditServiceProxy = existingAuditService;
    }

    abstract void addHandlers(AuditServiceBuilder builder) throws ConfigException;

    void registerHandlerName(String name)
    {
      names.add(name);
    }

    List<String> getHandlerNames()
    {
      return names;
    }

    boolean mustCreateAuditServiceProxy()
    {
      return existingAuditServiceProxy == null;
    }

    AuditServiceProxy getExistingAuditServiceProxy()
    {
      return existingAuditServiceProxy;
    }

  }

  private AuditServiceProxy buildAuditService(AuditServiceSetup setup)
      throws IOException, AuditException, ConfigException
  {
    final JsonValue jsonConfig;
    try (InputStream input = getClass().getResourceAsStream(AUDIT_SERVICE_JSON_CONFIGURATION_FILE))
    {
      jsonConfig = AuditJsonConfig.getJson(input);
    }

    EventTopicsMetaData eventTopicsMetaData = coreTopicSchemas()
        .withCoreTopicSchemaExtensions(jsonConfig.get("extensions"))
        .withAdditionalTopicSchemas(jsonConfig.get("additionalTopics"))
        .build();
    AuditServiceBuilder builder = newAuditService()
        .withEventTopicsMetaData(eventTopicsMetaData)
        .withDependencyProvider(dependencyProvider);

    setup.addHandlers(builder);

    AuditServiceConfiguration auditConfig = new AuditServiceConfiguration();
    auditConfig.setAvailableAuditEventHandlers(setup.getHandlerNames());
    auditConfig.setFilterPolicies(getFilterPoliciesToPreventHttpHeadersLogging());
    builder.withConfiguration(auditConfig);
    AuditService audit = builder.build();

    final AuditServiceProxy proxy;
    if (setup.mustCreateAuditServiceProxy())
    {
      proxy = new AuditServiceProxy(audit);
      logger.trace("Starting up new common audit service");
      proxy.startup();
    }
    else
    {
      proxy = setup.getExistingAuditServiceProxy();
      proxy.setDelegate(audit);
      logger.trace("Starting up existing updated common audit service");
    }
    return proxy;
  }

  /**
   * Build filter policies at the AuditService level to prevent logging of the headers for HTTP requests.
   * <p>
   * HTTP Headers may contains authentication information.
   */
  private Map<String, FilterPolicy> getFilterPoliciesToPreventHttpHeadersLogging()
  {
    Map<String, FilterPolicy> filterPolicies = new HashMap<>();
    FilterPolicy policy = new FilterPolicy();
    policy.setExcludeIf(asList("/http-access/http/request/headers"));
    filterPolicies.put("field", policy);
    return filterPolicies;
  }

  private void addHandlerToBuilder(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
  {
    if (publisher.isCsv())
    {
      addCsvHandler(publisher, builder);
    }
    else if (publisher.isExternal())
    {
      addExternalHandler(publisher, builder);
    }
    else
    {
      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_HANDLER_TYPE.get(publisher.getDn()));
    }
  }

  /** Add a handler defined externally in a JSON configuration file. */
  private void addExternalHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
  {
    ExternalConfigData config = publisher.getExternalConfig();
    File configFile = getFileForPath(config.getConfigurationFile());
    try (InputStream input = new BufferedInputStream(new FileInputStream(configFile)))
    {
      JsonValue jsonConfig = AuditJsonConfig.getJson(input);
      registerHandlerToService(jsonConfig, builder, configurationFramework.getClassLoader());
    }
    catch (IOException e)
    {
      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_JSON_FILE.get(configFile, publisher.getDn(), e), e);
    }
    catch (Exception e)
    {
      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_CREATION.get(publisher.getDn(), e), e);
    }
  }

  private void addCsvHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
  {
    String name = publisher.getName();
    try
    {
      CsvConfigData config = publisher.getCsvConfig();
      CsvAuditEventHandlerConfiguration csvConfig = new CsvAuditEventHandlerConfiguration();
      File logDirectory = getFileForPath(config.getLogDirectory());
      csvConfig.setLogDirectory(logDirectory.getAbsolutePath());
      csvConfig.setName(name);
      csvConfig.setTopics(Collections.singleton(publisher.getCommonAuditTopic()));

      addCsvHandlerFormattingConfig(config, csvConfig);
      addCsvHandlerBufferingConfig(config, csvConfig);
      addCsvHandlerSecureConfig(publisher, config, csvConfig);
      addCsvHandlerRotationConfig(publisher, config, csvConfig);
      addCsvHandlerRetentionConfig(publisher, config, csvConfig);

      builder.withAuditEventHandler(CsvAuditEventHandler.class, csvConfig);
    }
    catch (Exception e)
    {
      throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_CREATION.get(publisher.getDn(), e), e);
    }
  }

  private void addCsvHandlerFormattingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
      throws ConfigException
  {
    CsvFormatting formatting = new CsvFormatting();
    formatting.setQuoteChar(config.getQuoteChar());
    formatting.setDelimiterChar(config.getDelimiterChar());
    String endOfLineSymbols = config.getEndOfLineSymbols();
    if (endOfLineSymbols != null && !endOfLineSymbols.isEmpty())
    {
      formatting.setEndOfLineSymbols(endOfLineSymbols);
    }
    auditConfig.setFormatting(formatting);
  }

  private void addCsvHandlerBufferingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
  {
    EventBufferingConfiguration bufferingConfig = new EventBufferingConfiguration();
    bufferingConfig.setEnabled(config.isAsynchronous());
    bufferingConfig.setAutoFlush(config.isAutoFlush());
    auditConfig.setBufferingConfiguration(bufferingConfig);
  }

  private void addCsvHandlerSecureConfig(PublisherConfig publisher, CsvConfigData config,
      CsvAuditEventHandlerConfiguration auditConfig)
  {
    if (config.isTamperEvident())
    {
      CsvSecurity security = new CsvSecurity();
      security.setSignatureInterval(config.getSignatureTimeInterval() + "ms");
      security.setEnabled(true);
      String keyStoreFile = config.getKeystoreFile();
      security.setFilename(getFileForPath(keyStoreFile).getPath());
      security.setPassword(getSecurePassword(publisher, config));
      auditConfig.setSecurity(security);
    }
  }

  private void addCsvHandlerRotationConfig(PublisherConfig publisher, CsvConfigData config,
      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
  {
    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
    SortedSet<String> rotationPolicies = config.getRotationPolicies();
    if (rotationPolicies.isEmpty())
    {
      return;
    }

    FileRotation fileRotation = new FileRotation();
    fileRotation.setRotationEnabled(true);
    for (final String policy : rotationPolicies)
    {
      LogRotationPolicyCfg policyConfig = root.getLogRotationPolicy(policy);
      if (policyConfig instanceof FixedTimeLogRotationPolicyCfg)
      {
        List<String> times = convertTimesOfDay(publisher, (FixedTimeLogRotationPolicyCfg) policyConfig);
        fileRotation.setRotationTimes(times);
      }
      else if (policyConfig instanceof SizeLimitLogRotationPolicyCfg)
      {
        fileRotation.setMaxFileSize(((SizeLimitLogRotationPolicyCfg) policyConfig).getFileSizeLimit());
      }
      else if (policyConfig instanceof TimeLimitLogRotationPolicyCfg)
      {
        long rotationInterval = ((TimeLimitLogRotationPolicyCfg) policyConfig).getRotationInterval();
        fileRotation.setRotationInterval(String.valueOf(rotationInterval) + " ms");
      }
      else
      {
        throw new ConfigException(
            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_ROTATION_POLICY.get(publisher.getDn(), policyConfig.dn()));
      }
    }
    auditConfig.setFileRotation(fileRotation);
  }

  private void addCsvHandlerRetentionConfig(PublisherConfig publisher, CsvConfigData config,
      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
  {
    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
    SortedSet<String> retentionPolicies = config.getRetentionPolicies();
    if (retentionPolicies.isEmpty())
    {
      return;
    }

    FileRetention fileRetention = new FileRetention();
    for (final String policy : retentionPolicies)
    {
      LogRetentionPolicyCfg policyConfig = root.getLogRetentionPolicy(policy);
      if (policyConfig instanceof FileCountLogRetentionPolicyCfg)
      {
        fileRetention.setMaxNumberOfHistoryFiles(((FileCountLogRetentionPolicyCfg) policyConfig).getNumberOfFiles());
      }
      else if (policyConfig instanceof FreeDiskSpaceLogRetentionPolicyCfg)
      {
        fileRetention.setMinFreeSpaceRequired(((FreeDiskSpaceLogRetentionPolicyCfg) policyConfig).getFreeDiskSpace());
      }
      else if (policyConfig instanceof SizeLimitLogRetentionPolicyCfg)
      {
        fileRetention.setMaxDiskSpaceToUse(((SizeLimitLogRetentionPolicyCfg) policyConfig).getDiskSpaceUsed());
      }
      else
      {
        throw new ConfigException(
            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_RETENTION_POLICY.get(publisher.getDn(), policyConfig.dn()));
      }
    }
    auditConfig.setFileRetention(fileRetention);
  }

  /**
   * Convert the set of provided times of day using 24-hour format "HHmm" to a list of
   * times of day using duration in minutes, e.g "20 minutes".
   * <p>
   * Example: "0230" => "150 minutes"
   */
  private List<String> convertTimesOfDay(PublisherConfig publisher, FixedTimeLogRotationPolicyCfg policyConfig)
      throws ConfigException
  {
    SortedSet<String> timesOfDay = policyConfig.getTimeOfDay();
    List<String> times = new ArrayList<>();
    for (String timeOfDay : timesOfDay)
    {
      try
      {
        int time = Integer.valueOf(timeOfDay.substring(0, 2)) * 60 + Integer.valueOf(timeOfDay.substring(2, 4));
        times.add(String.valueOf(time) + " minutes");
      }
      catch (NumberFormatException | IndexOutOfBoundsException e)
      {
        throw new ConfigException(ERR_COMMON_AUDIT_INVALID_TIME_OF_DAY.get(publisher.getDn(), timeOfDay,
            StaticUtils.stackTraceToSingleLineString(e)));
      }
    }
    return times;
  }

  private String getSecurePassword(PublisherConfig publisher, CsvConfigData config)
  {
    String fileName = config.getKeystorePinFile();
    File pinFile = getFileForPath(fileName);

    if (!pinFile.exists())
    {
      logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_MISSING.get(publisher.getDn(), pinFile));
      return "";
    }

    try (BufferedReader br = new BufferedReader(new FileReader(pinFile)))
    {
      String pinStr = br.readLine();
      if (pinStr == null)
      {
        logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_CONTAINS_EMPTY_PIN.get(publisher.getDn(), pinFile));
        return "";
      }
      return pinStr;
    }
    catch (IOException ioe)
    {
      logger.warn(ERR_COMMON_AUDIT_ERROR_READING_KEYSTORE_PIN_FILE.get(publisher.getDn(), pinFile,
          stackTraceToSingleLineString(ioe)), ioe);
      return "";
    }
  }

  /**
   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
   * <p>
   * The common audit publisher may not already exist.
   * <p>
   * This method must not be used when the corresponding configuration is deleted, because it
   * implies checking the corresponding configuration entry in the server.
   *
   * @param config
   *          The log publisher configuration.
   * @return {@code true} if publisher corresponds to a common audit publisher
   * @throws ConfigException
   *           If an error occurs
   */
  public boolean isCommonAuditConfig(LogPublisherCfg config) throws ConfigException
  {
    return new PublisherConfig(config).isCommonAudit();
  }

  /**
   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
   *
   * @param config
   *          The log publisher configuration.
   * @return {@code true} if publisher is defined for common audit, {@code false} otherwise
   * @throws ConfigException
   *           If an error occurs
   */
  public boolean isExistingCommonAuditConfig(LogPublisherCfg config) throws ConfigException
  {
    String name = getConfigNormalizedName(config);
    return accessPublishers.containsKey(name) || httpAccessPublishers.containsKey(name);
  }

  /**
   * Indicates if HTTP access logging is enabled for common audit.
   *
   * @return {@code true} if there is at least one HTTP access logger enabled for common audit.
   */
  public boolean isHttpAccessLogEnabled()
  {
    return !httpAccessPublishers.isEmpty();
  }

  private String getConfigNormalizedName(LogPublisherCfg config)
  {
    return config.dn().toNormalizedUrlSafeString();
  }

  /**
   * Returns the audit service that manages HTTP Access logging.
   *
   * @return the request handler that accepts audit events
   */
  public RequestHandler getAuditServiceForHttpAccessLog()
  {
    return httpAccessAuditService;
  }

  /**
   * This class hides all ugly code needed to determine which type of publisher and audit event handler is needed.
   * <p>
   * In particular, it allows to retrieve a common configuration that can be used for log publishers that
   * publish to the same kind of handler.
   * For example: for CSV handler, DJ configurations for the log publishers contain the same methods but
   * do not have a common interface (CsvFileAccessLogPublisherCfg vs CsvFileHTTPAccessLogPublisherCfg).
   */
  private static class PublisherConfig
  {
    private final LogPublisherCfg config;
    private final boolean isCommonAudit;
    private LogType logType;
    private AuditType auditType;

    PublisherConfig(LogPublisherCfg config) throws ConfigException
    {
      this.config = config;
      ConfigEntry configEntry = DirectoryServer.getConfigEntry(config.dn());
      if (configEntry.hasObjectClass("ds-cfg-csv-file-access-log-publisher"))
      {
        auditType = AuditType.CSV;
        logType = LogType.ACCESS;
      }
      else if (configEntry.hasObjectClass("ds-cfg-csv-file-http-access-log-publisher"))
      {
        auditType = AuditType.CSV;
        logType = LogType.HTTP_ACCESS;
      }
      else if (configEntry.hasObjectClass("ds-cfg-external-access-log-publisher"))
      {
        auditType = AuditType.EXTERNAL;
        logType = LogType.ACCESS;
      }
      else if (configEntry.hasObjectClass("ds-cfg-external-http-access-log-publisher"))
      {
        auditType = AuditType.EXTERNAL;
        logType = LogType.HTTP_ACCESS;
      }
      isCommonAudit = auditType != null;
    }

    DN getDn()
    {
      return config.dn();
    }

    String getName()
    {
      return config.dn().getRDN(0).getAttributeValue(0).toString();
    }

    String getCommonAuditTopic() throws ConfigException
    {
      if (isAccessLog())
      {
        return "ldap-access";
      }
      else if (isHttpAccessLog())
      {
        return "http-access";
      }
      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
    }

    boolean isExternal()
    {
      return AuditType.EXTERNAL == auditType;
    }

    boolean isCsv()
    {
      return AuditType.CSV == auditType;
    }

    boolean isAccessLog()
    {
      return LogType.ACCESS == logType;
    }

    boolean isHttpAccessLog()
    {
      return LogType.HTTP_ACCESS == logType;
    }

    boolean isCommonAudit()
    {
      return isCommonAudit;
    }

    CsvConfigData getCsvConfig() throws ConfigException
    {
      if (isAccessLog())
      {
        CsvFileAccessLogPublisherCfg conf = (CsvFileAccessLogPublisherCfg) config;
        return new CsvConfigData(conf.getLogDirectory(), conf.getCsvQuoteChar(), conf.getCsvDelimiterChar(), conf
            .getCsvEolSymbols(), conf.isAsynchronous(), conf.isAutoFlush(), conf.isTamperEvident(), conf
            .getSignatureTimeInterval(), conf.getKeyStoreFile(), conf.getKeyStorePinFile(), conf.getRotationPolicy(),
            conf.getRetentionPolicy());
      }
      if (isHttpAccessLog())
      {
        CsvFileHTTPAccessLogPublisherCfg conf = (CsvFileHTTPAccessLogPublisherCfg) config;
        return new CsvConfigData(conf.getLogDirectory(), conf.getCsvQuoteChar(), conf.getCsvDelimiterChar(), conf
            .getCsvEolSymbols(), conf.isAsynchronous(), conf.isAutoFlush(), conf.isTamperEvident(), conf
            .getSignatureTimeInterval(), conf.getKeyStoreFile(), conf.getKeyStorePinFile(), conf.getRotationPolicy(),
            conf.getRetentionPolicy());
      }
      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
    }

    ExternalConfigData getExternalConfig() throws ConfigException
    {
      if (isAccessLog())
      {
        ExternalAccessLogPublisherCfg conf = (ExternalAccessLogPublisherCfg) config;
        return new ExternalConfigData(conf.getConfigFile());
      }
      if (isHttpAccessLog())
      {
        ExternalHTTPAccessLogPublisherCfg conf = (ExternalHTTPAccessLogPublisherCfg) config;
        return new ExternalConfigData(conf.getConfigFile());
      }
      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
    }

    @Override
    public boolean equals(Object obj)
    {
      if (this == obj)
      {
        return true;
      }
      if (!(obj instanceof PublisherConfig))
      {
        return false;
      }
      PublisherConfig other = (PublisherConfig) obj;
      return config.dn().equals(other.config.dn());
    }

    @Override
    public int hashCode()
    {
      return config.dn().hashCode();
    }

  }

  /** Types of audit handlers managed. */
  private enum AuditType
  {
    CSV, EXTERNAL
  }

  /** Types of log managed. */
  private enum LogType
  {
    ACCESS, HTTP_ACCESS
  }

  /**
   * Contains the parameters for a CSV handler.
   * <p>
   * OpenDJ log publishers that logs to a CSV handler have the same parameters but do not share
   * a common ancestor with all the parameters (e.g Access Log, HTTP Access Log, ...), hence this class
   * is necessary to avoid duplicating code that setup the configuration of the CSV handler.
   */
  private static class CsvConfigData
  {
    private final String logDirectory;
    private final String eolSymbols;
    private final String delimiterChar;
    private final String quoteChar;
    private final boolean asynchronous;
    private final boolean autoFlush;
    private final boolean tamperEvident;
    private final long signatureTimeInterval;
    private final String keystoreFile;
    private final String keystorePinFile;
    private final SortedSet<String> rotationPolicies;
    private final SortedSet<String> retentionPolicies;

    CsvConfigData(String logDirectory, String quoteChar, String delimiterChar, String eolSymbols, boolean asynchronous,
        boolean autoFlush, boolean tamperEvident, long signatureTimeInterval, String keystoreFile,
        String keystorePinFile, SortedSet<String> rotationPolicies, SortedSet<String> retentionPolicies)
    {
      this.logDirectory = logDirectory;
      this.quoteChar = quoteChar;
      this.delimiterChar = delimiterChar;
      this.eolSymbols = eolSymbols;
      this.asynchronous = asynchronous;
      this.autoFlush = autoFlush;
      this.tamperEvident = tamperEvident;
      this.signatureTimeInterval = signatureTimeInterval;
      this.keystoreFile = keystoreFile;
      this.keystorePinFile = keystorePinFile;
      this.rotationPolicies = rotationPolicies;
      this.retentionPolicies = retentionPolicies;
    }

    String getEndOfLineSymbols()
    {
      return eolSymbols;
    }

    char getDelimiterChar() throws ConfigException
    {
      String filtered = delimiterChar.replaceAll(Pattern.quote("\\"), "");
      if (filtered.length() != 1)
      {
        throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_DELIMITER_CHAR.get("", filtered));
      }
      return filtered.charAt(0);
    }

    public char getQuoteChar() throws ConfigException
    {
      String filtered = quoteChar.replaceAll(Pattern.quote("\\"), "");
      if (filtered.length() != 1)
      {
        throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_QUOTE_CHAR.get("", filtered));
      }
      return filtered.charAt(0);
    }

    String getLogDirectory()
    {
      return logDirectory;
    }

    boolean isAsynchronous()
    {
      return asynchronous;
    }

    boolean isAutoFlush()
    {
      return autoFlush;
    }

    boolean isTamperEvident()
    {
      return tamperEvident;
    }

    long getSignatureTimeInterval()
    {
      return signatureTimeInterval;
    }

    String getKeystoreFile()
    {
      return keystoreFile;
    }

    String getKeystorePinFile()
    {
      return keystorePinFile;
    }

    SortedSet<String> getRotationPolicies()
    {
      return rotationPolicies;
    }

    SortedSet<String> getRetentionPolicies()
    {
      return retentionPolicies;
    }
  }

  /**
   * Contains the parameters for an external handler.
   * <p>
   * OpenDJ log publishers that logs to an external handler have the same
   * parameters but do not share a common ancestor with all the parameters (e.g
   * Access Log, HTTP Access Log, ...), hence this class is necessary to avoid
   * duplicating code that setup the configuration of an external handler.
   */
  private static class ExternalConfigData
  {
    private final String configurationFile;

    ExternalConfigData(String configurationFile)
    {
      this.configurationFile = configurationFile;
    }

    String getConfigurationFile()
    {
      return configurationFile;
    }
  }

}
