package at.gv.egovernment.moa.spss.server.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Element;

import iaik.asn1.structures.Name;
import iaik.utils.RFC2253NameParser;
import iaik.utils.RFC2253NameParserException;

import at.gv.egovernment.moa.logging.LogMsg;
import at.gv.egovernment.moa.logging.Logger;
import at.gv.egovernment.moa.util.DOMUtils;

import at.gv.egovernment.moa.spss.util.MessageProvider;

/**
 * A class providing access to the MOA configuration data.
 * 
 * <p>Configuration data is read from an XML file, whose location is given by
 * the <code>moa.spss.server.configuration</code> system property.</p>
 * <p>This class implements the Singleton pattern. The <code>reload()</code>
 * method can be used to update the configuration data. Therefore, it is not
 * guaranteed that consecutive calls to <code>getInstance()</code> will return
 * the same <code>ConfigurationProvider</code> all the time. During the
 * processing of a web service request, the current
 * <code>TransactionContext</code> should be used to obtain the
 * <code>ConfigurationProvider</code> local to that request.</p>
 * 
 * @author Patrick Peck
 * @author Sven Aigner
 * @version $Id$
 */
public class ConfigurationProvider {

  /** 
   * The name of the system property which contains the file name of the 
   * configuration file.
   */
  public static final String CONFIG_PROPERTY_NAME =
    "moa.spss.server.configuration";

  /**
   * The name of the generic configuration property giving the root directory of
   * a directory based cert store.
   */
  public static final String DIRECTORY_CERTSTORE_PARAMETER_PROPERTY =
    "DirectoryCertStoreParameters.RootDir";

  /** The name of the generic configuration property which determines if
   * certificates should be added to the cert store automatically. */
  public static final String AUTO_ADD_CERTIFICATES_PROPERTY =
    "autoAddCertificates";

  /** The name of the generic configuration property whether the authority
   * info access should be used. */
  public static final String USE_AUTHORITY_INFO_ACCESS_PROPERTY =
    "useAuthorityInfoAccess";

  /** The name of the generic configuration property determining the maximum
   * age of CRL entries. */
  public static final String MAX_REVOCATION_AGE_PROPERTY = "maxRevocationAge";

  /**
   * The name of the generic configuration property giving the database URL of
   * the CRL archive.
   */
  public static final String DATABASE_ARCHIVE_PARAMETER_URL_PROPERTY =
    "DataBaseArchiveParameter.JDBCUrl";

  /**
   * The name of the generic configuration property giving the JDBC driver 
   * class name for accessing the database used for the the CRL archive.
   */
  public static final String DATABASE_ARCHIVE_PARAMETER_DRIVERCLASS_PROPERTY =
    "DataBaseArchiveParameter.JDBCDriverClass";

  /** The name of the generic configuration property determining whether
   * to check the revocation status of signer certificates. */
  public static final String REVOCATION_CHECKING_PROPERTY = "checkRevocation";

  /** The name of the generic configuration property determining whether to
   * archive revocation information. */
  public static final String ARCHIVE_REVOCATION_INFO_PROPERTY =
    "archiveRevocationInfo";

  /** The name of the generic configuration property used for setting the
   * signing time to a predefined value. (Use for testing purposes only). */
  public static final String TEST_SIGNING_TIME_PROPERTY = "test.SigningTime";

  /** 
   * A fake <code>IssuerAndSerial</code> object for storing KeyGroup information
   * accessible by all clients.
   */
  private static final IssuerAndSerial ANONYMOUS_ISSUER_SERIAL =
    new IssuerAndSerial(new Name(), new BigInteger("0"));

  /** Singleton instance. <code>null</code>, if none has been created. */
  private static ConfigurationProvider instance;

  //
  // configuration data
  //

  /** The warnings generated when building the configuration. */
  private List warnings = new ArrayList();

  /** The default digest method algorithm name */
  private String digestMethodAlgorithmName;

  /** The default canonicalization algorithm name */
  private String canonicalizationAlgorithmName;

  /**
   * A <code>Map</code> which contains generic configuration information. Maps a
   * configuration name (a <code>String</code>) to a configuration value (also a
   * <code>String</code>).
   */
  private Map genericConfiguration;

  /** 
   * A <code>List</code> of <code>HardwareCryptoModule</code> objects for 
   * configuring hardware modules.
   */
  private List hardwareCryptoModules;

