/**
 * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2006-2014.
 *
 * Licensed 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.italiangrid.voms.util;

import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.italiangrid.voms.VOMSError;

import eu.emi.security.authn.x509.ProxySupport;
import eu.emi.security.authn.x509.RevocationParameters;
import eu.emi.security.authn.x509.StoreUpdateListener;
import eu.emi.security.authn.x509.ValidationErrorListener;
import eu.emi.security.authn.x509.ValidationResult;
import eu.emi.security.authn.x509.X509CertChainValidatorExt;
import eu.emi.security.authn.x509.impl.CertificateUtils;
import eu.emi.security.authn.x509.impl.FormatMode;

/**
 * A Certificate validator that caches validation results for a configurable
 * period of time. The cache is keyed by the fingerprint of the certificate at
 * the top of the chain (likely the EEC).
 *
 *
 * @author andreaceccanti
 *
 */
public class CachingCertificateValidator implements X509CertChainValidatorExt {

  /**
   * Simple concurrent cache for validation results
   */
  protected final ConcurrentMap<String, CachedValidationResult> validationResultsCache;

  /**
   * The wrapped CANL certificate validator
   */
  protected final X509CertChainValidatorExt validator;

  /**
   * The cache entry lifetime for this validator
   */
  protected final long cacheEntryLifetimeMsec;

  /**
   * Builds a caching validator wrapping the validator passed as argument.
   *
   * @param val
   *          The CANL validator to be wrapped.
   * @param maxCacheEntryLifetime
   *          the maximum cache entry lifetime (in msecs)
   */
  public CachingCertificateValidator(X509CertChainValidatorExt val,
    long maxCacheEntryLifetime) {

    cacheEntryLifetimeMsec = maxCacheEntryLifetime;
    validator = val;
    validationResultsCache = new ConcurrentHashMap<String, CachedValidationResult>();
  }

  /**
   * Checks whether the {@link CachedValidationResult} passed as argument has
   * expired with respect to the {@link #cacheEntryLifetimeMsec} defined for
   * this validator and the reference time passed as argument.
   *
   * @param cvr
   *          a {@link CachedValidationResult} object
   * @param referenceTime
   *          the reference time (msecs since the epoch)
   * @return <code>true</code> when expired, <code>false</code> otherwise
   */
  public boolean cachedValidationResultHasExpired(CachedValidationResult cvr,
    long referenceTime) {

    return (referenceTime - cvr.getTimestamp() > cacheEntryLifetimeMsec);
  }

  /**
   * Gets a validation result from the memory cache
   *
   * @param certFingerprint
   *          the certificate fingerprint for the certificate at the top of the
   *          chain
   * @return the validation result, if found. <code>null</code> otherwise.
   */
  protected ValidationResult getCachedResult(String certFingerprint) {

    CachedValidationResult cvr = validationResultsCache.get(certFingerprint);

    if (cvr == null)
      return null;

    if (!cachedValidationResultHasExpired(cvr, System.currentTimeMillis())) {
      return cvr.getResult();
    }

    validationResultsCache.remove(certFingerprint, cvr);
    return null;
  }

  /**
   * Obvious sanity checks on input certificate chain
   *
   * @param certChain
   *          the chain to be checked
   */
  private void certChainSanityChecks(X509Certificate[] certChain) {

    if (certChain == null)
      throw new IllegalArgumentException("Cannot validate a null cert chain.");

    if (certChain.length == 0)
      throw new IllegalArgumentException(
        "Cannot validate a cert chain of length 0.");
  }

  /**
   * Validates a certificate chain using the wrapped validator, caching the
   * result for future validation calls.
   *
   * @param certChain
   *          the certificate chain that will be validated
   * @return a possibly cached {@link ValidationResult}
   * @see eu.emi.security.authn.x509.X509CertChainValidator#validate(java.security.cert.X509Certificate[])
   */
  public ValidationResult validate(X509Certificate[] certChain) {

    certChainSanityChecks(certChain);

    String certFingerprint = null;

    try {
      certFingerprint = FingerprintHelper
        .getFingerprint(certChain[certChain.length - 1]);

    } catch (Throwable t) {

      String errorMsg = String.format("Error computing fingerprint for "
        + "certificate: %s. Cause: %s",
        CertificateUtils.format(certChain[0], FormatMode.COMPACT_ONE_LINE),
        t.getMessage());

      throw new VOMSError(errorMsg, t);

    }

    ValidationResult res = getCachedResult(certFingerprint);

    if (res == null) {
      res = validator.validate(certChain);
      validationResultsCache.putIfAbsent(certFingerprint,
        new CachedValidationResult(certFingerprint, res));
    }

    return res;

  }

  /**
   * @see eu.emi.security.authn.x509.X509CertChainValidatorExt#dispose()
   */
  public void dispose() {

    validator.dispose();
  }

  /**
   * @return the proxy support information
   * @see eu.emi.security.authn.x509.X509CertChainValidatorExt#getProxySupport()
   */
  public ProxySupport getProxySupport() {

    return validator.getProxySupport();
  }

