/*
 * Copyright (c) 2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package eu.emi.security.authn.x509.helpers.ocsp;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.Map;

import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.SingleResp;


/**
 * OCSP responses cache: in memory with disk persistence.
 * 
 * @author K. Benedyczak
 */
public class OCSPResponsesCache extends OCSPCacheBase
{
	private Map<String, ResponseCacheEntry> responsesCache;
	
	/**
	 * 
	 * @param maxTtl maximum time after each cached response expires. Negative for no cache at all, 0 for no limit
	 * (i.e. caching time will be only controlled by the OCSP response validity period). In ms.
	 * @param diskPath if not null, cached responses will be stored on disk.
	 * @param prefix used if disk cache is enabled, as a common prefix for all files created in the cache directory. 
	 */
	public OCSPResponsesCache(long maxTtl, File diskPath, String prefix)
	{
		super(maxTtl, diskPath, prefix);
		responsesCache = Collections.synchronizedMap(new BoundedSizeLruMap<String, ResponseCacheEntry>(100));
	}

	/**
	 * 
	 * @param responseKey response key
	 * @param client OCSP client
	 * @param toCheckCert mandatory certificate to be checked
	 * @param issuerCert mandatory certificate of the toCheckCert issuer
	 * @return The cached response if available, null otherwise.
	 * @throws IOException IO exception
	 */
	public SingleResp getCachedResp(String responseKey, OCSPClientImpl client, X509Certificate toCheckCert, 
			X509Certificate issuerCert) throws IOException
	{
		ResponseCacheEntry cachedResp = responsesCache.get(responseKey);
		if (cachedResp == null && diskPath != null)
		{
			File f = new File(diskPath, prefix + responseKey);
			if (f.exists())
				cachedResp = loadResponseFromDisk(f, client, toCheckCert, issuerCert);
		}
		if (cachedResp == null)
			return null;
		
		Date nextUpdate = cachedResp.response != null ? cachedResp.response.getNextUpdate() : null;
		Date maxCacheValidity = new Date(cachedResp.cacheDate.getTime() + maxTtl);
		if (nextUpdate != null && maxCacheValidity.after(nextUpdate))
			maxCacheValidity = nextUpdate;
		if (maxCacheValidity.after(cachedResp.maxValidity))
			maxCacheValidity = cachedResp.maxValidity;
		
		Date now = new Date();
		if (now.after(maxCacheValidity))
		{
			responsesCache.remove(responseKey);
			if (diskPath != null)
			{
				File f = new File(diskPath, prefix + responseKey);
				f.delete();
			}
			return null;
		}
		
		return cachedResp.response;
	}
	
	public String createResponseKey(X509Certificate toCheckCert, X509Certificate issuerCert)
	{
		MessageDigest digest;
		try
		{
			digest = MessageDigest.getInstance("SHA-1");
		} catch (NoSuchAlgorithmException e)
		{
			throw new RuntimeException("JDK problem: SHA-1 hash not supported by any provider!", e);
		}
		PublicKey issuerKey = issuerCert.getPublicKey();
		digest.update(issuerCert.getSubjectX500Principal().getEncoded());
		digest.update(issuerKey.getEncoded());
		digest.update(toCheckCert.getSerialNumber().toByteArray());

		return encodeDigest(digest);
	}

	public void addToCache(String key, OCSPResponseStructure fullResp, SingleResp singleResp) throws IOException
	{
		if (fullResp.getMaxCache() == null)
			fullResp.setMaxCache(singleResp.getNextUpdate());

		responsesCache.put(key, new ResponseCacheEntry(new Date(), fullResp.getMaxCache(), singleResp));
		if (diskPath != null)
		{
			File f = new File(diskPath, prefix + key);
			storeResponseToDisk(f, fullResp);
		}
	}
	
	public void clearMemoryCache()
	{
		responsesCache.clear();
	}
	
	private void storeResponseToDisk(File f, OCSPResponseStructure fullResp) throws IOException
	{
		if (f.exists())
			f.delete();
		Date maxCache = fullResp.getMaxCache();
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
		try
		{
			oos.writeObject(maxCache);
			oos.writeObject(fullResp.getResponse().getEncoded());
		} finally
		{
			oos.close();
		}
	}

	private ResponseCacheEntry loadResponseFromDisk(File f, OCSPClientImpl client, X509Certificate toCheckCert, 
			X509Certificate issuerCert)
	{
		ObjectInputStream ois = null;
		try
		{
			ois = new ObjectInputStream(new FileInputStream(f));
			Date maxCache = (Date)ois.readObject();
			byte[] resp = (byte[]) ois.readObject();
			OCSPResp fullResp = new OCSPResp(resp);
			SingleResp diskResp = client.verifyResponse(fullResp, toCheckCert, issuerCert, null);
			return new ResponseCacheEntry(new Date(f.lastModified()), maxCache, diskResp);
		} catch (Exception e)
		{
			f.delete();
			return null;
		} finally 
		{
			if (ois != null)
				try
				{
					ois.close();
				} catch (IOException e)
				{ //ok
				}
		}
	}

	private static class ResponseCacheEntry
	{
		private Date cacheDate;
		private Date maxValidity;
		private SingleResp response;
		
		public ResponseCacheEntry(Date cacheDate, Date maxValidity, SingleResp response)
		{
			this.cacheDate = cacheDate;
			this.maxValidity = maxValidity;
			this.response = response;
		}
	}
}









