+ * + * The algorithm for cache is as follows: a HashMap is maintained for fast + * object lookup. Two linked lists are maintained: one keeps objects in the + * order they are accessed from cache, the other keeps objects in the order + * they were originally added to cache. When objects are added to cache, they + * are first wrapped by a CacheObject which maintains the following pieces + * of information:
+ * + * Keeping track of cache hits and misses lets one measure how efficient + * the cache is; the higher the percentage of hits, the more efficient. + */ + protected long cacheHits, cacheMisses = 0L; + + /** + * Create a new cache and specify the maximum size of for the cache in + * bytes, and the maximum lifetime of objects. + * + * @param maxSize the maximum number of objects the cache will hold. -1 + * means the cache has no max size. + * @param maxLifetime the maximum amount of time (in ms) objects can exist in + * cache before being deleted. -1 means objects never expire. + */ + public Cache(int maxSize, long maxLifetime) { + if (maxSize == 0) { + throw new IllegalArgumentException("Max cache size cannot be 0."); + } + this.maxCacheSize = maxSize; + this.maxLifetime = maxLifetime; + + // Our primary data structure is a hash map. The default capacity of 11 + // is too small in almost all cases, so we set it bigger. + map = new HashMap(103); + + lastAccessedList = new LinkedList(); + ageList = new LinkedList(); + } + + public synchronized Object put(Object key, Object value) { + Object oldValue = null; + // Delete an old entry if it exists. + if (map.containsKey(key)) { + oldValue = remove(key, true); + } + + CacheObject cacheObject = new CacheObject(value); + map.put(key, cacheObject); + // Make an entry into the cache order list. + // Store the cache order list entry so that we can get back to it + // during later lookups. + cacheObject.lastAccessedListNode = lastAccessedList.addFirst(key); + // Add the object to the age list + LinkedListNode ageNode = ageList.addFirst(key); + ageNode.timestamp = System.currentTimeMillis(); + cacheObject.ageListNode = ageNode; + + // If cache is too full, remove least used cache entries until it is not too full. + cullCache(); + + return oldValue; + } + + public synchronized Object get(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + CacheObject cacheObject = (CacheObject) map.get(key); + if (cacheObject == null) { + // The object didn't exist in cache, so increment cache misses. + cacheMisses++; + return null; + } + // Remove the object from it's current place in the cache order list, + // and re-insert it at the front of the list. + cacheObject.lastAccessedListNode.remove(); + lastAccessedList.addFirst(cacheObject.lastAccessedListNode); + + // The object exists in cache, so increment cache hits. Also, increment + // the object's read count. + cacheHits++; + cacheObject.readCount++; + + return cacheObject.object; + } + + public synchronized Object remove(Object key) { + return remove(key, false); + } + + /* + * Remove operation with a flag so we can tell coherence if the remove was + * caused by cache internal processing such as eviction or loading + */ + public synchronized Object remove(Object key, boolean internal) { + CacheObject cacheObject = (CacheObject) map.remove(key); + // If the object is not in cache, stop trying to remove it. + if (cacheObject == null) { + return null; + } + // Remove from the cache order list + cacheObject.lastAccessedListNode.remove(); + cacheObject.ageListNode.remove(); + // Remove references to linked list nodes + cacheObject.ageListNode = null; + cacheObject.lastAccessedListNode = null; + + return cacheObject.object; + } + + public synchronized void clear() { + Object[] keys = map.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + remove(keys[i]); + } + + // Now, reset all containers. + map.clear(); + lastAccessedList.clear(); + ageList.clear(); + + cacheHits = 0; + cacheMisses = 0; + } + + public synchronized int size() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.size(); + } + + public synchronized boolean isEmpty() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.isEmpty(); + } + + public synchronized Collection values() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + Object[] cacheObjects = map.values().toArray(); + Object[] values = new Object[cacheObjects.length]; + for (int i = 0; i < cacheObjects.length; i++) { + values[i] = ((CacheObject) cacheObjects[i]).object; + } + return Collections.unmodifiableList(Arrays.asList(values)); + } + + public synchronized boolean containsKey(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.containsKey(key); + } + + public void putAll(Map map) { + for (Iterator i = map.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry) i.next(); + Object value = entry.getValue(); + // If the map is another DefaultCache instance than the + // entry values will be CacheObject instances that need + // to be converted to the normal object form. + if (value instanceof CacheObject) { + value = ((CacheObject) value).object; + } + put(entry.getKey(), value); + } + } + + public synchronized boolean containsValue(Object value) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + CacheObject cacheObject = new CacheObject(value); + + return map.containsValue(cacheObject); + } + + public synchronized Set entrySet() { + // Warning -- this method returns CacheObject instances and not Objects + // in the same form they were put into cache. + + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableSet(map.entrySet()); + } + + public synchronized Set keySet() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableSet(map.keySet()); + } + + public long getCacheHits() { + return cacheHits; + } + + public long getCacheMisses() { + return cacheMisses; + } + + public int getMaxCacheSize() { + return maxCacheSize; + } + + public synchronized void setMaxCacheSize(int maxCacheSize) { + this.maxCacheSize = maxCacheSize; + // It's possible that the new max size is smaller than our current cache + // size. If so, we need to delete infrequently used items. + cullCache(); + } + + public long getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(long maxLifetime) { + this.maxLifetime = maxLifetime; + } + + /** + * Clears all entries out of cache where the entries are older than the + * maximum defined age. + */ + protected synchronized void deleteExpiredEntries() { + // Check if expiration is turned on. + if (maxLifetime <= 0) { + return; + } + + // Remove all old entries. To do this, we remove objects from the end + // of the linked list until they are no longer too old. We get to avoid + // any hash lookups or looking at any more objects than is strictly + // neccessary. + LinkedListNode node = ageList.getLast(); + // If there are no entries in the age list, return. + if (node == null) { + return; + } + + // Determine the expireTime, which is the moment in time that elements + // should expire from cache. Then, we can do an easy check to see + // if the expire time is greater than the expire time. + long expireTime = System.currentTimeMillis() - maxLifetime; + + while (expireTime > node.timestamp) { + if (remove(node.object, true) == null) { + System.err.println("Error attempting to remove(" + node.object.toString() + + ") - cacheObject not found in cache!"); + // remove from the ageList + node.remove(); + } + + // Get the next node. + node = ageList.getLast(); + // If there are no more entries in the age list, return. + if (node == null) { + return; + } + } + } + + /** + * Removes the least recently used elements if the cache size is greater than + * or equal to the maximum allowed size until the cache is at least 10% empty. + */ + protected synchronized void cullCache() { + // Check if a max cache size is defined. + if (maxCacheSize < 0) { + return; + } + + // See if the cache is too big. If so, clean out cache until it's 10% free. + if (map.size() > maxCacheSize) { + // First, delete any old entries to see how much memory that frees. + deleteExpiredEntries(); + // Next, delete the least recently used elements until 10% of the cache + // has been freed. + int desiredSize = (int) (maxCacheSize * .90); + for (int i=map.size(); i>desiredSize; i--) { + // Get the key and invoke the remove method on it. + if (remove(lastAccessedList.getLast().object, true) == null) { + System.err.println("Error attempting to cullCache with remove(" + + lastAccessedList.getLast().object.toString() + ") - " + + "cacheObject not found in cache!"); + lastAccessedList.getLast().remove(); + } + } + } + } + + /** + * Wrapper for all objects put into cache. It's primary purpose is to maintain + * references to the linked lists that maintain the creation time of the object + * and the ordering of the most used objects. + * + * This class is optimized for speed rather than strictly correct encapsulation. + */ + private static class CacheObject { + + /** + * Underlying object wrapped by the CacheObject. + */ + public Object object; + + /** + * A reference to the node in the cache order list. We keep the reference + * here to avoid linear scans of the list. Every time the object is + * accessed, the node is removed from its current spot in the list and + * moved to the front. + */ + public LinkedListNode lastAccessedListNode; + + /** + * A reference to the node in the age order list. We keep the reference + * here to avoid linear scans of the list. The reference is used if the + * object has to be deleted from the list. + */ + public LinkedListNode ageListNode; + + /** + * A count of the number of times the object has been read from cache. + */ + public int readCount = 0; + + /** + * Creates a new cache object wrapper. + * + * @param object the underlying Object to wrap. + */ + public CacheObject(Object object) { + this.object = object; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CacheObject)) { + return false; + } + + final CacheObject cacheObject = (CacheObject) o; + + if (!object.equals(cacheObject.object)) { + return false; + } + + return true; + } + + public int hashCode() { + return object.hashCode(); + } + } + + /** + * Simple LinkedList implementation. The main feature is that list nodes + * are public, which allows very fast delete operations when one has a + * reference to the node that is to be deleted.
+ */ + private static class LinkedList { + + /** + * The root of the list keeps a reference to both the first and last + * elements of the list. + */ + private LinkedListNode head = new LinkedListNode("head", null, null); + + /** + * Creates a new linked list. + */ + public LinkedList() { + head.next = head.previous = head; + } + + /** + * Returns the first linked list node in the list. + * + * @return the first element of the list. + */ + public LinkedListNode getFirst() { + LinkedListNode node = head.next; + if (node == head) { + return null; + } + return node; + } + + /** + * Returns the last linked list node in the list. + * + * @return the last element of the list. + */ + public LinkedListNode getLast() { + LinkedListNode node = head.previous; + if (node == head) { + return null; + } + return node; + } + + /** + * Adds a node to the beginning of the list. + * + * @param node the node to add to the beginning of the list. + */ + public LinkedListNode addFirst(LinkedListNode node) { + node.next = head.next; + node.previous = head; + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the beginning of the list by automatically creating a + * a new node and adding it to the beginning of the list. + * + * @param object the object to add to the beginning of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addFirst(Object object) { + LinkedListNode node = new LinkedListNode(object, head.next, head); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the end of the list by automatically creating a + * a new node and adding it to the end of the list. + * + * @param object the object to add to the end of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addLast(Object object) { + LinkedListNode node = new LinkedListNode(object, head, head.previous); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Erases all elements in the list and re-initializes it. + */ + public void clear() { + //Remove all references in the list. + LinkedListNode node = getLast(); + while (node != null) { + node.remove(); + node = getLast(); + } + + //Re-initialize. + head.next = head.previous = head; + } + + /** + * Returns a String representation of the linked list with a comma + * delimited list of all the elements in the list. + * + * @return a String representation of the LinkedList. + */ + public String toString() { + LinkedListNode node = head.next; + StringBuffer buf = new StringBuffer(); + while (node != head) { + buf.append(node.toString()).append(", "); + node = node.next; + } + return buf.toString(); + } + } + + /** + * Doubly linked node in a LinkedList. Most LinkedList implementations keep the + * equivalent of this class private. We make it public so that references + * to each node in the list can be maintained externally. + * + * Exposing this class lets us make remove operations very fast. Remove is + * built into this class and only requires two reference reassignments. If + * remove existed in the main LinkedList class, a linear scan would have to + * be performed to find the correct node to delete. + * + * The linked list implementation was specifically written for the Jive + * cache system. While it can be used as a general purpose linked list, for + * most applications, it is more suitable to use the linked list that is part + * of the Java Collections package. + */ + private static class LinkedListNode { + + public LinkedListNode previous; + public LinkedListNode next; + public Object object; + + /** + * This class is further customized for the Jive cache system. It + * maintains a timestamp of when a Cacheable object was first added to + * cache. Timestamps are stored as long values and represent the number + * of milliseconds passed since January 1, 1970 00:00:00.000 GMT.
+ * + * The creation timestamp is used in the case that the cache has a + * maximum lifetime set. In that case, when + * [current time] - [creation time] > [max lifetime], the object will be + * deleted from cache. + */ + public long timestamp; + + /** + * Constructs a new linked list node. + * + * @param object the Object that the node represents. + * @param next a reference to the next LinkedListNode in the list. + * @param previous a reference to the previous LinkedListNode in the list. + */ + public LinkedListNode(Object object, LinkedListNode next, + LinkedListNode previous) + { + this.object = object; + this.next = next; + this.previous = previous; + } + + /** + * Removes this node from the linked list that it is a part of. + */ + public void remove() { + previous.next = next; + next.previous = previous; + } + + /** + * Returns a String representation of the linked list node by calling the + * toString method of the node's object. + * + * @return a String representation of the LinkedListNode. + */ + public String toString() { + return object.toString(); + } + } +} \ No newline at end of file diff --git a/source/org/jivesoftware/smack/util/DNSUtil.java b/source/org/jivesoftware/smack/util/DNSUtil.java new file mode 100644 index 000000000..a483be66d --- /dev/null +++ b/source/org/jivesoftware/smack/util/DNSUtil.java @@ -0,0 +1,219 @@ +/** + * $Revision: 1456 $ + * $Date: 2005-06-01 22:04:54 -0700 (Wed, 01 Jun 2005) $ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.Attributes; +import java.util.Hashtable; +import java.util.Map; + +/** + * Utilty class to perform DNS lookups for XMPP services. + * + * @author Matt Tucker + */ +public class DNSUtil { + + /** + * Create a cache to hold the 100 most recently accessed DNS lookups for a period of + * 10 minutes. + */ + private static Map cache = new Cache(100, 1000*60*10); + + private static DirContext context; + + static { + try { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + context = new InitialDirContext(env); + } + catch (Exception e) { + // Ignore. + } + } + + /** + * Returns the host name and port that the specified XMPP server can be + * reached at for client-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-client._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5222.
+ * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return a HostAddress, which encompasses the hostname and port that the XMPP + * server can be reached at for the specified domain. + */ + public static HostAddress resolveXMPPDomain(String domain) { + if (context == null) { + return new HostAddress(domain, 5222); + } + String key = "c" + domain; + // Return item from cache if it exists. + if (cache.containsKey(key)) { + HostAddress address = (HostAddress)cache.get(key); + if (address != null) { + return address; + } + } + String host = domain; + int port = 5222; + try { + Attributes dnsLookup = context.getAttributes("_xmpp-client._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e) { + // Ignore. + } + // Host entries in DNS should end with a ".". + if (host.endsWith(".")) { + host = host.substring(0, host.length()-1); + } + HostAddress address = new HostAddress(host, port); + // Add item to cache. + cache.put(key, address); + return address; + } + + /** + * Returns the host name and port that the specified XMPP server can be + * reached at for server-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-server._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5269.
+ * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return a HostAddress, which encompasses the hostname and port that the XMPP + * server can be reached at for the specified domain. + */ + public static HostAddress resolveXMPPServerDomain(String domain) { + if (context == null) { + return new HostAddress(domain, 5269); + } + String key = "s" + domain; + // Return item from cache if it exists. + if (cache.containsKey(key)) { + HostAddress address = (HostAddress)cache.get(key); + if (address != null) { + return address; + } + } + String host = domain; + int port = 5269; + try { + Attributes dnsLookup = context.getAttributes("_xmpp-server._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e) { + // Attempt lookup with older "jabber" name. + try { + Attributes dnsLookup = context.getAttributes("_jabber._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e2) { } + } + // Host entries in DNS should end with a ".". + if (host.endsWith(".")) { + host = host.substring(0, host.length()-1); + } + HostAddress address = new HostAddress(host, port); + // Add item to cache. + cache.put(key, address); + return address; + } + + /** + * Encapsulates a hostname and port. + */ + public static class HostAddress { + + private String host; + private int port; + + private HostAddress(String host, int port) { + this.host = host; + this.port = port; + } + + /** + * Returns the hostname. + * + * @return the hostname. + */ + public String getHost() { + return host; + } + + /** + * Returns the port. + * + * @return the port. + */ + public int getPort() { + return port; + } + + public String toString() { + return host + ":" + port; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostAddress)) { + return false; + } + + final HostAddress address = (HostAddress) o; + + if (!host.equals(address.host)) { + return false; + } + if (port != address.port) { + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/test/org/jivesoftware/smack/util/CacheTest.java b/test/org/jivesoftware/smack/util/CacheTest.java new file mode 100644 index 000000000..34ce3251a --- /dev/null +++ b/test/org/jivesoftware/smack/util/CacheTest.java @@ -0,0 +1,77 @@ +/** + * $RCSfile$ + * $Revision: 2485 $ + * $Date: 2005-04-14 17:56:37 -0700 (Thu, 14 Apr 2005) $ + * + * Copyright (C) 2002-2003 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * 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 end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JIVE SOFTWARE OR + * ITS CONTRIBUTORS 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.jivesoftware.smack.util; + +import junit.framework.TestCase; + +/** + * A test case for the Cache class. + */ +public class CacheTest extends TestCase { + + public void testMaxSize() { + Cache cache = new Cache(100, -1); + for (int i=0; i<1000; i++) { + cache.put(new Integer(i), "value"); + assertTrue("Cache size must never be larger than 100.", cache.size() <= 100); + } + } + + public void testLRU() { + Cache cache = new Cache(100, -1); + for (int i=0; i<1000; i++) { + cache.put(new Integer(i), "value"); + assertTrue("LRU algorithm for cache key of '0' failed.", cache.get(new Integer(0)) != null); + } + } +}