  /** 
   * A <code>List</code> of <code>HardwareKey</code> objects containing the
   * configuration data for hardware keys.
   */
  private List hardwareKeyModules;

  /**
   * A <code>List</code> of <code>SoftwareKey</code> objects containing the
   * configuration data for software keys.
   */
  private List softwareKeyModules;

  /**
   * A <code>Map</code> which contains a KeyGroupId (a <code>String</code>) to
   * KeyGroup mapping.
   */
  private Map keyGroups;

  /**
   * A <code>Map</code> which contains the <code>IssuerAndSerial</code> to
   * <code>KeyGroup</code> mapping.
   */
  private Map keyGroupMappings;

  /** The default chaining mode. */
  private String defaultChainingMode;

  /** 
   * A <code>Map</code> which contains the <code>IssuerAndSerial</code> to
   * chaining mode (a <code>String</code>) mapping.
   */
  private Map chainingModes;

  /**
   * A <code>Map</code> which contains the CAIssuerDN (a <code>String</code>)
   * to distribution points (a <code>Set</code> of
   * <code>DistributionPoint</code>s) mapping.
   */
  private Map crlDistributionPoints;

  /** The CRL archive duration. */
  private int cRLArchiveDuration;

  /**
   * A <code>Map</code> which contains a mapping from
   * CreateSignatureEnvironmentProfile Ids (<code>String</code>) to
   * CreateSignatureEnvironmentProfile elements (an <code>Element</code>).
   */
  private Map createSignatureEnvironmentProfiles;

  /**
   * A <code>Map</code> which contains a mapping from
   * CreateTransformsInfoProfile Ids (<code>String</code>) to
   * CreateTransformsInfoProfile elements (an <code>Element</code>).
   */
  private Map createTransformsInfoProfiles;

  /**
   * A <code>Map</code> which contains a mapping from
   * VerifyTransformsInfoProfile Ids (<code>String</code>) to
   * VerifyTransformsInfoProfile elements (an <code>Element</code>).
   */
  private Map verifyTransformsInfoProfiles;

  /**
   * A <code>Map</code> which contains a mapping from
   * SupplementProfile Ids (<code>String</code>) to SupplementProfile elements
   * (an <code>Element</code>).
   */
  private Map supplementProfiles;

  /**
   * A <code>Map</code> which contains a TrustProfile Id (a <code>String</code>
   * to trust profile (a <code>TrustProfile</code>) mapping.
   */
  private Map trustProfiles;

  /**
   * Return the single instance of configuration data.
   * 
   * @return MOAConfigurationProvider The current configuration data.
   * @throws ConfigurationException Failure to load the configuration data.
   */
  public static synchronized ConfigurationProvider getInstance()
    throws ConfigurationException {

    if (instance == null) {
      reload();
    }
    return instance;
  }

  /**
   * Reload the configuration data and set it if successful.
   * 
   * @return MOAConfigurationProvider The loaded configuration data.
   * @throws ConfigurationException Failure to load the configuration data.
   */
  public static synchronized ConfigurationProvider reload()
    throws ConfigurationException {
    String fileName = System.getProperty(CONFIG_PROPERTY_NAME);

    if (fileName == null) {
      // find out where we are running and use the configuration provided
      // under WEB-INF/conf/moa-spss/MOA-SPSSConfiguration
      URL url = ConfigurationProvider.class.getResource("/");
      fileName =
        new File(url.getPath()).getParent()
          + "/conf/moa-spss/MOA-SPSSConfiguration.xml";
      info("config.05", new Object[] { CONFIG_PROPERTY_NAME });
    }

    instance = new ConfigurationProvider(fileName);
    return instance;
  }

  /**
   * Constructor for ConfigurationProvider.
   * 
   * @param fileName The name of the configuration file.
   * @throws ConfigurationException An error occurred loading the configuration.
   */
  public ConfigurationProvider(String fileName) throws ConfigurationException {
    load(fileName);
  }

