/*
 * dnssecjava - a DNSSEC validating stub resolver for Java
 * Copyright (c) 2013-2015 Ingo Bauersachs
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * This file is based on work under the following copyright and permission
 * notice:
 * 
 *     Copyright (c) 2005 VeriSign. All rights reserved.
 * 
 *     Redistribution and use in source and binary forms, with or without
 *     modification, are permitted provided that the following conditions are
 *     met:
 * 
 *     1. Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *     2. Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *     3. The name of the author may not be used to endorse or promote
 *        products derived from this software without specific prior written
 *        permission.
 * 
 *     THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 *     IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *     ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 *     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 *     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 *     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 *     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 *     IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *     POSSIBILITY OF SUCH DAMAGE.
 */

package org.jitsi.dnssec.validator;

import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

import org.jitsi.dnssec.SecurityStatus;
import org.xbill.DNS.Name;
import org.xbill.DNS.Type;

/**
 * Cache for DNSKEY RRsets or corresponding null/bad key entries with a limited
 * size and respect for TTL values.
 * 
 * @author davidb
 * @author Ingo Bauersachs
 */
public class KeyCache {
    /** Name of the property that configures the maximum cache TTL. */
    public static final String MAX_TTL_CONFIG = "org.jitsi.dnssec.keycache.max_ttl";

    /** Name of the property that configures the maximum cache size. */
    public static final String MAX_CACHE_SIZE_CONFIG = "org.jitsi.dnssec.keycache.max_size";

    private static final int MILLISECONDS_PER_SECOND = 1000;
    private static final int DEFAULT_MAX_TTL = 900;
    private static final int DEFAULT_MAX_CACHE_SIZE = 1000;

    /** This is the main caching data structure. */
    private Map<String, CacheEntry> cache;

    /** This is the maximum TTL [s] that all key cache entries will have. */
    private long maxTtl = DEFAULT_MAX_TTL;

    /** This is the maximum number of entries that the key cache will hold. */
    private int maxCacheSize = DEFAULT_MAX_CACHE_SIZE;

    /**
     * Creates a new instance of this class.
     */
    public KeyCache() {
        this.cache = Collections.synchronizedMap(new LinkedHashMap<String, CacheEntry>() {
            @Override
            protected boolean removeEldestEntry(java.util.Map.Entry<String, CacheEntry> eldest) {
                return size() >= KeyCache.this.maxCacheSize;
            }
        });
    }

    /**
     * Initialize the cache. This implementation recognizes the following
     * configuration parameters:
     * <dl>
     * <dt>org.jitsi.dnssec.keycache.max_ttl
     * <dd>The maximum TTL to apply to any cache entry.
     * <dt>org.jitsi.dnssec.keycache.max_size
     * <dd>The maximum number of entries that the cache will hold.
     * </dl>
     * 
     * @param config The configuration information.
     */
    public void init(Properties config) {
        if (config == null) {
            return;
        }

        String s = config.getProperty(MAX_TTL_CONFIG);
        if (s != null) {
            this.maxTtl = Long.parseLong(s);
        }

        s = config.getProperty(MAX_CACHE_SIZE_CONFIG);
        if (s != null) {
            this.maxCacheSize = Integer.parseInt(s);
        }
    }

    /**
     * Find the 'closest' trusted DNSKEY rrset to the given name.
     * 
     * @param n The name to start the search.
     * @param dclass The class this DNSKEY rrset should be in.
     * 
     * @return The 'closest' entry to 'n' in the same class as 'dclass'.
     */
    public KeyEntry find(Name n, int dclass) {
        while (n.labels() > 0) {
            String k = this.key(n, dclass);
            KeyEntry entry = this.lookupEntry(k);
            if (entry != null) {
                return entry;
            }

            n = new Name(n, 1);
        }

        return null;
    }

    /**
     * Store a {@link KeyEntry} in the cache. The entry will be ignored if it's
     * rrset isn't a DNSKEY rrset or if it doesn't have the SECURE security
     * status.
     * 
     * @param ke The key entry to cache.
     * @return The passed {@link KeyEntry} to allow method chaining.
     */
    public KeyEntry store(KeyEntry ke) {
        if (ke.getRRset() != null) {
            if (ke.getRRset().getType() != Type.DNSKEY) {
                return ke;
            }

            if (ke.getRRset().getSecurityStatus() != SecurityStatus.SECURE) {
                return ke;
            }
        }

        String k = this.key(ke.getName(), ke.getDClass());
        CacheEntry ce = new CacheEntry(ke, this.maxTtl);
        this.cache.put(k, ce);
        return ke;
    }

    private String key(Name n, int dclass) {
        return "K" + dclass + "/" + n;
    }

    private KeyEntry lookupEntry(String key) {
        CacheEntry centry = this.cache.get(key);
        if (centry == null) {
            return null;
        }

        if (centry.expiration.before(new Date())) {
            this.cache.remove(key);
            return null;
        }

        return centry.keyEntry;
    }

    /**
     * Utility class to cache key entries with an expiration date.
     */
    private static class CacheEntry {
        private Date expiration;
        private KeyEntry keyEntry;

        CacheEntry(KeyEntry keyEntry, long maxTtl) {
            long ttl = keyEntry.getTTL();
            if (ttl > maxTtl) {
                ttl = maxTtl;
            }

            this.expiration = new Date(System.currentTimeMillis() + (ttl * MILLISECONDS_PER_SECOND));
            this.keyEntry = keyEntry;
        }
    }
}