  /**
   * @param certPath
   *          the certificate path that will be validated
   * @return the {@link ValidationResult}
   * @see eu.emi.security.authn.x509.X509CertChainValidator#validate(java.security.cert.CertPath)
   */
  public ValidationResult validate(CertPath certPath) {

    return validator.validate(certPath);
  }

  /**
   * @return revocation parameters for the wrapped validator
   * @see eu.emi.security.authn.x509.X509CertChainValidatorExt#getRevocationCheckingMode()
   */
  public RevocationParameters getRevocationCheckingMode() {

    return validator.getRevocationCheckingMode();
  }

  /**
   * @return trusted issuers from the wrapped validator
   * @see eu.emi.security.authn.x509.X509CertChainValidator#getTrustedIssuers()
   */
  public X509Certificate[] getTrustedIssuers() {

    return validator.getTrustedIssuers();
  }

  /**
   * @param listener
   *          the {@link ValidationErrorListener} to be added to this validator
   *          
   * @see eu.emi.security.authn.x509.X509CertChainValidator#addValidationListener(eu.emi.security.authn.x509.ValidationErrorListener)
   */
  public void addValidationListener(ValidationErrorListener listener) {

    validator.addValidationListener(listener);
  }

  /**
   * @param listener
   *        the {@link ValidationErrorListener} that must be removed from 
   *        this validator
   * @see eu.emi.security.authn.x509.X509CertChainValidator#removeValidationListener(eu.emi.security.authn.x509.ValidationErrorListener)
   */
  public void removeValidationListener(ValidationErrorListener listener) {

    validator.removeValidationListener(listener);
  }

  /**
   * @param listener
   *          the {@link StoreUpdateListener} that must be added to this 
   *          validator
   *        
   * @see eu.emi.security.authn.x509.X509CertChainValidator#addUpdateListener(eu.emi.security.authn.x509.StoreUpdateListener)
   */
  public void addUpdateListener(StoreUpdateListener listener) {

    validator.addUpdateListener(listener);
  }

  /**
   * @param listener
   *          the {@link StoreUpdateListener} that must be removed from this 
   *          validator
   *          
   * @see eu.emi.security.authn.x509.X509CertChainValidator#removeUpdateListener(eu.emi.security.authn.x509.StoreUpdateListener)
   */
  public void removeUpdateListener(StoreUpdateListener listener) {

    validator.removeUpdateListener(listener);
  }

}

/**
 * A validation result cache entry.
 *
 * @author cecco
 *
 */
class CachedValidationResult {

  /**
   * Default constructor.
   *
   * @param certificateFingerprint
   *          the certificate fingerprint for this entry
   * @param res
   *          the validation result
   */
  public CachedValidationResult(String certificateFingerprint,
    ValidationResult res) {

    certFingerprint = certificateFingerprint;
    result = res;
    timestamp = System.currentTimeMillis();
  }

  /** The certificate fingerprint for this cache entry **/
  private String certFingerprint;

  /** The validation result for this cache entry **/
  private ValidationResult result;

  /** The cache entry creation timestamp **/
  private long timestamp;

  /**
   * Returns the validation result for this entry.
   *
   * @return a {@link ValidationResult}
   */
  public ValidationResult getResult() {

    return result;
  }

  /**
   * Sets the validation result for this entry
   *
   * @param result
   *          a {@link ValidationResult}
   */
  public void setResult(ValidationResult result) {

    this.result = result;
  }

  /**
   * Returns this entry creation timestamp.
   *
   * @return the timestamp expressed as milliseconds since epoch
   */
  public long getTimestamp() {

    return timestamp;
  }

  /**
   * Sets this entry creation timestamp (in milliseconds since the epoch).
   *
   * @param timestamp
   *          the timestamp
   */
  public void setTimestamp(long timestamp) {

    this.timestamp = timestamp;
  }

  /**
   * Returns the certificate fingerprint for this entry.
   *
   * The certificate fingerprint is the SHA1 hash of the DER encoding of the
   * certificate.
   *
   *
   *
   * @return the fingerprint for this entry
   * @see FingerprintHelper
   */
  public String getCertFingerprint() {

    return certFingerprint;
  }

  /**
   *
   * Sets the certificate finger for this entry. The certificate fingerprint is
   * the SHA1 hash of the DER encoding of the certificate.
   *
   * It can be computed with the
   * {@link FingerprintHelper#getFingerprint(X509Certificate)} method.
   *
   * @param certFingerprint
   *          a certificate fingerprint describing a certificate
   */
  public void setCertFingerprint(String certFingerprint) {

    this.certFingerprint = certFingerprint;
  }

  @Override
  public int hashCode() {

    final int prime = 31;
    int result = 1;
    result = prime * result
      + ((certFingerprint == null) ? 0 : certFingerprint.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {

    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    CachedValidationResult other = (CachedValidationResult) obj;
    if (certFingerprint == null) {
      if (other.certFingerprint != null)
        return false;
    } else if (!certFingerprint.equals(other.certFingerprint))
      return false;
    return true;
  }
}