  /**
   * Load the configuration data from XML file with the given name and build
   * the internal data structures representing the MOA configuration.
   * 
   * @param fileName The name of the XML file to load.
   * @throws ConfigurationException The MOA configuration could not be
   * read/built.
   */
  private void load(String fileName) throws ConfigurationException {
    FileInputStream stream = null;
    File configFile;
    File configRoot;
    Element configElem;
    ConfigurationPartsBuilder builder;
    List allKeyModules;
    

    // load the main config file
    try {
      configFile = new File(fileName);
      configRoot = new File(configFile.getParent());
      info("config.21", new Object[] { configFile.getAbsoluteFile()});
      stream = new FileInputStream(fileName);
      configElem = DOMUtils.parseXmlValidating(new FileInputStream(fileName));
    } catch (Throwable t) {
      throw new ConfigurationException("config.10", null, t);
    }

    // build the internal datastructures
    try {
      builder = new ConfigurationPartsBuilder(configElem);
      digestMethodAlgorithmName = builder.getDigestMethodAlgorithmName();
      canonicalizationAlgorithmName =
        builder.getCanonicalizationAlgorithmName();
      hardwareCryptoModules = builder.buildHardwareCryptoModules();
      hardwareKeyModules =
        builder.buildHardwareKeyModules(Collections.EMPTY_LIST);
      softwareKeyModules =
        builder.buildSoftwareKeyModules(hardwareKeyModules, configRoot);
      allKeyModules = new ArrayList(hardwareKeyModules);
      allKeyModules.addAll(softwareKeyModules);
      keyGroups = builder.buildKeyGroups(allKeyModules);
      keyGroupMappings =
        builder.buildKeyGroupMappings(keyGroups, ANONYMOUS_ISSUER_SERIAL);
      defaultChainingMode = builder.getDefaultChainingMode();
      chainingModes = builder.buildChainingModes();
      crlDistributionPoints = builder.buildCRLDistributionPoints();
      cRLArchiveDuration = builder.getCRLArchiveDuration();
      genericConfiguration = builder.buildGenericConfiguration();
      absolutizeCertStoreRoot(configRoot);
      createTransformsInfoProfiles =
        builder.buildCreateTransformsInfoProfiles(configRoot);
      createSignatureEnvironmentProfiles =
        builder.buildCreateSignatureEnvironmentProfiles(configRoot);
      verifyTransformsInfoProfiles =
        builder.buildVerifyTransformsInfoProfiles(configRoot);
      supplementProfiles = builder.buildSupplementProfiles(configRoot);
      trustProfiles = builder.buildTrustProfiles(configRoot);
      warnings = new ArrayList(builder.getWarnings());
      checkConsistency();
    } catch (Throwable t) {
      throw new ConfigurationException("config.11", null, t);
    } finally {
      try {
        if (stream != null) {
          stream.close();
        }
      } catch (IOException e) {
        // don't complain about this
      }
    }
  }

  /**
   * Returns the warnings encountered during building the configuration.
   * 
   * @return A <code>List</code> of <code>String</code>s, containing the
   * warning messages.
   */
  public List getWarnings() {
    return warnings;
  }

  /**
   * Make the <code>DIRECTORY_CERTSTORE_PARAMETER_PROPERTY</code> generic 
   * configuration value an absolute file name.
   * 
   * @param configRoot The root directory of the main configuration file.
   */
  private void absolutizeCertStoreRoot(File configRoot) {
    String certStoreRoot =
      getGenericConfiguration(DIRECTORY_CERTSTORE_PARAMETER_PROPERTY);

    if (certStoreRoot != null) {
      if (!new File(certStoreRoot).isAbsolute()) {
        // make the cert store absolute
        File absCertStore = new File(configRoot, certStoreRoot);

        setGenericConfiguration(
          DIRECTORY_CERTSTORE_PARAMETER_PROPERTY,
          absCertStore.getAbsolutePath());
      }
    } else {
      // no value given: set it to a reasonable (absolute) default
      File absCertStore = new File(configRoot, "certstore");

      setGenericConfiguration(
        DIRECTORY_CERTSTORE_PARAMETER_PROPERTY,
        absCertStore.getAbsolutePath());
    }
  }

  /**
   * Do some additional consistency checks on the configuration.
   */
  private void checkConsistency() {
    // check for valid DirectoryCertStoreParameters.RootDir
    String certStoreRoot =
      getGenericConfiguration(DIRECTORY_CERTSTORE_PARAMETER_PROPERTY);

    if (certStoreRoot != null) {
      File certStore = new File(certStoreRoot);

      if (!certStore.exists() && !certStore.isDirectory()) {
        boolean created = false;

        try {
          created = certStore.mkdirs();
        } finally {
          if (!created) {
            warn(
              "config.30",
              new Object[] { DIRECTORY_CERTSTORE_PARAMETER_PROPERTY });
          }
        }
      }
    }

  }

