/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package netscape.ldap; import java.util.*; import java.io.*; import netscape.ldap.client.*; import netscape.ldap.util.*; import java.util.zip.CRC32; /** * LDAPCache 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 * 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. *

* * Note that LDAPCache does not maintain consistency * with the directory, so that cached search results may no longer be * valid after a directory update. If the same application is performing * both cached searches and directory updates, then the * application should flush the corresponding cache entries after an update. * To do this use the flushEntries method. *

* * Also, note that search requests that return referrals are not cached. *

* * 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 Serializable { static final long serialVersionUID = 6275167993337814294L; /** * A hashtable of search results. The key is created from the search * request parameters (see createKey() method). The value is a Vector * where the first element is a Long integer representing the size * of all entries, followed by the actual search result entries (of type * LDAPEntry). */ private Hashtable m_cache; /** * A list of cached entries ordered by time (augments m_cache). Each * element in the list is a 2 element Vector where the element at index * 0 is the key in the m_cache table, and the element at index 1 is the * time when the entry was created. * The list is used to track the time-to-live limit and to implement the * FIFO algorithm when adding new entries; if the size of the new entry * exceeds the cache available space, the extra space is made by removing * existing cached results in the order of their entry in the cache. */ private Vector m_orderedStruct; private long m_timeToLive; private long m_maxSize; private String[] m_dns; private long m_remainingSize = 0; // Count of LDAPConnections that share this cache private int m_refCnt = 0; /** * Delimiter used internally when creating keys * for the cache. */ public static final String DELIM = "#"; private TTLTimer m_timer = null; private long m_totalOpers = 0; private long m_hits = 0; private long m_flushes = 0; // Debug can be activated by defining debug.cache property private static boolean m_debug = false; static { try { String traceProp = System.getProperty("debug.cache"); m_debug = (traceProp != null); } catch (Exception e) { ;// In browser access to property might not be allowed } } /** * 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; * 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(); // reset stats m_totalOpers = m_hits = m_flushes = 0; 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); // LDAPEntries start at idx 1, at idx 0 is a Long // (size of all LDAPEntries returned by search()) 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 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, LDAPConstraints 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 stay longer or equal to the time-to-live. */ synchronized void flushEntries() { Vector v = null; boolean delete = false; long currTime = System.currentTimeMillis(); 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 += ((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 * @return a flag indicating whether the entry was added. */ synchronized boolean addEntry(Long key, Object value) { // if entry exists, dont perform add operation if (m_cache.get(key) != null) return false; Vector v = (Vector)value; long size = ((Long)v.elementAt(0)).longValue(); if (size > m_maxSize) { if (m_debug) { System.out.println("Failed to add an entry to the cache since the new entry exceeds the cache size"); } return false; } // 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 += ((Long)val.elementAt(0)).longValue(); if (m_remainingSize >= size) break; } } m_remainingSize -= size; m_cache.put(key, v); Vector element = new Vector(2); element.addElement(key); element.addElement(new Long(System.currentTimeMillis())); m_orderedStruct.addElement(element); // Start TTL Timer if first entry is added if (m_orderedStruct.size() == 1) { scheduleTTLTimer(); } 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()); } return true; } /** * Flush entries which stayed longer or equal to the time-to-live, and * Set up the TTLTimer for the next flush. Called when first entry is * added to the cache and when the TTLTimer expires. */ synchronized void scheduleTTLTimer() { if (m_orderedStruct.size() <= 0) { return; } if (m_timer == null) { m_timer = new TTLTimer(this); } Vector v = (Vector)m_orderedStruct.firstElement(); long currTime = System.currentTimeMillis(); long creationTime = ((Long)v.elementAt(1)).longValue(); long timeout = creationTime + m_timeToLive - currTime; if (timeout > 0) { m_timer.start(timeout); } else { flushEntries(); scheduleTTLTimer(); } } /** * Gets the number of entries being cached. * @return the number of entries being cached. */ public int getNumEntries() { return m_cache.size(); } /** * Get number of LDAPConnections that share this cache * @return Reference Count */ int getRefCount() { return m_refCnt; } /** * Add a new reference to this cache. * */ synchronized void addReference() { m_refCnt++; if (m_debug) { System.err.println("Cache refCnt="+ m_refCnt); } } /** * Remove a reference to this cache. * If the reference count is 0, cleaup the cache. * */ synchronized void removeReference() { if (m_refCnt > 0) { m_refCnt--; if (m_debug) { System.err.println("Cache refCnt="+ m_refCnt); } if (m_refCnt == 0 ) { cleanup(); } } } /** * Cleans up */ synchronized void cleanup() { flushEntries(null, 0); if (m_timer != null) { m_timer.stop(); m_timer = null; } } /** * 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(); } /** * Concatenates the specified integer with the delimiter. * @param str the String to concatenate with the delimiter * @return the concatenated string. */ private String appendString(String str) { if (str == null) return "null"+DELIM; else return str.trim()+DELIM; } /** * Concatenates the specified integer with the delimiter. * @param num the integer to 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 { String[] sorted = new String[str.length]; System.arraycopy( str, 0, sorted, 0, str.length ); sortStrings(sorted); String s = sorted.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) { if (barray==null) { return 0; } 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. */ class TTLTimer implements Runnable{ private long m_timeout; private LDAPCache m_cache; private Thread t = null; /** * Constructor with the specified timout. * @param timeout the timeout value in milliseconds */ TTLTimer(LDAPCache cache) { m_cache = cache; } /** * (Re)start the timer. */ void start(long timeout) { m_timeout = timeout; if (Thread.currentThread() != t) { stop(); } t = new Thread(this, "LDAPCache-TTLTimer"); t.setDaemon(true); t.start(); } /** * Stop the timer. */ void stop() { if (t !=null) { t.interrupt(); } } /** * 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_timeout); } catch (InterruptedException e) { // This happens if the timer is stopped return; } } m_cache.scheduleTTLTimer(); } }