/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * The contents of this file are subject to the Netscape Public License * Version 1.0 (the "NPL"); you may not use this file except in * compliance with the NPL. You may obtain a copy of the NPL at * http://www.mozilla.org/NPL/ * * Software distributed under the NPL is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL * for the specific language governing rights and limitations under the * NPL. * * The Initial Developer of this code under the NPL is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1998 Netscape Communications Corporation. All Rights * Reserved. */ package netscape.ldap; import java.util.*; import java.io.*; import netscape.ldap.*; import netscape.ldap.client.*; import netscape.ldap.util.*; import java.util.zip.CRC32; /** * LDAPCache is the class that represents an * in-memory cache that you can use to reduce the number of * search requests sent to the LDAP server. *

* * Each item in the cache represents a search request and * its results. Each item is uniquely identified by the * search criteria, which includes: *

* *

*

* * After a search request is cached, the results of any * subsequent search requests using the same criteria are 8 * read from the cache. Note that if any part of the * criteria differs (for example, if a different DN is used * when binding to the server or if a different set of * attributes to be returned is specified), the search * request is sent to the server. *

* * When you create the cache, you specify the maximum amount * of time that an item can be kept in the cache. When an * item's age exceeds that time limit, the item is removed * from the cache. *

* * The cache also has a maximum size that you specify when * creating the cache. If adding a new item exceeds the * maximum size of the cache, the first entries in the cache * are removed to make enough space for the new item. *

* * Finally, when creating the cache, you can specify a list * of the base DNs in search requests that you want to cache. * For example, if you specify o=Airius.com as * a base DN to cache, your client caches search requests * where the base DN is o=Airius.com. *

* * To specify that you want to use a cache for a particular * LDAP session, call the setCache method of * the LDAPConnection object that you are * working with. *

* * All clones of an LDAPConnection object share * the same LDAPCache object. *

* * The LDAPCache class includes methods for * getting statistics (such as hit rates) from the cache and * for flushing entries from the cache. *

* * @see netscape.ldap.LDAPConnection#setCache(netscape.ldap.LDAPCache) * @see netscape.ldap.LDAPConnection#getCache */ public class LDAPCache implements TimerEventListener { private Hashtable m_cache; private long m_timeToLive; private long m_maxSize; private String[] m_dns; private Vector m_orderedStruct; private long m_remainingSize = 0; /** * Delimiter used internally when creating keys * for the cache. */ public static final String DELIM = "#"; private Timer m_timer = null; private static long TIMEOUT = 60000; private long m_totalOpers = 0; private static final boolean m_debug = false; private long m_hits = 0; private long m_flushes = 0; /** * Constructs a new LDAPCache object, using the * specified maximum size of the cache (in bytes) and the maximum * age of cached items (in seconds). When items in the cache * exceed this age, they are removed from the cache. *