  /**
   * Return the name of the digest algorithm used during signature creation.
   * 
   * @return The digest method algorithm name, or an empty <code>String</code>,
   * if none has been configured.
   */
  public String getDigestMethodAlgorithmName() {
    return digestMethodAlgorithmName;
  }

  /**
   * Return the name of the canonicalization algorithm used during signature
   * creation.
   * 
   * @return The canonicalization algorithm name, or an empty
   * <code>String</code> if none has been configured.
   */
  public String getCanonicalizationAlgorithmName() {
    return canonicalizationAlgorithmName;
  }

  /**
   * Return the configured hardware crypto modules.
   * 
   * @return A <code>List</code> of <code>HardwareCryptoModule</code> objects
   * containing the hardware crypto module configurations.
   */
  public List getHardwareCryptoModules() {
    return hardwareCryptoModules;
  }

  /**
   * Return the hardware key modules configuration.
   * 
   * @return A <code>List</code> of <code>HardwareKeyModule</code> objects
   * containing the configuration of the hardware key modules.
   */
  public List getHardwareKeyModules() {
    return hardwareKeyModules;
  }

  /**
   * Return the software key module configuration.
   * 
   * @return A <code>List</code> of <code>SoftwareKeyModule</code> objects
   * containing the configuration of the software key modules.
   */
  public List getSoftwareKeyModules() {
    return softwareKeyModules;
  }

  /**
   * Return the key group mapping.
   * 
   * @return A mapping from key group ID (a <code>String</code>) to 
   * <code>KeyGroup</code> mapping.
   */
  public Map getKeyGroups() {
    return keyGroups;
  }

  /**
   * Return the set of <code>KeyGroupEntry</code>s of a given key group, which a
   * client (identified by an issuer/serial pair) may access.
   * 
   * @param issuer The issuer of the client certificate.
   * @param serial The serial number of the client certificate.
   * @param keyGroupId The ID of the key group.
   * @return A <code>Set</code> of all the <code>KeyGroupEntry</code>s in the
   * given key group, if the user may access them. Returns <code>null</code>, if
   * the user may not access the given key group or if the key group does not
   * exist.
   */
  public Set getKeyGroupEntries(
    Principal issuer,
    BigInteger serial,
    String keyGroupId) {

    IssuerAndSerial issuerAndSerial;
    Map mapping;

    if (issuer == null && serial == null) {
      issuerAndSerial = ANONYMOUS_ISSUER_SERIAL;
    } else {
      issuerAndSerial = new IssuerAndSerial(issuer, serial);
    }

    mapping = (Map) keyGroupMappings.get(issuerAndSerial);
    if (mapping != null) {
      KeyGroup keyGroup = (KeyGroup) mapping.get(keyGroupId);

      if (keyGroup != null) {
        return keyGroup.getKeyGroupEntries();
      }
    }
    return null;
  }

  /**
   * Return the chaining mode for a given trust anchor.
   * 
   * @param trustAnchor The trust anchor for which the chaining mode should be
   * returned.
   * @return The chaining mode for the given trust anchor. If the trust anchor
   * has not been configured separately, the system default will be returned.
   */
  public String getChainingMode(X509Certificate trustAnchor) {
    Principal issuer = trustAnchor.getIssuerDN();
    BigInteger serial = trustAnchor.getSerialNumber();
    IssuerAndSerial issuerAndSerial = new IssuerAndSerial(issuer, serial);

    String mode = (String) chainingModes.get(issuerAndSerial);
    return mode != null ? mode : defaultChainingMode;
  }

  /**
   * Return the CRL distribution points for a given CA.
   * 
   * @param cert The certificate for which the CRL distribution points should be
   * looked up. The issuer information is used to perform the lookup.
   * @return A <code>Set</code> of <code>DistributionPoint</code> objects. The
   * set will be empty, if no distribution points have been configured for this
   * certificate.
   */
  public Set getCRLDP(X509Certificate cert) {
    try {
      RFC2253NameParser nameParser =
        new RFC2253NameParser(cert.getIssuerDN().toString());
      String caIssuerDN = nameParser.parse().getName();
      Set dps = (Set) crlDistributionPoints.get(caIssuerDN);

      if (dps == null) {
        return Collections.EMPTY_SET;
      }
      return dps;
    } catch (RFC2253NameParserException e) {
      return Collections.EMPTY_SET;
    }
  }

  /**
   * Return the CRL archive duration.
   * 
   * @return The duration of how long to keep CRL archive entries (measured in
   * days).
   */
  public int getCRLArchiveDuration() {
    return cRLArchiveDuration;
  }

  /**
   * Sets a generic configuration value.
   * 
   * Existing values are overridden.
   * 
   * @param name The name of the generic configuration.
   * @param value The new value of the generic configuration.
   */
  private void setGenericConfiguration(String name, String value) {
    genericConfiguration.put(name, value);
  }

  /**
   * Return the value of a generic configuration.
   * 
   * @param name The name of the generic configuration.
   * @return The value of the generic configuration with the given name, or
   * <code>null</code>, if none can be found.
   */
  public String getGenericConfiguration(String name) {
    return (String) genericConfiguration.get(name);
  }

  /**
   * Return the value of a generic configuration, or a given default value.
   * 
   * @param name The name of the generic configuration.
   * @param defaultValue A default value to be returned in case that the generic
   * configuration with the given name does not exist.
   * @return The value of the generic configuration with the given name, or the
   * <code>defaultValue</code>, if none can be found.
   */
  public String getGenericConfiguration(String name, String defaultValue) {
    String value = (String) genericConfiguration.get(name);
    return value != null ? value : defaultValue;
  }

  /**
   * Return a <code>CreateTransformsInfoProfile</code> with the given ID.
   * 
   * @param id The <code>CreateTransformsInfoProfile</code> ID.
   * @return The <code>CreateTransformsInfoProfile</code> with the given
   * ID or <code>null</code>, if none exists.
   */
  public Element getCreateTransformsInfoProfile(String id) {
    return (Element) createTransformsInfoProfiles.get(id);
  }

  /**
   * Return a <code>CreateSignatureEnvironmentProfile</code> with the given ID.
   * 
   * @param id The <code>CreateSignatureEnvironmentProfile</code> ID.
   * @return The <code>CreateSignatureEnvironmentProfile</code> with the given
   * ID or <code>null</code>, if none exists.
   */
  public Element getCreateSignatureEnvironmentProfile(String id) {
    return (Element) createSignatureEnvironmentProfiles.get(id);
  }

  /**
   * Return a <code>VerifyTransformsInfoProfile</code> with the given ID.
   * 
   * @param id The <code>VerifyTransformsInfoProfile</code> ID.
   * @return The <code>VerifyTransformsInfoProfile</code> with the given ID or
   * <code>null</code>, if none exists.
   */
  public Element getVerifyTransformsInfoProfile(String id) {
    return (Element) verifyTransformsInfoProfiles.get(id);
  }

  /**
   * Return a <code>SupplementProfile</code> with the given ID.
   * 
   * @param id The <code>SupplementProfile</code> ID.
   * @return The <code>SupplementProfile</code> with the given ID or
   * <code>null</code>, if none exists.
   */
  public Element getSupplementProfile(String id) {
    return (Element) supplementProfiles.get(id);
  }

  /**
   * Return a <code>TrustProfile</code> with the given ID.
   * 
   * @param id The <code>TrustProfile</code> ID.
   * @return The <code>TrustProfile</code> with the given ID or
   * <code>null</code>, if none exists.
   */
  public TrustProfile getTrustProfile(String id) {
    return (TrustProfile) trustProfiles.get(id);
  }

  /**
   * Log a warning.
   * 
   * @param messageId The message ID.
   * @param parameters Additional parameters for the message.
   * @see at.gv.egovernment.moa.spss.server.util.MessageProvider
   */
  private static void info(String messageId, Object[] parameters) {
    MessageProvider msg = MessageProvider.getInstance();
    Logger.info(new LogMsg(msg.getMessage(messageId, parameters)));
  }

  /**
   * Log a warning.
   * 
   * @param messageId The message ID.
   * @param args Additional parameters for the message.
   * @see at.gv.egovernment.moa.spss.server.util.MessageProvider
   */
  private void warn(String messageId, Object[] args) {
    MessageProvider msg = MessageProvider.getInstance();
    String txt = msg.getMessage(messageId, args);

    Logger.warn(new LogMsg(txt));
    warnings.add(txt);
  }

}