* * @param ttl The maximum amount of time that an item can be cached * (in seconds) * @param size The maximum size of the cache (in bytes) */ public LDAPCache(long ttl, long size) { init(ttl, size); } /** * Constructs a new LDAPCache object, using the * specified maximum size of the cache (in bytes), and the maximum * age of cached items (in seconds), and an array of the base DNs * of searches that you want to cache. (For example, * if the array of base DNs includes o=Airius.com, * the cache stores search results if the base DN in the search * request is o=Airius.com.) *

* * @param ttl The maximum amount of time that an item can be cached * (in seconds) * @param size The maximum size of the cache (in bytes) * @param dns The list of base DNs of searches that you want to cache. */ public LDAPCache(long ttl, long size, String[] dns) { init(ttl, size); m_dns = new String[dns.length]; if ((dns != null) && (dns.length > 0)) for (int i=0; i * * @return The maximum size of the cache (in bytes) */ public long getSize() { return m_maxSize; } /** * Gets the maximum age allowed for cached items (in * seconds). (Items that exceed this age are * removed from the cache.) *

* * @return The maximum age of items in the cache (in * seconds). */ public long getTimeToLive() { return m_timeToLive/1000; } /** * Gets the array of base DNs of searches to be cached. * (Search requests with these base DNs are cached.) *

* * @return The array of base DNs. */ public String[] getBaseDNs() { return m_dns; } /** * Flush the entries identified by DN and scope from the cache. *

* * @param dn The distinguished name (or base DN) of the entries * to be removed from the cache. Use this parameter in conjunction * with scope to identify the entries that you want * removed from the cache. If this parameter is null, * the entire cache is flushed. * @param scope The scope identifying the entries that you want * removed from the cache. The value of this parameter can be * one of the following: *

*

* @return true if the entry is removed from the cache, * or false if the entry is not removed. */ public synchronized boolean flushEntries(String dn, int scope) { if (m_debug) System.out.println("DEBUG: User request for flushing entry: dn "+ dn+" and scope "+scope); // if the dn is null, invalidate the whole cache if (dn == null) { // reclaim all the cache spaces m_remainingSize = m_maxSize; m_cache.clear(); m_orderedStruct.removeAllElements(); return true; } DN dn2 = new DN(dn); Enumeration e = m_cache.keys(); while(e.hasMoreElements()) { Long key = (Long)e.nextElement(); Vector val = (Vector)m_cache.get(key); int j=1; int size2=val.size(); for (; j"+key); return true; } } if (m_debug) System.out.println("DEBUG: The number of keys in the cache is " +m_cache.size()); return false; } /** * Gets invoked when the timer expires. * @param e The timer event containing the timer itself. */ public void timerExpired(TimerEvent e) { flushEntries(); Timer t = (Timer)e.getSource(); t.start(); } /** * Gets the amount of available space (in bytes) left in the cache. *

* * @return The available space (in bytes) in the cache. */ public long getAvailableSize() { return m_remainingSize; } /** * Gets the total number of requests for retrieving items from * the cache. This includes both items successfully found in * the cache and items not found in the cache. *

* * @return The total number of requests for retrieving items from * the cache. */ public long getTotalOperations() { return m_totalOpers; } /** * Gets the total number of requests which failed to find and * retrieve an item from the cache. *

* * @return The number of requests that did not find and retrieve * an item in the cache. */ public long getNumMisses() { return (m_totalOpers - m_hits); } /** * Gets the total number of requests which successfully found and * retrieved an item from the cache. * @return The number of requests that successfully found and * retrieved an item from the cache. */ public long getNumHits() { return m_hits; } /** * Gets the total number of entries that are flushed when timer expires * and flushEntries is called. *

* * @return The total number of entries that are flushed when timer * expires. */ public long getNumFlushes() { return m_flushes; } /** * Create a key for a cache entry by concatenating all input parameters * @return The key for a cache entry * @exception LDAPException Thrown when failed to create key. */ Long createKey(String host, int port, String baseDN, String filter, int scope, String[] attrs, String bindDN, LDAPSearchConstraints cons) throws LDAPException { DN dn = new DN(baseDN); baseDN = dn.toString(); if (m_dns != null) { int i=0; for (; i= m_dns.length) throw new LDAPException(baseDN+" is not a cached base DN", LDAPException.OTHER); } String key = null; key = appendString(baseDN); key = key+appendString(scope); key = key+appendString(host); key = key+appendString(port); key = key+appendString(filter); key = key+appendString(attrs); key = key+appendString(bindDN); LDAPControl[] serverControls = null; LDAPControl[] clientControls = null; // get server and client controls if (cons != null) { serverControls = cons.getServerControls(); clientControls = cons.getClientControls(); } if ((serverControls != null) && (serverControls.length > 0)) { String[] objID = new String[serverControls.length]; for (int i=0; i 0)) { String[] objID = new String[clientControls.length]; for (int i=0; i "+key+ " not found in the cache."); else System.out.println("DEBUG: Entry whose key -> "+key+ " found in the cache."); } if (obj != null) m_hits++; return obj; } /** * Flush entries which stays longer or equal to the time-to-live. */ synchronized void flushEntries() { Vector v = null; boolean delete = false; Date date = new Date(); long currTime = date.getTime(); m_flushes = 0; while(true) { if (m_orderedStruct.size() <= 0) break; v = (Vector)m_orderedStruct.firstElement(); long diff = currTime-((Long)v.elementAt(1)).longValue(); if (diff >= m_timeToLive) { Long key = (Long)v.elementAt(0); if (m_debug) System.out.println("DEBUG: Timer flush entry whose key is "+key); Vector entry = (Vector)m_cache.remove(key); m_remainingSize = m_remainingSize + ((Long)entry.firstElement()).longValue(); // always delete the first one m_orderedStruct.removeElementAt(0); m_flushes++; } else break; } if (m_debug) System.out.println("DEBUG: The number of keys in the cache is " +m_cache.size()); } /** * Add the entry to the hashtable cache and to the vector respectively. * The vector is used to keep track of the order of the entries being added. * @param key The key for the cache entry. * @param value The cache entry being added to the cache for the specified * key. * @exception LDAPException Get thrown when failed to add the entry. */ synchronized void addEntry(Long key, Object value) throws LDAPException { // if entry exists, dont perform add operation if (m_cache.get(key) != null) return; Vector v = (Vector)value; // assume the size of the key is 4 bytes long size = ((Long)v.elementAt(0)).longValue()+4; if (size > m_maxSize) { throw new LDAPException("Failed to add an entry to the cache since the new entry exceeds the cache size", LDAPException.OTHER); } v.setElementAt(new Long(size), 0); // if the size of entry being added is bigger than the spare space in the // cache if (size > m_remainingSize) { while (true) { Vector element = (Vector)m_orderedStruct.firstElement(); Long str = (Long)element.elementAt(0); Vector val = (Vector)m_cache.remove(str); if (m_debug) System.out.println("DEBUG: The spare size of the cache is not big enough "+ "to hold the new entry, deleting the entry whose key -> "+str); // always remove the first one m_orderedStruct.removeElementAt(0); m_remainingSize = m_remainingSize + ((Long)val.elementAt(0)).longValue(); if (m_remainingSize >= size) break; } } m_remainingSize = m_remainingSize - size; m_cache.put(key, v); Vector element = new Vector(); element.addElement(key); Date date = new Date(); element.addElement(new Long(date.getTime())); m_orderedStruct.addElement(element); if (m_debug) { System.out.println("DEBUG: Adding a new entry whose key -> "+key); System.out.println("DEBUG: The current number of keys in the cache "+ m_cache.size()); } } /** * Gets the number of entries being cached. * @return The number of entries being cached. */ int size() { return m_cache.size(); } /** * Clean up */ void cleanup() { m_timer.stop(); } /** * Initialize the instance variables. */ private void init(long ttl, long size) { m_cache = new Hashtable(); m_timeToLive = ttl*1000; m_maxSize = size; m_remainingSize = size; m_dns = null; m_orderedStruct = new Vector(); m_timer = new Timer(TIMEOUT); m_timer.addTimerExpiredEventListener((TimerEventListener)this); m_timer.start(); } /** * Concatenate the specified integer with the delimiter. * @param str The string which concatenate with the delimiter. * @return The concatenated string */ private String appendString(String str) { if (str == null) return "null"+DELIM; else return str.trim()+DELIM; } /** * Concatenate the specified integer with the delimiter. * @param num The integer which concatenate with the delimiter. * @return The concatenated string */ private String appendString(int num) { return num+DELIM; } /** * Concatenate the specified string array with the delimiter. * @param str A string array. * @return The concatenated string */ private String appendString(String[] str) { if ((str == null) || (str.length < 1)) return "0"+DELIM; else { sortStrings(str); String s = str.length+DELIM; for (int i=0; i 0) { String t = str[i]; str[i] = str[j]; str[j] = t; } } } /** * Create a 32 bits CRC from the given byte array. */ private long getCRC32(byte[] barray) { CRC32 crcVal = new CRC32(); crcVal.update(barray); return crcVal.getValue(); } } /** * Represents a timer which will timeout for every certain interval. It * provides methods to start, stop, or restart timer. It also provides * methods to register/deregister the event listeners. */ class Timer { private long m_timeout; private Thread t = null; private TimerEventListener listener; protected TimerEventListener stopListener = null; /** * Constructor with the specified timout. * @param timeout The timeout value in milliseconds. */ Timer(long timeout) { m_timeout = timeout; } /** * Start the timer. */ void start() { TimerRunnable trun = new TimerRunnable(this); t = new Thread(trun); t.start(); } /** * Suspend the timer. */ void suspend() { t.suspend(); } /** * Stop the timer. */ void stop() { t.stop(); } /** * Restart the timer. */ void restart() { t.resume(); } /** * Get the timeout value. * @return the timeout value. */ long getTimeout() { return m_timeout; } /** * Notify the listener when the timer expires. */ void fireExpiredEvent() { TimerEvent event; if (stopListener != null) { event = new TimerEvent(this); stopListener.timerExpired(event); } } /** * Add the listener to the queue who wants to be notified when the timer * expires. */ void addTimerExpiredEventListener(TimerEventListener listener) { stopListener = listener; } /** * Remove the listener from the queue who will not get notified when the * timer expires. */ void removeTimerExpiredEventListener(TimerEventListener listener) { stopListener = null; } } /** * Represents the starting point for the timer thread to execute. */ class TimerRunnable implements Runnable { Timer m_timer; /** * Constructor with the specified timer object. * @param t The timer */ TimerRunnable(Timer t) { m_timer = t; } /** * The runnable waits until the timeout period has elapsed. It then notify * the registered listener who listens for the timeout event. */ public void run() { synchronized(this) { try { this.wait(m_timer.getTimeout()); } catch (InterruptedException e) { System.out.println("Timer get interrupted"); } } m_timer.fireExpiredEvent(); } } /** * Represents the timer event. When the timer expires, it will notify * all the registered listeners which receive the timer event. The * listener can retrieve the source of the event from the timer event. */ class TimerEvent extends EventObject { /** * Constructor with the specified source of the timer event. * @param source The source of the timer event. */ TimerEvent(Object source) { super(source); } } /** * The timer client needs to implement this interface if it wants to * receive the timeout event. */ interface TimerEventListener extends EventListener { /** * Gets invoked when the timer expires. * @param timeout Timeout event contains the source of the event which is * the timer. */ void timerExpired(TimerEvent timeout); }