diff --git a/LICENSE.txt b/LICENSE.txt
index f36fa5a439b668d3d94e2eace040e070d0a5ec7e..0539f2bcdbf35b0102153e1dfed5a3606f4eeeb7 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -72,6 +72,10 @@ Public domain except as listed below:
    Contains some code Copyright 2006 Sun Microsystems, Inc.
    See licenses/LICENSE-InstallCert.txt
 
+   BlockFile:
+   Copyright (c) 2006, Matthew Estes
+   See licenses/LICENSE-BlockFile.txt
+
 
 Router:
 Public domain except as listed below:
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java
index 22bf33b5c6e45a6d2b451baa36d89fd1954d12b4..14d8b3bd29eb3f909938348e6d8fb7dc04ecec0c 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java
@@ -24,7 +24,6 @@ package net.i2p.addressbook;
 import java.io.File;
 import java.io.IOException;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
 
 import net.i2p.I2PAppContext;
@@ -42,7 +41,7 @@ class AddressBook {
 
     private String location;
 
-    private Map addresses;
+    private Map<String, String> addresses;
 
     private boolean modified;
 
@@ -53,7 +52,7 @@ class AddressBook {
      *            A Map containing human readable addresses as keys, mapped to
      *            base64 i2p destinations.
      */
-    public AddressBook(Map addresses) {
+    public AddressBook(Map<String, String> addresses) {
         this.addresses = addresses;
     }
 
@@ -139,7 +138,7 @@ class AddressBook {
      *         is a human readable name, and the value is a base64 i2p
      *         destination.
      */
-    public Map getAddresses() {
+    public Map<String, String> getAddresses() {
         return this.addresses;
     }
 
@@ -167,10 +166,10 @@ class AddressBook {
     private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100;  // longer than any known cert type for now
 
     /**
-     * Do basic validation of the hostname and dest
+     * Do basic validation of the hostname
      * hostname was already converted to lower case by ConfigParser.parse()
      */
-    private static boolean valid(String host, String dest) {
+    public static boolean isValidKey(String host) {
 	return
 		host.endsWith(".i2p") &&
 		host.length() > 4 &&
@@ -194,8 +193,15 @@ class AddressBook {
                 (! host.equals("console.i2p")) &&
                 (! host.endsWith(".proxy.i2p")) &&
                 (! host.endsWith(".router.i2p")) &&
-                (! host.endsWith(".console.i2p")) &&
+                (! host.endsWith(".console.i2p"))
+                ;	
+    }
 
+    /**
+     * Do basic validation of the b64 dest, without bothering to instantiate it
+     */
+    private static boolean isValidDest(String dest) {
+	return
                 // null cert ends with AAAA but other zero-length certs would be AA
 		((dest.length() == MIN_DEST_LENGTH && dest.endsWith("AA")) ||
 		 (dest.length() > MIN_DEST_LENGTH && dest.length() <= MAX_DEST_LENGTH)) &&
@@ -218,13 +224,11 @@ class AddressBook {
      *            The log to write messages about new addresses or conflicts to.
      */
     public void merge(AddressBook other, boolean overwrite, Log log) {
-        Iterator otherIter = other.addresses.keySet().iterator();
-
-        while (otherIter.hasNext()) {
-            String otherKey = (String) otherIter.next();
-            String otherValue = (String) other.addresses.get(otherKey);
+        for (Map.Entry<String, String> entry : other.addresses.entrySet()) {
+            String otherKey = entry.getKey();
+            String otherValue = entry.getValue();
 
-            if (valid(otherKey, otherValue)) {
+            if (isValidKey(otherKey) && isValidDest(otherValue)) {
                 if (this.addresses.containsKey(otherKey) && !overwrite) {
                     if (!this.addresses.get(otherKey).equals(otherValue)
                             && log != null) {
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
index 7efc27ea5cae8eff4353dc94c3469054a70c663d..b09804f23a08fc8a5e6c1d0b17eee4707f08f1b3 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
@@ -30,7 +30,6 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.StringReader;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -85,8 +84,8 @@ class ConfigParser {
      *             if the BufferedReader cannot be read.
      *  
      */
-    public static Map parse(BufferedReader input) throws IOException {
-        Map result = new HashMap();
+    public static Map<String, String>  parse(BufferedReader input) throws IOException {
+        Map<String, String>  result = new HashMap();
         String inputLine;
         inputLine = input.readLine();
         while (inputLine != null) {
@@ -111,11 +110,11 @@ class ConfigParser {
      * @throws IOException
      *             if file cannot be read.
      */
-    public static Map parse(File file) throws IOException {
+    public static Map<String, String>  parse(File file) throws IOException {
         FileInputStream fileStream = new FileInputStream(file);
         BufferedReader input = new BufferedReader(new InputStreamReader(
                 fileStream));
-        Map rv = ConfigParser.parse(input);
+        Map<String, String>  rv = ConfigParser.parse(input);
         try {
             fileStream.close();
         } catch (IOException ioe) {}
@@ -132,7 +131,7 @@ class ConfigParser {
      * @throws IOException
      *             if file cannot be read.
      */
-    public static Map parse(String string) throws IOException {
+    public static Map<String, String>  parse(String string) throws IOException {
         StringReader stringReader = new StringReader(string);
         BufferedReader input = new BufferedReader(stringReader);
         return ConfigParser.parse(input);
@@ -149,14 +148,13 @@ class ConfigParser {
      * @return A Map containing the key, value pairs from file, or if file
      *         cannot be read, map.
      */
-    public static Map parse(File file, Map map) {
-        Map result;
+    public static Map<String, String>  parse(File file, Map<String, String>  map) {
+        Map<String, String>  result;
         try {
             result = ConfigParser.parse(file);
-            for (Iterator iter = map.keySet().iterator(); iter.hasNext(); ) {
-                String key = (String) iter.next();
-                if (!result.containsKey(key))
-                    result.put(key, map.get(key));
+            for (Map.Entry<String, String> entry : map.entrySet()) {
+                if (!result.containsKey(entry.getKey()))
+                    result.put(entry.getKey(), entry.getValue());
             }
         } catch (IOException exp) {
             result = map;
@@ -177,9 +175,9 @@ class ConfigParser {
      * @throws IOException
      *             if input cannot be read.
      */
-    public static List parseSubscriptions(BufferedReader input)
+    public static List<String> parseSubscriptions(BufferedReader input)
             throws IOException {
-        List result = new LinkedList();
+        List<String> result = new LinkedList();
         String inputLine = input.readLine();
         while (inputLine != null) {
             inputLine = ConfigParser.stripComments(inputLine).trim();
@@ -201,11 +199,11 @@ class ConfigParser {
      * @throws IOException
      *             if file cannot be read.
      */
-    public static List parseSubscriptions(File file) throws IOException {
+    public static List<String> parseSubscriptions(File file) throws IOException {
         FileInputStream fileStream = new FileInputStream(file);
         BufferedReader input = new BufferedReader(new InputStreamReader(
                 fileStream));
-        List rv = ConfigParser.parseSubscriptions(input);
+        List<String> rv = ConfigParser.parseSubscriptions(input);
         try {
             fileStream.close();
         } catch (IOException ioe) {}
@@ -221,7 +219,7 @@ class ConfigParser {
      * @throws IOException
      *             if string cannot be read.
      */
-    public static List parseSubscriptions(String string) throws IOException {
+    public static List<String> parseSubscriptions(String string) throws IOException {
         StringReader stringReader = new StringReader(string);
         BufferedReader input = new BufferedReader(stringReader);
         return ConfigParser.parseSubscriptions(input);
@@ -238,8 +236,8 @@ class ConfigParser {
      * @return A List consisting of one element for each line in file, or if
      *         file cannot be read, list.
      */
-    public static List parseSubscriptions(File file, List list) {
-        List result;
+    public static List<String> parseSubscriptions(File file, List<String> list) {
+        List<String> result;
         try {
             result = ConfigParser.parseSubscriptions(file);
         } catch (IOException exp) {
@@ -263,12 +261,9 @@ class ConfigParser {
      * @throws IOException
      *             if the BufferedWriter cannot be written to.
      */
-    public static void write(Map map, BufferedWriter output) throws IOException {
-        Iterator keyIter = map.keySet().iterator();
-
-        while (keyIter.hasNext()) {
-            String key = (String) keyIter.next();
-            output.write(key + "=" + (String) map.get(key));
+    public static void write(Map<String, String>  map, BufferedWriter output) throws IOException {
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            output.write(entry.getKey() + '=' + entry.getValue());
             output.newLine();
         }
         output.close();
@@ -288,7 +283,7 @@ class ConfigParser {
      * @throws IOException
      *             if file cannot be written to.
      */
-    public static void write(Map map, File file) throws IOException {
+    public static void write(Map<String, String>  map, File file) throws IOException {
         boolean success = false;
         if (!isWindows) {
             File tmp = SecureFile.createTempFile("temp-", ".tmp", file.getAbsoluteFile().getParentFile());
@@ -318,12 +313,10 @@ class ConfigParser {
      * @throws IOException
      *             if output cannot be written to.
      */
-    public static void writeSubscriptions(List list, BufferedWriter output)
+    public static void writeSubscriptions(List<String> list, BufferedWriter output)
             throws IOException {
-        Iterator iter = list.iterator();
-
-        while (iter.hasNext()) {
-            output.write((String) iter.next());
+        for (String s : list) {
+            output.write(s);
             output.newLine();
         }
         output.close();
@@ -340,7 +333,7 @@ class ConfigParser {
      * @throws IOException
      *             if output cannot be written to.
      */
-    public static void writeSubscriptions(List list, File file)
+    public static void writeSubscriptions(List<String> list, File file)
             throws IOException {
         ConfigParser.writeSubscriptions(list, new BufferedWriter(
                 new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
index 274fa8c4f6401e78fd991a26e481c88a17e3e782..fd0e42eadbb64749fc713bb3e0d240bd8599a74f 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
@@ -29,6 +29,10 @@ import java.util.List;
 import java.util.Map;
 
 import net.i2p.I2PAppContext;
+import net.i2p.client.naming.NamingService;
+import net.i2p.client.naming.SingleFileNamingService;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
 import net.i2p.util.SecureDirectory;
 
 /**
@@ -55,19 +59,20 @@ public class Daemon {
      * @param published
      *            The published AddressBook. This address book is published on
      *            the user's eepsite so that others may subscribe to it.
+     *            If non-null, overwrite with the new addressbook.
      * @param subscriptions
      *            A SubscriptionList listing the remote address books to update
      *            from.
      * @param log
      *            The log to write changes and conflicts to.
      */
-    public void update(AddressBook master, AddressBook router,
+    public static void update(AddressBook master, AddressBook router,
             File published, SubscriptionList subscriptions, Log log) {
         router.merge(master, true, null);
-        Iterator iter = subscriptions.iterator();
+        Iterator<AddressBook> iter = subscriptions.iterator();
         while (iter.hasNext()) {
             // yes, the EepGet fetch() is done in next()
-            router.merge((AddressBook) iter.next(), false, log);
+            router.merge(iter.next(), false, log);
         }
         router.write();
         if (published != null)
@@ -75,6 +80,71 @@ public class Daemon {
         subscriptions.write();
     }
 
+    /**
+     * Update the router and published address books using remote data from the
+     * subscribed address books listed in subscriptions.
+     * 
+     * @param router
+     *            The router AddressBook. This is the address book read by
+     *            client applications.
+     * @param published
+     *            The published AddressBook. This address book is published on
+     *            the user's eepsite so that others may subscribe to it.
+     *            If non-null, overwrite with the new addressbook.
+     * @param subscriptions
+     *            A SubscriptionList listing the remote address books to update
+     *            from.
+     * @param log
+     *            The log to write changes and conflicts to.
+     * @since 0.8.6
+     */
+    public static void update(NamingService router, File published, SubscriptionList subscriptions, Log log) {
+        NamingService publishedNS = null;
+        Iterator<AddressBook> iter = subscriptions.iterator();
+        while (iter.hasNext()) {
+            // yes, the EepGet fetch() is done in next()
+            AddressBook sub = iter.next();
+            for (Map.Entry<String, String> entry : sub.getAddresses().entrySet()) {
+                String key = entry.getKey();
+                Destination oldDest = router.lookup(key);
+                try {
+                    if (oldDest == null) {
+                        if (AddressBook.isValidKey(key)) {
+                            Destination dest = new Destination(entry.getValue());
+                            boolean success = router.put(key, dest);
+                            if (log != null) {
+                                if (success)
+                                    log.append("New address " + key +
+                                               " added to address book. From: " + sub.getLocation());
+                                else
+                                    log.append("Save to naming service " + router + " failed for new key " + key);
+                            }
+                            // now update the published addressbook
+                            if (published != null) {
+                                if (publishedNS == null)
+                                    publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath());
+                                success = publishedNS.putIfAbsent(key, dest);
+                                if (!success)
+                                    log.append("Save to published addressbook " + published.getAbsolutePath() + " failed for new key " + key);
+                            }
+                        } else if (log != null) {
+                            log.append("Bad hostname " + key + " from "
+                                   + sub.getLocation());
+                        }        
+                    } else if (!oldDest.toBase64().equals(entry.getValue()) && log != null) {
+                        log.append("Conflict for " + key + " from "
+                                   + sub.getLocation()
+                                   + ". Destination in remote address book is "
+                                   + entry.getValue());
+                    }
+                } catch (DataFormatException dfe) {
+                    if (log != null)
+                        log.append("Invalid b64 for" + key + " From: " + sub.getLocation());
+                }
+            }
+        }
+    }
+
     /**
      * Run an update, using the Map settings to provide the parameters.
      * 
@@ -83,26 +153,26 @@ public class Daemon {
      * @param home
      *            The directory containing addressbook's configuration files.
      */
-    public void update(Map settings, String home) {
-        File masterFile = new File(home, (String) settings
+    public static void update(Map<String, String> settings, String home) {
+        File masterFile = new File(home, settings
                 .get("master_addressbook"));
-        File routerFile = new File(home, (String) settings
+        File routerFile = new File(home, settings
                 .get("router_addressbook"));
         File published = null;
         if ("true".equals(settings.get("should_publish"))) 
-            published = new File(home, (String) settings
+            published = new File(home, settings
                 .get("published_addressbook"));
-        File subscriptionFile = new File(home, (String) settings
+        File subscriptionFile = new File(home, settings
                 .get("subscriptions"));
-        File logFile = new File(home, (String) settings.get("log"));
-        File etagsFile = new File(home, (String) settings.get("etags"));
-        File lastModifiedFile = new File(home, (String) settings
+        File logFile = new File(home, settings.get("log"));
+        File etagsFile = new File(home, settings.get("etags"));
+        File lastModifiedFile = new File(home, settings
                 .get("last_modified"));
-        File lastFetchedFile = new File(home, (String) settings
+        File lastFetchedFile = new File(home, settings
                 .get("last_fetched"));
         long delay;
         try {
-            delay = Long.parseLong((String) settings.get("update_delay"));
+            delay = Long.parseLong(settings.get("update_delay"));
         } catch (NumberFormatException nfe) {
             delay = 12;
         }
@@ -111,16 +181,44 @@ public class Daemon {
         AddressBook master = new AddressBook(masterFile);
         AddressBook router = new AddressBook(routerFile);
         
-        List defaultSubs = new LinkedList();
+        List<String> defaultSubs = new LinkedList();
         // defaultSubs.add("http://i2p/NF2RLVUxVulR3IqK0sGJR0dHQcGXAzwa6rEO4WAWYXOHw-DoZhKnlbf1nzHXwMEJoex5nFTyiNMqxJMWlY54cvU~UenZdkyQQeUSBZXyuSweflUXFqKN-y8xIoK2w9Ylq1k8IcrAFDsITyOzjUKoOPfVq34rKNDo7fYyis4kT5bAHy~2N1EVMs34pi2RFabATIOBk38Qhab57Umpa6yEoE~rbyR~suDRvD7gjBvBiIKFqhFueXsR2uSrPB-yzwAGofTXuklofK3DdKspciclTVzqbDjsk5UXfu2nTrC1agkhLyqlOfjhyqC~t1IXm-Vs2o7911k7KKLGjB4lmH508YJ7G9fLAUyjuB-wwwhejoWqvg7oWvqo4oIok8LG6ECR71C3dzCvIjY2QcrhoaazA9G4zcGMm6NKND-H4XY6tUWhpB~5GefB3YczOqMbHq4wi0O9MzBFrOJEOs3X4hwboKWANf7DT5PZKJZ5KorQPsYRSq0E3wSOsFCSsdVCKUGsAAAA/i2p/hosts.txt");
         defaultSubs.add("http://www.i2p2.i2p/hosts.txt");
         
         SubscriptionList subscriptions = new SubscriptionList(subscriptionFile,
-                etagsFile, lastModifiedFile, lastFetchedFile, delay, defaultSubs, (String) settings
-                .get("proxy_host"), Integer.parseInt((String) settings.get("proxy_port")));
+                etagsFile, lastModifiedFile, lastFetchedFile, delay, defaultSubs, settings
+                .get("proxy_host"), Integer.parseInt(settings.get("proxy_port")));
         Log log = new Log(logFile);
 
-        update(master, router, published, subscriptions, log);
+        if (true)
+            update(getNamingService(), published, subscriptions, log);
+        else
+            update(master, router, published, subscriptions, log);
+    }
+
+    /** depth-first search */
+    private static NamingService searchNamingService(NamingService ns, String srch)
+    {
+        String name = ns.getName();
+        if (name == srch)
+                return ns;
+        List<NamingService> list = ns.getNamingServices();
+        if (list != null) {
+            for (NamingService nss : list) {
+                NamingService rv = searchNamingService(nss, srch);
+                if (rv != null)
+                    return rv;
+            }
+        }
+        return null;                
+    }
+
+    /** @return the NamingService for the current file name, or the root NamingService */
+    private static NamingService getNamingService()
+    {
+        NamingService root = I2PAppContext.getGlobalContext().namingService();
+        NamingService rv = searchNamingService(root, "hosts.txt");
+        return rv != null ? rv : root;                
     }
 
     /**
@@ -149,7 +247,7 @@ public class Daemon {
             homeFile = new SecureDirectory(System.getProperty("user.dir"));
         }
         
-        Map defaultSettings = new HashMap();
+        Map<String, String> defaultSettings = new HashMap();
         defaultSettings.put("proxy_host", "127.0.0.1");
         defaultSettings.put("proxy_port", "4444");
         defaultSettings.put("master_addressbook", "../userhosts.txt");
@@ -173,7 +271,7 @@ public class Daemon {
         
         File settingsFile = new File(homeFile, settingsLocation);
         
-        Map settings = ConfigParser.parse(settingsFile, defaultSettings);
+        Map<String, String> settings = ConfigParser.parse(settingsFile, defaultSettings);
         // wait
         try {
             Thread.sleep(5*60*1000 + I2PAppContext.getGlobalContext().random().nextLong(5*60*1000));
@@ -181,7 +279,7 @@ public class Daemon {
         } catch (InterruptedException ie) {}
         
         while (_running) {
-            long delay = Long.parseLong((String) settings.get("update_delay"));
+            long delay = Long.parseLong(settings.get("update_delay"));
             if (delay < 1) {
                 delay = 1;
             }
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java b/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java
index b2ff2c5112a2ddeef4606c152af77f78af482299..44592f8ef57dd4d84d1bea3d438107268463cd2d 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java
@@ -21,13 +21,18 @@
 
 package net.i2p.addressbook;
 
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.NamingServiceUpdater;
+
 /**
  * A thread that waits five minutes, then runs the addressbook daemon.  
  * 
  * @author Ragnarok
  *
  */
-class DaemonThread extends Thread {
+class DaemonThread extends Thread implements NamingServiceUpdater {
 
     private String[] args;
 
@@ -49,11 +54,21 @@ class DaemonThread extends Thread {
         //    Thread.sleep(5 * 60 * 1000);
         //} catch (InterruptedException exp) {
         //}
+        I2PAppContext.getGlobalContext().namingService().registerUpdater(this);
         Daemon.main(this.args);
+        I2PAppContext.getGlobalContext().namingService().unregisterUpdater(this);
     }
 
     public void halt() {
         Daemon.stop();
         interrupt();
     }
+
+    /**
+     *  The NamingServiceUpdater interface
+     *  @since 0.8.6
+     */
+    public void update(Properties options) {
+        interrupt();
+    }
 }
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java
index 6a362b8475a67efbc33e444b7476c2928b65d65e..d4c0ee98c83ba4001753572c78ad4b7fc95cbef2 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java
@@ -35,9 +35,9 @@ import net.i2p.data.DataHelper;  // debug
  * 
  * @author Ragnarok
  */
-class SubscriptionIterator implements Iterator {
+class SubscriptionIterator implements Iterator<AddressBook> {
 
-    private Iterator subIterator;
+    private Iterator<Subscription> subIterator;
     private String proxyHost;
     private int proxyPort;
     private final long delay;
@@ -51,7 +51,7 @@ class SubscriptionIterator implements Iterator {
      * @param proxyHost proxy hostname
      * @param proxyPort proxt port number
      */
-    public SubscriptionIterator(List subscriptions, long delay, String proxyHost, int proxyPort) {
+    public SubscriptionIterator(List<Subscription> subscriptions, long delay, String proxyHost, int proxyPort) {
         this.subIterator = subscriptions.iterator();
         this.delay = delay;
         this.proxyHost = proxyHost;
@@ -72,8 +72,8 @@ class SubscriptionIterator implements Iterator {
      * see java.util.Iterator#next()
      * @return an AddressBook (empty if the minimum delay has not been met)
      */
-    public Object next() {
-        Subscription sub = (Subscription) this.subIterator.next();
+    public AddressBook next() {
+        Subscription sub = this.subIterator.next();
         if (sub.getLastFetched() + this.delay < I2PAppContext.getGlobalContext().clock().now()) {
             //System.err.println("Fetching addressbook from " + sub.getLocation());
             return new AddressBook(sub, this.proxyHost, this.proxyPort);
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java
index d67cd9af53d4eb3b8bb2d062874e89b754d09ff9..9b64bb31666c3ca7c011ea51ba1090a76df999cd 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java
@@ -24,7 +24,6 @@ package net.i2p.addressbook;
 import java.io.File;
 import java.io.IOException;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -37,7 +36,7 @@ import java.util.Map;
  */
 class SubscriptionList {
 
-    private List subscriptions;
+    private List<Subscription> subscriptions;
 
     private File etagsFile;
 
@@ -68,7 +67,7 @@ class SubscriptionList {
      * @param proxyPort proxy port number
      */
     public SubscriptionList(File locationsFile, File etagsFile,
-            File lastModifiedFile, File lastFetchedFile, long delay, List defaultSubs, String proxyHost, 
+            File lastModifiedFile, File lastFetchedFile, long delay, List<String> defaultSubs, String proxyHost, 
             int proxyPort) {
         this.subscriptions = new LinkedList();
         this.etagsFile = etagsFile;
@@ -77,11 +76,10 @@ class SubscriptionList {
         this.delay = delay;
         this.proxyHost = proxyHost;
         this.proxyPort = proxyPort;
-        Map etags;
-        Map lastModified;
-        Map lastFetched;
-        String location;
-        List locations = ConfigParser.parseSubscriptions(locationsFile, 
+        Map<String, String> etags;
+        Map<String, String> lastModified;
+        Map<String, String> lastFetched;
+        List<String> locations = ConfigParser.parseSubscriptions(locationsFile, 
                 defaultSubs);
         try {
             etags = ConfigParser.parse(etagsFile);
@@ -98,12 +96,10 @@ class SubscriptionList {
         } catch (IOException exp) {
             lastFetched = new HashMap();
         }
-        Iterator iter = locations.iterator();
-        while (iter.hasNext()) {
-            location = (String) iter.next();
-            this.subscriptions.add(new Subscription(location, (String) etags.get(location),
-                                   (String) lastModified.get(location),
-                                   (String) lastFetched.get(location)));
+        for (String location : locations) {
+            this.subscriptions.add(new Subscription(location, etags.get(location),
+                                   lastModified.get(location),
+                                   lastFetched.get(location)));
         }
     }
     
@@ -125,13 +121,10 @@ class SubscriptionList {
      * won't be read back correctly; the '=' should be escaped.
      */
     public void write() {
-        Iterator iter = this.subscriptions.iterator();
-        Subscription sub;
-        Map etags = new HashMap();
-        Map lastModified = new HashMap();
-        Map lastFetched = new HashMap();
-        while (iter.hasNext()) {
-            sub = (Subscription) iter.next();
+        Map<String, String> etags = new HashMap();
+        Map<String, String>  lastModified = new HashMap();
+        Map<String, String>  lastFetched = new HashMap();
+        for (Subscription sub : this.subscriptions) {
             if (sub.getEtag() != null) {
                 etags.put(sub.getLocation(), sub.getEtag());
             }
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java
index 5d91d469ccb4b44b88bb731bd0f13a6b2bd48fd8..8e611a112999b458d08abd080abfcf1011ff12c6 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java
@@ -24,6 +24,10 @@
 
 package i2p.susi.dns;
 
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
+import net.i2p.data.Base64;
+
 public class AddressBean
 {
 	private String name, destination;
@@ -58,4 +62,14 @@ public class AddressBean
 	{
 		this.name = name;
 	}
+
+	/** @since 0.8.6 */
+	public String getB32() 
+	{
+		byte[] dest = Base64.decode(destination);
+		if (dest == null)
+			return "";
+		byte[] hash = I2PAppContext.getGlobalContext().sha().calculateHash(dest).getData();
+		return Base32.encode(hash) + ".b32.i2p";
+	}
 }
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java
index 1c503a999965de268bb9a3fc85984fc196df5002..6421f23a93eb6259cf102ebb48cd6a2dbe515b7b 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java
@@ -26,13 +26,10 @@ package i2p.susi.dns;
 
 import java.util.Comparator;
 
-public 	class AddressByNameSorter implements Comparator
+public 	class AddressByNameSorter implements Comparator<AddressBean>
 {
-	public int compare(Object arg0, Object arg1)
+	public int compare(AddressBean a, AddressBean b)
 	{
-		AddressBean a = (AddressBean)arg0;
-		AddressBean b = (AddressBean)arg1;
-		
 		if( a == null )
 			return 1;
 		
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
index b6af8a12dada88fa0d30c0f359f158379d1fc681..f5901b505e697dc8f411f00a039ce267b901e243 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
@@ -42,12 +42,13 @@ import net.i2p.util.SecureFileOutputStream;
 
 public class AddressbookBean
 {
-	private String book, action, serial, lastSerial, filter, search, hostname, destination;
-	private int beginIndex, endIndex;
-	private Properties properties, addressbook;
+	protected String book, action, serial, lastSerial, filter, search, hostname, destination;
+	protected int beginIndex, endIndex;
+	protected final Properties properties;
+	private Properties addressbook;
 	private int trClass;
-	private LinkedList deletionMarks;
-	private static Comparator sorter;
+	protected final LinkedList<String> deletionMarks;
+	protected static final Comparator<AddressBean> sorter;
 	private static final int DISPLAY_SIZE=100;
 
 	static {
@@ -78,6 +79,7 @@ public class AddressbookBean
 	{
 		return addressbook != null && !addressbook.isEmpty();
 	}
+
 	public AddressbookBean()
 	{
 		properties = new Properties();
@@ -85,10 +87,12 @@ public class AddressbookBean
 		beginIndex = 0;
 		endIndex = DISPLAY_SIZE - 1;
 	}
+
 	private long configLastLoaded = 0;
 	private static final String PRIVATE_BOOK = "private_addressbook";
 	private static final String DEFAULT_PRIVATE_BOOK = "../privatehosts.txt";
-	private void loadConfig()
+
+	protected void loadConfig()
 	{
 		long currentTime = System.currentTimeMillis();
 		
@@ -112,6 +116,7 @@ public class AddressbookBean
 				try { fis.close(); } catch (IOException ioe) {}
 		}	
 	}
+
 	public String getFileName()
 	{
 		loadConfig();
@@ -123,11 +128,14 @@ public class AddressbookBean
 		} catch (IOException ioe) {}
 		return filename;
 	}
-	private Object[] entries;
-	public Object[] getEntries()
+
+	protected AddressBean[] entries;
+
+	public AddressBean[] getEntries()
 	{
 		return entries;
 	}
+
 	public String getAction() {
 		return action;
 	}
@@ -140,7 +148,7 @@ public class AddressbookBean
 				book.compareToIgnoreCase( "router" ) != 0 &&
 				book.compareToIgnoreCase( "private" ) != 0 &&
 				book.compareToIgnoreCase( "published" ) != 0  ))
-			book = "master";
+			book = "router";
 		
 		return book;
 	}
@@ -167,7 +175,7 @@ public class AddressbookBean
 		try {
 			fis =  new FileInputStream( getFileName() );
 			addressbook.load( fis );
-			LinkedList list = new LinkedList();
+			LinkedList<AddressBean> list = new LinkedList();
 			Enumeration e = addressbook.keys();
 			while( e.hasMoreElements() ) {
 				String name = (String)e.nextElement();
@@ -189,52 +197,11 @@ public class AddressbookBean
 				}
 				list.addLast( new AddressBean( name, destination ) );
 			}
-			Object array[] = list.toArray();
+			AddressBean array[] = list.toArray(new AddressBean[list.size()]);
 			Arrays.sort( array, sorter );
 			entries = array;
 
-			// Format a message about filtered addressbook size, and the number of displayed entries
-			// addressbook.jsp catches the case where the whole book is empty.
-			String filterArg = "";
-			if( search != null && search.length() > 0 ) {
-				message = _("Search") + ' ';
-			}
-			if( filter != null && filter.length() > 0 ) {
-				if( search != null && search.length() > 0 )
-					message = _("Search within filtered list") + ' ';
-				else
-					message = _("Filtered list") + ' ';
-				filterArg = "&filter=" + filter;
-			}
-			if (entries.length == 0) {
-				message += "- " + _("no matches") + '.';
-			} else if (getBeginInt() == 0 && getEndInt() == entries.length - 1) {
-				if (message.length() == 0)
-					message = _("Addressbook") + ' ';
-				if (entries.length <= 0)
-					message += _("contains no entries");
-				else if (entries.length == 1)
-					message += _("contains 1 entry");
-				else
-					message += _("contains {0} entries", entries.length);
-				message += '.';
-			} else {
-				if (getBeginInt() > 0) {
-					int newBegin = Math.max(0, getBeginInt() - DISPLAY_SIZE);
-					int newEnd = Math.max(0, getBeginInt() - 1);
-			       		message += "<a href=\"addressbook.jsp?book=" + getBook() + filterArg +
-					           "&begin=" + newBegin + "&end=" + newEnd + "\">" + newBegin +
-					           '-' + newEnd + "</a> | ";
-		       		}
-				message += _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), entries.length);
-				if (getEndInt() < entries.length - 1) {
-					int newBegin = Math.min(entries.length - 1, getEndInt() + 1);
-					int newEnd = Math.min(entries.length, getEndInt() + DISPLAY_SIZE);
-			       		message += " | <a href=\"addressbook.jsp?book=" + getBook() + filterArg +
-					           "&begin=" + newBegin + "&end=" + newEnd + "\">" + newBegin +
-					           '-' + newEnd + "</a>";
-				}
-			}
+			message = generateLoadMessage();
 		}
 		catch (Exception e) {
 			Debug.debug( e.getClass().getName() + ": " + e.getMessage() );
@@ -246,6 +213,63 @@ public class AddressbookBean
 			message = "<p>" + message + "</p>";
 		return message;
 	}
+
+	/**
+	 *  Format a message about filtered addressbook size, and the number of displayed entries
+	 *  addressbook.jsp catches the case where the whole book is empty.
+	 */
+	protected String generateLoadMessage() {
+		String message;
+		String filterArg = "";
+		int resultCount = resultSize();
+		if( filter != null && filter.length() > 0 ) {
+			if( search != null && search.length() > 0 )
+				message = ngettext("One result for search within filtered list.",
+				                   "{0} results for search within filtered list.",
+				                   resultCount);
+			else
+				message = ngettext("Filtered list contains 1 entry.",
+				                   "Fltered list contains {0} entries.",
+				                   resultCount);
+			filterArg = "&amp;filter=" + filter;
+		} else if( search != null && search.length() > 0 ) {
+			message = ngettext("One result for search.",
+			                   "{0} results for search.",
+			                   resultCount);
+		} else {
+			if (resultCount <= 0)
+				// covered in jsp
+				//message = _("This addressbook is empty.");
+				message = "";
+			else
+				message = ngettext("Addressbook contains 1 entry.",
+				                   "Addressbook contains {0} entries.",
+				                   resultCount);
+		}
+		if (resultCount <= 0) {
+			// nothing to display
+		} else if (getBeginInt() == 0 && getEndInt() == resultCount - 1) {
+			// nothing to display
+		} else {
+			if (getBeginInt() > 0) {
+				int newBegin = Math.max(0, getBeginInt() - DISPLAY_SIZE);
+				int newEnd = Math.max(0, getBeginInt() - 1);
+		       		message += " <a href=\"addressbook.jsp?book=" + getBook() + filterArg +
+				           "&amp;begin=" + newBegin + "&amp;end=" + newEnd + "\">" + newBegin +
+				           '-' + newEnd + "</a> | ";
+	       		}
+			message += ' ' + _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), Integer.valueOf(resultCount));
+			if (getEndInt() < resultCount - 1) {
+				int newBegin = Math.min(resultCount - 1, getEndInt() + 1);
+				int newEnd = Math.min(resultCount, getEndInt() + DISPLAY_SIZE);
+		       		message += " | <a href=\"addressbook.jsp?book=" + getBook() + filterArg +
+				           "&amp;begin=" + newBegin + "&amp;end=" + newEnd + "\">" + newBegin +
+				           '-' + newEnd + "</a>";
+			}
+		}
+		return message;
+	}
+
 	/** Perform actions, returning messages about this. */
 	public String getMessages()
 	{
@@ -255,8 +279,6 @@ public class AddressbookBean
 		if( action != null ) {
 			if( lastSerial != null && serial != null && serial.compareTo( lastSerial ) == 0 ) {
 				boolean changed = false;
-				int deleted = 0;
-				String name = null;
 				if (action.equals(_("Add")) || action.equals(_("Replace"))) {
 					if( addressbook != null && hostname != null && destination != null ) {
 						String oldDest = (String) addressbook.get(hostname);
@@ -291,18 +313,21 @@ public class AddressbookBean
 					// clear search when adding
 					search = null;
 				} else if (action.equals(_("Delete Selected"))) {
-					Iterator it = deletionMarks.iterator();
-					while( it.hasNext() ) {
-						name = (String)it.next();
-						addressbook.remove( name );
-						changed = true;
-						deleted++;
+					String name = null;
+					int deleted = 0;
+					for (String n : deletionMarks) {
+						addressbook.remove(n);
+						if (deleted++ == 0) {
+							changed = true;
+							name = n;
+						}
 					}
 					if( changed ) {
 						if (deleted == 1)
 							message = _("Destination {0} deleted.", name);
 						else
-							message = _("{0} destinations deleted.", deleted);
+							// parameter will always be >= 2
+							message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted);
 					}
 				}
 				if( changed ) {
@@ -337,6 +362,7 @@ public class AddressbookBean
 			fos.close();
 		} catch (IOException ioe) {}
 	}
+
 	public String getFilter() {
 		return filter;
 	}
@@ -382,41 +408,93 @@ public class AddressbookBean
 	public void setHostname(String hostname) {
 		this.hostname = DataHelper.stripHTML(hostname).trim();  // XSS
 	}
-	private int getBeginInt() {
-		return Math.max(0, Math.min(entries.length - 1, beginIndex));
+
+	protected int getBeginInt() {
+		return Math.max(0, Math.min(resultSize() - 1, beginIndex));
 	}
+
 	public String getBegin() {
 		return "" + getBeginInt();
 	}
+
+	/**
+	 *  @return beginning index into results
+	 *  @since 0.8.6
+	 */
+	public String getResultBegin() {
+		return isPrefiltered() ? "0" : Integer.toString(getBeginInt());
+	}
+
 	public void setBegin(String s) {
 		try {
 			beginIndex = Integer.parseInt(s);
 		} catch (NumberFormatException nfe) {}
 	}
-	private int getEndInt() {
-		return Math.max(0, Math.max(getBeginInt(), Math.min(entries.length - 1, endIndex)));
+
+	protected int getEndInt() {
+		return Math.max(0, Math.max(getBeginInt(), Math.min(resultSize() - 1, endIndex)));
 	}
+
 	public String getEnd() {
 		return "" + getEndInt();
 	}
+
+	/**
+	 *  @return ending index into results
+	 *  @since 0.8.6
+	 */
+	public String getResultEnd() {
+		return Integer.toString(isPrefiltered() ? resultSize() - 1 : getEndInt());
+	}
+
 	public void setEnd(String s) {
 		try {
 			endIndex = Integer.parseInt(s);
 		} catch (NumberFormatException nfe) {}
 	}
 
+	/**
+	 *  Does the entries map contain only the lookup result,
+	 *  or must we index into it?
+	 *  @since 0.8.6
+	 */
+	protected boolean isPrefiltered() {
+		return false;
+	}
+
+	/**
+	 *  @return the size of the lookup result
+	 *  @since 0.8.6
+	 */
+	protected int resultSize() {
+		return entries.length;
+	}
+
+	/**
+	 *  @return the total size of the address book
+	 *  @since 0.8.6
+	 */
+	protected int totalSize() {
+		return entries.length;
+	}
+
 	/** translate */
-	private static String _(String s) {
+	protected static String _(String s) {
 		return Messages.getString(s);
 	}
 
 	/** translate */
-	private static String _(String s, Object o) {
+	protected static String _(String s, Object o) {
 		return Messages.getString(s, o);
 	}
 
 	/** translate */
-	private static String _(String s, Object o, Object o2) {
+	protected static String _(String s, Object o, Object o2) {
 		return Messages.getString(s, o, o2);
 	}
+
+	/** translate (ngettext) @since 0.8.6 */
+	protected static String ngettext(String s, String p, int n) {
+		return Messages.getString(n, s, p);
+	}
 }
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/Messages.java b/apps/susidns/src/java/src/i2p/susi/dns/Messages.java
index b596a3be9a3e7c28c25c6c34816846bfd9fe1ecd..7d35e80f02fde23afa47ce50d1140a80cfed4f05 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/Messages.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/Messages.java
@@ -31,4 +31,9 @@ public class Messages {
     public static String getString(String s, Object o, Object o2) {
         return Translate.getString(s, o, o2, I2PAppContext.getGlobalContext(), BUNDLE_NAME);
     }
+
+    /** translate (ngettext) @since 0.8.6 */
+    public static String getString(int n, String s, String p) {
+        return Translate.getString(n, s, p, I2PAppContext.getGlobalContext(), BUNDLE_NAME);
+    }
 }
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d5d2a12b89055881525aa391b7611fc1e179589
--- /dev/null
+++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
@@ -0,0 +1,283 @@
+/*
+ *  This file is part of susidns project, see http://susi.i2p/
+ *  
+ *  Copyright (C) 2005 <susi23@mail.i2p>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *  
+ * @since 0.8.6
+ */
+
+package i2p.susi.dns;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.NamingService;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+
+/**
+ *  Talk to the NamingService API instead of modifying the hosts.txt files directly,
+ *  except for the 'published' addressbook.
+ *
+ *  @since 0.8.6
+ */
+public class NamingServiceBean extends AddressbookBean
+{
+	private static final String DEFAULT_NS = "BlockfileNamingService";
+
+	private boolean isDirect() {
+		return getBook().equals("published");
+	}
+
+	@Override
+	protected boolean isPrefiltered() {
+		if (isDirect())
+			return super.isPrefiltered();
+		return (search == null || search.length() <= 0) &&
+		       (filter == null || filter.length() <= 0) &&
+		       getNamingService().getName().equals(DEFAULT_NS);
+	}
+
+	@Override
+	protected int resultSize() {
+		if (isDirect())
+			return super.resultSize();
+		return isPrefiltered() ? totalSize() : entries.length;
+	}
+
+	@Override
+	protected int totalSize() {
+		if (isDirect())
+			return super.totalSize();
+		// only blockfile needs the list property
+		Properties props = new Properties();
+		props.setProperty("list", getFileName());
+		return getNamingService().size(props);
+	}
+
+	@Override
+	public boolean isNotEmpty()
+	{
+		if (isDirect())
+			return super.isNotEmpty();
+		return totalSize() > 0;
+	}
+
+	@Override
+	public String getFileName()
+	{
+		if (isDirect())
+			return super.getFileName();
+		loadConfig();
+		String filename = properties.getProperty( getBook() + "_addressbook" );
+		return basename(filename);
+	}
+
+	/** depth-first search */
+	private static NamingService searchNamingService(NamingService ns, String srch)
+	{
+		String name = ns.getName();
+		if (name.equals(srch) || basename(name).equals(srch) || name.equals(DEFAULT_NS))
+			return ns;
+		List<NamingService> list = ns.getNamingServices();
+		if (list != null) {
+			for (NamingService nss : list) {
+				NamingService rv = searchNamingService(nss, srch);
+				if (rv != null)
+					return rv;
+			}
+		}
+		return null;		
+	}
+
+	private static String basename(String filename) {
+		int slash = filename.lastIndexOf('/');
+		if (slash >= 0)
+			filename = filename.substring(slash + 1);
+		return filename;
+	}
+
+	/** @return the NamingService for the current file name, or the root NamingService */
+	private NamingService getNamingService()
+	{
+		NamingService root = I2PAppContext.getGlobalContext().namingService();
+		NamingService rv = searchNamingService(root, getFileName());		
+		return rv != null ? rv : root;		
+	}
+
+	/**
+	 *  Load addressbook and apply filter, returning messages about this.
+	 *  To control memory, don't load the whole addressbook if we can help it...
+	 *  only load what is searched for.
+	 */
+	@Override
+	public String getLoadBookMessages()
+	{
+		if (isDirect())
+			return super.getLoadBookMessages();
+		NamingService service = getNamingService();
+		Debug.debug("Searching within " + service + " with filename=" + getFileName() + " and with filter=" + filter + " and with search=" + search);
+		String message = "";
+		try {
+			LinkedList<AddressBean> list = new LinkedList();
+			Map<String, Destination> results;
+			Properties searchProps = new Properties();
+			// only blockfile needs this
+			searchProps.setProperty("list", getFileName());
+			if (filter != null) {
+				String startsAt = filter.equals("0-9") ? "[0-9]" : filter;
+				searchProps.setProperty("startsWith", startsAt);
+			}
+			if (isPrefiltered()) {
+				// Only limit if we not searching or filtering, so we will
+				// know the total number of results
+				if (beginIndex > 0)
+					searchProps.setProperty("skip", Integer.toString(beginIndex));
+				int limit = 1 + endIndex - beginIndex;
+				if (limit > 0)
+					searchProps.setProperty("limit", Integer.toString(limit));
+			}
+			if (search != null && search.length() > 0)
+				searchProps.setProperty("search", search.toLowerCase());
+			results = service.getEntries(searchProps);
+
+			Debug.debug("Result count: " + results.size());
+			for (Map.Entry<String, Destination> entry : results.entrySet()) {
+				String name = entry.getKey();
+				if( filter != null && filter.length() > 0 ) {
+					if( filter.compareTo( "0-9" ) == 0 ) {
+						char first = name.charAt(0);
+						if( first < '0' || first > '9' )
+							continue;
+					}
+					else if( ! name.toLowerCase().startsWith( filter.toLowerCase() ) ) {
+						continue;
+					}
+				}
+				if( search != null && search.length() > 0 ) {
+					if( name.indexOf( search ) == -1 ) {
+						continue;
+					}
+				}
+				String destination = entry.getValue().toBase64();
+				list.addLast( new AddressBean( name, destination ) );
+			}
+			AddressBean array[] = list.toArray(new AddressBean[list.size()]);
+			Arrays.sort( array, sorter );
+			entries = array;
+
+			message = generateLoadMessage();
+		}
+		catch (Exception e) {
+			Debug.debug( e.getClass().getName() + ": " + e.getMessage() );
+		}
+		if( message.length() > 0 )
+			message = "<p>" + message + "</p>";
+		return message;
+	}
+
+	/** Perform actions, returning messages about this. */
+	@Override
+	public String getMessages()
+	{
+		if (isDirect())
+			return super.getMessages();
+		// Loading config and addressbook moved into getLoadBookMessages()
+		String message = "";
+		
+		if( action != null ) {
+			Properties nsOptions = new Properties();
+			// only blockfile needs this
+                        nsOptions.setProperty("list", getFileName());
+			if( lastSerial != null && serial != null && serial.compareTo( lastSerial ) == 0 ) {
+				boolean changed = false;
+				if (action.equals(_("Add")) || action.equals(_("Replace"))) {
+					if(hostname != null && destination != null) {
+						Destination oldDest = getNamingService().lookup(hostname, nsOptions, null);
+						if (oldDest != null && destination.equals(oldDest.toBase64())) {
+							message = _("Host name {0} is already in addressbook, unchanged.", hostname);
+						} else if (oldDest != null && !action.equals(_("Replace"))) {
+							message = _("Host name {0} is already in addressbook with a different destination. Click \"Replace\" to overwrite.", hostname);
+						} else {
+							try {
+								Destination dest = new Destination(destination);
+					                        nsOptions.setProperty("s", _("Manually added via SusiDNS"));
+								boolean success = getNamingService().put(hostname, dest, nsOptions);
+								if (success) {
+									changed = true;
+									if (oldDest == null)
+										message = _("Destination added for {0}.", hostname);
+									else
+										message = _("Destination changed for {0}.", hostname);
+									// clear form
+									hostname = null;
+									destination = null;
+								} else {
+									message = _("Failed to add Destination for {0} to naming service {1}", hostname, getNamingService()) + "<br>";
+								}
+							} catch (DataFormatException dfe) {
+								message = _("Invalid Base 64 destination.");
+							}
+						}
+					} else {
+						message = _("Please enter a host name and destination");
+					}
+					// clear search when adding
+					search = null;
+				} else if (action.equals(_("Delete Selected"))) {
+					String name = null;
+					int deleted = 0;
+					for (String n : deletionMarks) {
+						boolean success = getNamingService().remove(n, nsOptions);
+						if (!success) {
+							message += _("Failed to delete Destination for {0} from naming service {1}", name, getNamingService()) + "<br>";
+						} else if (deleted++ == 0) {
+							changed = true;
+							name = n;
+						}
+					}
+					if( changed ) {
+						if (deleted == 1)
+							message += _("Destination {0} deleted.", name);
+						else
+							// parameter will always be >= 2
+							message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted);
+					}
+				}
+				if( changed ) {
+					message += "<br>" + _("Addressbook saved.");
+				}
+			}			
+			else {
+				message = _("Invalid form submission, probably because you used the \"back\" or \"reload\" button on your browser. Please resubmit.");
+			}
+		}
+		
+		action = null;
+		
+		if( message.length() > 0 )
+			message = "<p class=\"messages\">" + message + "</p>";
+		return message;
+	}
+}
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
index 176561acaf16c6934ae7c3a879b46b3f91d2cc63..88a6552be38c9f484fa1d0a0a79fe6ebab49d6cb 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
@@ -33,6 +33,7 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Properties;
 
+import net.i2p.I2PAppContext;
 import net.i2p.util.SecureFileOutputStream;
 
 public class SubscriptionsBean
@@ -130,15 +131,19 @@ public class SubscriptionsBean
 				if (action.equals(_("Save"))) {
 					save();
 					String nonce = System.getProperty("addressbook.nonce");
+				/*******
 					if (nonce != null) {	
 						// Yes this is a hack.
 						// No it doesn't work on a text-mode browser.
 						// Fetching from the addressbook servlet
 						// with the correct parameters will kick off a
 						// config reload and fetch.
-						message = _("Subscriptions saved, updating addressbook from subscription sources now.") +
-						          "<img height=\"1\" width=\"1\" alt=\"\" " +
-						          "src=\"/addressbook/?wakeup=1&nonce=" + nonce + "\">";
+				*******/
+					if (content != null && content.length() > 2) {
+						message = _("Subscriptions saved, updating addressbook from subscription sources now.");
+						          // + "<img height=\"1\" width=\"1\" alt=\"\" " +
+						          // "src=\"/addressbook/?wakeup=1&nonce=" + nonce + "\">";
+						I2PAppContext.getGlobalContext().namingService().requestUpdate(null);
 					} else {
 						message = _("Subscriptions saved.");
 					}
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java b/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java
index ffc7010462fbec0065cf456a57d83e96d09159e4..0949ec31f6ecfa4b9ff61bd2d8af293c1dcaac2e 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java
@@ -26,8 +26,9 @@ package i2p.susi.dns;
 
 public class VersionBean {
 	
-	private static String version = "0.5";
-	private static String url = "http://susi.i2p/?i2paddresshelper=T2DU1KAz3meB0B53U8Y06-I7vHR7XmC0qXAJfLW6b-1L1FVKoySRZz4xazHAwyv2xtRpvKrv6ukLm1tThEW0zQWtZPtX8G6KkzMibD8t7IS~4yw-9VkBtUydyYfsX08AK3v~-egSW8HCXTdyIJVtrETJb337VDUHW-7D4L1JLbwSH4if2ooks6yFTrljK5aVMi-16dZOVvmoyJc3jBqSdK6kraO4gW5-vHTmbLwL498p9nug1KOg1DqgN2GeU5X1QlVrlpFb~IIfdP~O8NT7u-LAjW3jSJsMbLDHMSYTIhC7xmJIiBoi-qk8p6TLynAmvJ7HRvbx4N1EB-uJHyD16wsZkkHyEOfmXbj0ZqLyKEGb3thPwCz-M9v~c2Qt3WbwjXJAtHpjlHkdJ4Fg91cX2oak~JoapnPf6Syw8hko5syf6VVoCYLnrrYyM8oGl8mLclHkj~VCidQNqMSM74IhrHfK6HmRikqtZBexb5M6wfMTTqBvaHURdD21GOpFKYBUAAAA";
+	private static String version = "0.6";
+	//private static String url = "http://susi.i2p/?i2paddresshelper=T2DU1KAz3meB0B53U8Y06-I7vHR7XmC0qXAJfLW6b-1L1FVKoySRZz4xazHAwyv2xtRpvKrv6ukLm1tThEW0zQWtZPtX8G6KkzMibD8t7IS~4yw-9VkBtUydyYfsX08AK3v~-egSW8HCXTdyIJVtrETJb337VDUHW-7D4L1JLbwSH4if2ooks6yFTrljK5aVMi-16dZOVvmoyJc3jBqSdK6kraO4gW5-vHTmbLwL498p9nug1KOg1DqgN2GeU5X1QlVrlpFb~IIfdP~O8NT7u-LAjW3jSJsMbLDHMSYTIhC7xmJIiBoi-qk8p6TLynAmvJ7HRvbx4N1EB-uJHyD16wsZkkHyEOfmXbj0ZqLyKEGb3thPwCz-M9v~c2Qt3WbwjXJAtHpjlHkdJ4Fg91cX2oak~JoapnPf6Syw8hko5syf6VVoCYLnrrYyM8oGl8mLclHkj~VCidQNqMSM74IhrHfK6HmRikqtZBexb5M6wfMTTqBvaHURdD21GOpFKYBUAAAA";
+	private static String url = "http://susi.i2p/";
 	
 	public String getVersion() {
 		return version;
diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp
index 15c76dfc8496054c909c1ff2066b0a08de38583a..9c34fe74135e6aca77006c8cec7627ea49a2e909 100644
--- a/apps/susidns/src/jsp/addressbook.jsp
+++ b/apps/susidns/src/jsp/addressbook.jsp
@@ -32,7 +32,7 @@
 <%@ page contentType="text/html"%>
 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 <jsp:useBean id="version" class="i2p.susi.dns.VersionBean" scope="application" />
-<jsp:useBean id="book" class="i2p.susi.dns.AddressbookBean" scope="session" />
+<jsp:useBean id="book" class="i2p.susi.dns.NamingServiceBean" scope="session" />
 <jsp:useBean id="intl" class="i2p.susi.dns.Messages" scope="application" />
 <jsp:setProperty name="book" property="*" />
 <jsp:setProperty name="book" property="resetDeletionMarks" value="1"/>
@@ -55,10 +55,10 @@
 <div id="navi">
 <p>
 <%=intl._("addressbooks")%>
-<a href="addressbook.jsp?book=private&filter=none&begin=0&end=99"><%=intl._("private")%></a> |
-<a href="addressbook.jsp?book=master&filter=none&begin=0&end=99"><%=intl._("master")%></a> |
-<a href="addressbook.jsp?book=router&filter=none&begin=0&end=99"><%=intl._("router")%></a> |
-<a href="addressbook.jsp?book=published&filter=none&begin=0&end=99"><%=intl._("published")%></a> *
+<a href="addressbook.jsp?book=private&amp;filter=none&amp;begin=0&amp;end=99"><%=intl._("private")%></a> |
+<a href="addressbook.jsp?book=master&amp;filter=none&amp;begin=0&amp;end=99"><%=intl._("master")%></a> |
+<a href="addressbook.jsp?book=router&amp;filter=none&amp;begin=0&amp;end=99"><%=intl._("router")%></a> |
+<a href="addressbook.jsp?book=published&amp;filter=none&amp;begin=0&amp;end=99"><%=intl._("published")%></a> *
 <a href="subscriptions.jsp"><%=intl._("subscriptions")%></a> *
 <a href="config.jsp"><%=intl._("configuration")%></a> *
 <a href="index.jsp"><%=intl._("overview")%></a>
@@ -71,42 +71,42 @@
 
 <div id="messages">${book.messages}</div>
 
-<span>${book.loadBookMessages}</span>
+${book.loadBookMessages}
 
 <c:if test="${book.notEmpty}">
 <div id="filter">
 <p><%=intl._("Filter")%>:
-<a href="addressbook.jsp?filter=a&begin=0&end=99">a</a>
-<a href="addressbook.jsp?filter=b&begin=0&end=99">b</a>
-<a href="addressbook.jsp?filter=c&begin=0&end=99">c</a> 
-<a href="addressbook.jsp?filter=d&begin=0&end=99">d</a>
-<a href="addressbook.jsp?filter=e&begin=0&end=99">e</a>
-<a href="addressbook.jsp?filter=f&begin=0&end=99">f</a>
-<a href="addressbook.jsp?filter=g&begin=0&end=99">g</a>
-<a href="addressbook.jsp?filter=h&begin=0&end=99">h</a>
-<a href="addressbook.jsp?filter=i&begin=0&end=99">i</a>
-<a href="addressbook.jsp?filter=j&begin=0&end=99">j</a>
-<a href="addressbook.jsp?filter=k&begin=0&end=99">k</a>
-<a href="addressbook.jsp?filter=l&begin=0&end=99">l</a>
-<a href="addressbook.jsp?filter=m&begin=0&end=99">m</a>
-<a href="addressbook.jsp?filter=n&begin=0&end=99">n</a>
-<a href="addressbook.jsp?filter=o&begin=0&end=99">o</a>
-<a href="addressbook.jsp?filter=p&begin=0&end=99">p</a>
-<a href="addressbook.jsp?filter=q&begin=0&end=99">q</a>
-<a href="addressbook.jsp?filter=r&begin=0&end=99">r</a>
-<a href="addressbook.jsp?filter=s&begin=0&end=99">s</a>
-<a href="addressbook.jsp?filter=t&begin=0&end=99">t</a>
-<a href="addressbook.jsp?filter=u&begin=0&end=99">u</a>
-<a href="addressbook.jsp?filter=v&begin=0&end=99">v</a>
-<a href="addressbook.jsp?filter=w&begin=0&end=99">w</a>
-<a href="addressbook.jsp?filter=x&begin=0&end=99">x</a>
-<a href="addressbook.jsp?filter=y&begin=0&end=99">y</a>
-<a href="addressbook.jsp?filter=z&begin=0&end=99">z</a>
-<a href="addressbook.jsp?filter=0-9&begin=0&end=99">0-9</a>
-<a href="addressbook.jsp?filter=none&begin=0&end=99"><%=intl._("all")%></a></p>
+<a href="addressbook.jsp?filter=a&amp;begin=0&amp;end=99">a</a>
+<a href="addressbook.jsp?filter=b&amp;begin=0&amp;end=99">b</a>
+<a href="addressbook.jsp?filter=c&amp;begin=0&amp;end=99">c</a> 
+<a href="addressbook.jsp?filter=d&amp;begin=0&amp;end=99">d</a>
+<a href="addressbook.jsp?filter=e&amp;begin=0&amp;end=99">e</a>
+<a href="addressbook.jsp?filter=f&amp;begin=0&amp;end=99">f</a>
+<a href="addressbook.jsp?filter=g&amp;begin=0&amp;end=99">g</a>
+<a href="addressbook.jsp?filter=h&amp;begin=0&amp;end=99">h</a>
+<a href="addressbook.jsp?filter=i&amp;begin=0&amp;end=99">i</a>
+<a href="addressbook.jsp?filter=j&amp;begin=0&amp;end=99">j</a>
+<a href="addressbook.jsp?filter=k&amp;begin=0&amp;end=99">k</a>
+<a href="addressbook.jsp?filter=l&amp;begin=0&amp;end=99">l</a>
+<a href="addressbook.jsp?filter=m&amp;begin=0&amp;end=99">m</a>
+<a href="addressbook.jsp?filter=n&amp;begin=0&amp;end=99">n</a>
+<a href="addressbook.jsp?filter=o&amp;begin=0&amp;end=99">o</a>
+<a href="addressbook.jsp?filter=p&amp;begin=0&amp;end=99">p</a>
+<a href="addressbook.jsp?filter=q&amp;begin=0&amp;end=99">q</a>
+<a href="addressbook.jsp?filter=r&amp;begin=0&amp;end=99">r</a>
+<a href="addressbook.jsp?filter=s&amp;begin=0&amp;end=99">s</a>
+<a href="addressbook.jsp?filter=t&amp;begin=0&amp;end=99">t</a>
+<a href="addressbook.jsp?filter=u&amp;begin=0&amp;end=99">u</a>
+<a href="addressbook.jsp?filter=v&amp;begin=0&amp;end=99">v</a>
+<a href="addressbook.jsp?filter=w&amp;begin=0&amp;end=99">w</a>
+<a href="addressbook.jsp?filter=x&amp;begin=0&amp;end=99">x</a>
+<a href="addressbook.jsp?filter=y&amp;begin=0&amp;end=99">y</a>
+<a href="addressbook.jsp?filter=z&amp;begin=0&amp;end=99">z</a>
+<a href="addressbook.jsp?filter=0-9&amp;begin=0&amp;end=99">0-9</a>
+<a href="addressbook.jsp?filter=none&amp;begin=0&amp;end=99"><%=intl._("all")%></a></p>
 <c:if test="${book.hasFilter}">
 <p><%=intl._("Current filter")%>: ${book.filter}
-(<a href="addressbook.jsp?filter=none&begin=0&end=99"><%=intl._("clear filter")%></a>)</p>
+(<a href="addressbook.jsp?filter=none&amp;begin=0&amp;end=99"><%=intl._("clear filter")%></a>)</p>
 </c:if>
 </div>
 
@@ -141,16 +141,19 @@
 </c:if>
 
 <th><%=intl._("Name")%></th>
+<th><%=intl._("Links")%></th>
 <th><%=intl._("Destination")%></th>
 </tr>
 <!-- limit iterator, or "Form too large" may result on submit, and is a huge web page if we don't -->
-<c:forEach items="${book.entries}" var="addr" begin="${book.begin}" end="${book.end}">
+<c:forEach items="${book.entries}" var="addr" begin="${book.resultBegin}" end="${book.resultEnd}">
 <tr class="list${book.trClass}">
 <c:if test="${book.master || book.router || book.published || book.private}">
 <td class="checkbox"><input type="checkbox" name="checked" value="${addr.name}" title="<%=intl._("Mark for deletion")%>"></td>
 </c:if>
-<td class="names"><a href="http://${addr.name}/">${addr.name}</a> -
-<span class="addrhlpr">(<a href="http://${addr.name}/?i2paddresshelper=${addr.destination}"><%=intl._("address helper link")%></a>)</span>
+<td class="names"><a href="http://${addr.name}/">${addr.name}</a>
+</td><td class="names">
+<span class="addrhlpr">(<a href="http://${addr.b32}/">b32</a>)</span>
+<span class="addrhlpr">(<a href="http://${addr.name}/?i2paddresshelper=${addr.destination}"><%=intl._("helper")%></a>)</span>
 </td>
 <td class="destinations"><textarea rows="1" style="height: 3em;" cols="40" wrap="off" readonly="readonly" name="dest_${addr.name}" >${addr.destination}</textarea></td>
 </tr>
@@ -176,8 +179,8 @@
 </c:if>
 
 <div id="add">
-<p class="add">
 <h3><%=intl._("Add new destination")%>:</h3>
+<p class="add">
 <b><%=intl._("Hostname")%>:</b> <input type="text" name="hostname" value="${book.hostname}" size="20">
 <b><%=intl._("Destination")%>:</b> <textarea name="destination" rows="1" style="height: 3em;" cols="40" wrap="off" >${book.destination}</textarea><br/>
 </p><p>
diff --git a/build.xml b/build.xml
index 678abccf97eaf187351f6eadb6954653fa65faee..26e7f8bdd5b64991a97ae4d61089b30ae1bb4b47 100644
--- a/build.xml
+++ b/build.xml
@@ -258,7 +258,7 @@
             splitindex="true" 
             doctitle="I2P Javadocs for Release ${release.number} Build ${build.number}"
             windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}">
-            <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security" />
+            <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security:net.metanotion.*" />
             <group title="Streaming Library" packages="net.i2p.client.streaming" />
             <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:org.cybergarage.*:org.freenetproject" />
             <group title="Router Console" packages="net.i2p.router.web" />
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 5cfc37ac5d4370ebf054fda6bef635e220f49c6e..b54ca796998e5ba6f55de887e7935af9496326a7 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -7,7 +7,6 @@ import java.util.Random;
 import java.util.Set;
 
 import net.i2p.client.naming.NamingService;
-import net.i2p.client.naming.PetNameDB;
 import net.i2p.crypto.AESEngine;
 import net.i2p.crypto.CryptixAESEngine;
 import net.i2p.crypto.DSAEngine;
@@ -72,7 +71,6 @@ public class I2PAppContext {
     private StatManager _statManager;
     private SessionKeyManager _sessionKeyManager;
     private NamingService _namingService;
-    private PetNameDB _petnameDb;
     private ElGamalEngine _elGamalEngine;
     private ElGamalAESEngine _elGamalAESEngine;
     private AESEngine _AESEngine;
@@ -89,7 +87,6 @@ public class I2PAppContext {
     private volatile boolean _statManagerInitialized;
     private volatile boolean _sessionKeyManagerInitialized;
     private volatile boolean _namingServiceInitialized;
-    private volatile boolean _petnameDbInitialized;
     private volatile boolean _elGamalEngineInitialized;
     private volatile boolean _elGamalAESEngineInitialized;
     private volatile boolean _AESEngineInitialized;
@@ -177,7 +174,6 @@ public class I2PAppContext {
         _statManager = null;
         _sessionKeyManager = null;
         _namingService = null;
-        _petnameDb = null;
         _elGamalEngine = null;
         _elGamalAESEngine = null;
         _logManager = null;
@@ -580,23 +576,6 @@ public class I2PAppContext {
         }
     }
     
-    /** @deprecated unused */
-    public PetNameDB petnameDb() {
-        if (!_petnameDbInitialized)
-            initializePetnameDb();
-        return _petnameDb;
-    }
-
-    /** @deprecated unused */
-    private void initializePetnameDb() {
-        synchronized (this) {
-            if (_petnameDb == null) {
-                _petnameDb = new PetNameDB();
-            }
-            _petnameDbInitialized = true;
-        }
-    }
-    
     /**
      * This is the ElGamal engine used within this context.  While it doesn't
      * really have anything substantial that is context specific (the algorithm
diff --git a/core/java/src/net/i2p/client/naming/AddressDB.java b/core/java/src/net/i2p/client/naming/AddressDB.java
deleted file mode 100644
index c43ada589296afb6298bc30ff9c4607dad7374ba..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/AddressDB.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.i2p.client.naming;
-
-import java.lang.reflect.Constructor;
-import java.util.Collection;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Address;
-import net.i2p.util.Log;
-
-/**
- *  @deprecated unused
- */
-public abstract class AddressDB {
-    
-    private final static Log _log = new Log(NamingService.class);
-    protected I2PAppContext _context;
-    
-    /** what classname should be used as the address db impl? */
-    public static final String PROP_IMPL = "i2p.addressdb.impl";
-    private static final String DEFAULT_IMPL = "net.i2p.client.naming.FilesystemAddressDB";
-    
-    /** 
-     * The address db should only be constructed and accessed through the 
-     * application context.  This constructor should only be used by the 
-     * appropriate application context itself.
-     *
-     */
-    protected AddressDB(I2PAppContext context) {
-        _context = context;
-    }
-    
-    private AddressDB() { // nop
-    }
-    
-    /**
-     * Get an address db instance. This method ensures that there
-     * will be only one address db instance (singleton) as well as
-     * choose the implementation from the "i2p.addressdb.impl" system
-     * property.
-     */
-    public static final synchronized AddressDB createInstance(I2PAppContext context) {
-        AddressDB instance = null;
-        String impl = context.getProperty(PROP_IMPL, DEFAULT_IMPL);
-        try {
-            Class cls = Class.forName(impl);
-            Constructor con = cls.getConstructor(new Class[] { I2PAppContext.class });
-            instance = (AddressDB)con.newInstance(new Object[] { context });
-        } catch (Exception ex) {
-            _log.error("Cannot load address db implementation", ex);
-            instance = new DummyAddressDB(context); // fallback
-        }
-        return instance;
-    }
-    
-    public abstract Address get(String hostname);
-    public abstract Address put(Address address);
-    public abstract Address remove(String hostname);
-    public abstract Address remove(Address address);
-    public abstract boolean contains(Address address);
-    public abstract boolean contains(String hostname);
-    public abstract Collection hostnames();
-}
diff --git a/core/java/src/net/i2p/client/naming/AddressDBNamingService.java b/core/java/src/net/i2p/client/naming/AddressDBNamingService.java
deleted file mode 100644
index 928212c895ca5973735553ce600b986eb85fbad0..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/AddressDBNamingService.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package net.i2p.client.naming;
-
-import java.util.Iterator;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Address;
-import net.i2p.data.Destination;
-
-/**
- *  @deprecated unused
- */
-public class AddressDBNamingService extends NamingService {
-    
-    private AddressDB _addressdb;
-    
-    public AddressDBNamingService(I2PAppContext context) {
-        super(context);
-        _addressdb = AddressDB.createInstance(context);
-    }
-    
-    private AddressDBNamingService() {
-        super(null);
-    }
-    
-    @Override
-    public Destination lookup(String hostname) {
-        Address addr = _addressdb.get(hostname);
-        if (addr != null) {
-            return addr.getDestination();
-        } else {
-            // If we can't find hostname in the addressdb, assume it's a key.
-            return lookupBase64(hostname);
-        }
-    }
-    
-    @Override
-    public String reverseLookup(Destination dest) {
-        Iterator iter = _addressdb.hostnames().iterator();
-        while (iter.hasNext()) {
-            Address addr = _addressdb.get((String)iter.next());
-            if (addr != null && addr.getDestination().equals(dest)) {
-                return addr.getHostname();
-            }
-        }
-        return null;        
-    }
-}
diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..96372159563500a01c93e80bef8a8db260ff451d
--- /dev/null
+++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
@@ -0,0 +1,801 @@
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by mihi in 2004 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ */
+package net.i2p.client.naming;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
+
+import net.metanotion.io.Serializer;
+import net.metanotion.io.block.BlockFile;
+import net.metanotion.util.skiplist.SkipIterator;
+import net.metanotion.util.skiplist.SkipList;
+
+
+/**
+ * A naming service using the net.metanotion BlockFile database.
+ *
+ * This database contains the following skiplists:
+ * <pre>
+ *
+ * "%%__INFO__%%" is the master database skiplist, containing one entry:
+ *     "info": a Properties, serialized with DataHelper functions:
+ *             "version": "1"
+ *             "created": Java long time (ms)
+ *             "lists":   Comma-separated list of host databases, to be
+ *                        searched in-order for lookups
+ *
+ *
+ * For each host database, there is a skiplist containing
+ * the hosts for that database.
+ * The keys/values in these skiplists are as follows:
+ *      key: a UTF-8 String
+ *      value: a DestEntry, which is a Properties (serialized with DataHelper)
+ *             followed by a Destination (serialized as usual).
+ *
+ *
+ * The DestEntry Properties typically contains:
+ *             "a":   The time added (Java long time in ms)
+ *             "s":   The original source of the entry (typically a file name or subscription URL)
+ *             others TBD
+ *
+ * </pre>
+ *
+ * All host names are converted to lower case.
+ */
+public class BlockfileNamingService extends DummyNamingService {
+
+    private final BlockFile _bf;
+    private final RandomAccessFile _raf;
+    private final List<String> _lists;
+    private volatile boolean _isClosed;
+
+    private static final Serializer _infoSerializer = new PropertiesSerializer();
+    private static final Serializer _stringSerializer = new StringSerializer();
+    private static final Serializer _destSerializer = new DestEntrySerializer();
+
+    private static final String HOSTS_DB = "hostsdb.blockfile";
+    private static final String FALLBACK_LIST = "hosts.txt";
+
+    private static final String INFO_SKIPLIST = "%%__INFO__%%";
+    private static final String PROP_INFO = "info";
+    private static final String PROP_VERSION = "version";
+    private static final String PROP_LISTS = "lists";
+    private static final String PROP_CREATED = "created";
+    private static final String PROP_MODIFIED = "modified";
+    private static final String VERSION = "1";
+
+    private static final String PROP_ADDED = "a";
+    private static final String PROP_SOURCE = "s";
+    
+    /**
+     *  @throws RuntimeException on fatal error
+     */
+    public BlockfileNamingService(I2PAppContext context) {
+        super(context);
+        _lists = new ArrayList();
+        BlockFile bf = null;
+        RandomAccessFile raf = null;
+        File f = new File(_context.getRouterDir(), HOSTS_DB);
+        if (f.exists()) {
+            try {
+                // closing a BlockFile does not close the underlying file,
+                // so we must create and retain a RAF so we may close it later
+                raf = new RandomAccessFile(f, "rw");
+                bf = initExisting(raf);
+            } catch (IOException ioe) {
+                if (raf != null) {
+                    try { raf.close(); } catch (IOException e) {}
+                }
+                File corrupt = new File(_context.getRouterDir(), HOSTS_DB + ".corrupt");
+                _log.log(Log.CRIT, "Corrupt or unreadable database " + f + ", moving to " + corrupt +
+                                   " and creating new database", ioe);
+                boolean success = f.renameTo(corrupt);
+                if (!success)
+                    _log.log(Log.CRIT, "Failed to move corrupt database " + f + " to " + corrupt);
+            }
+        }
+        if (bf == null) {
+            try {
+                // closing a BlockFile does not close the underlying file,
+                // so we must create and retain a RAF so we may close it later
+                raf = new RandomAccessFile(f, "rw");
+                SecureFileOutputStream.setPerms(f);
+                bf = init(raf);
+            } catch (IOException ioe) {
+                if (raf != null) {
+                    try { raf.close(); } catch (IOException e) {}
+                }
+                _log.log(Log.CRIT, "Failed to initialize database", ioe);
+                throw new RuntimeException(ioe);
+            }
+        }
+        _bf = bf;
+        _raf = raf;
+        _context.addShutdownTask(new Shutdown());
+    }
+
+    /**
+     *  Create a new database and initialize it from the local files
+     *  privatehosts.txt, userhosts.txt, and hosts.txt,
+     *  creating a skiplist in the database for each.
+     */
+    private BlockFile init(RandomAccessFile f) throws IOException {
+        long start = _context.clock().now();
+        try {
+            BlockFile rv = new BlockFile(f, true);
+            SkipList hdr = rv.makeIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
+            Properties info = new Properties();
+            info.setProperty(PROP_VERSION, VERSION);
+            info.setProperty(PROP_CREATED, Long.toString(_context.clock().now()));
+            String list = _context.getProperty(HostsTxtNamingService.PROP_HOSTS_FILE,
+                                               HostsTxtNamingService.DEFAULT_HOSTS_FILE);
+            info.setProperty(PROP_LISTS, list);
+            hdr.put(PROP_INFO, info);
+
+            // TODO all in one skiplist or separate?
+            int total = 0;
+            for (String hostsfile : getFilenames(list)) {
+                File file = new File(_context.getRouterDir(), hostsfile);
+                if ((!file.exists()) || !(file.canRead()))
+                    continue;
+                int count = 0;
+                BufferedReader in = null;
+                try {
+                    in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"), 16*1024);
+                    String line = null;
+                    while ( (line = in.readLine()) != null) {
+                        if (line.startsWith("#"))
+                            continue;
+                        int split = line.indexOf('=');
+                        if (split <= 0)
+                            continue;
+                        String key = line.substring(0, split).toLowerCase();
+                        if (line.indexOf('#') > 0)  { // trim off any end of line comment
+                            line = line.substring(0, line.indexOf('#')).trim();
+                            if (line.length() < split + 1)
+                                continue;
+                        }
+                        String b64 = line.substring(split+1);   //.trim() ??????????????
+                        Destination d = lookupBase64(b64);
+                        if (d != null) {
+                            addEntry(rv, hostsfile, key, d, hostsfile);
+                            count++;
+                        }
+                    }
+                } catch (IOException ioe) {
+                    _log.error("Failed to read hosts from " + file, ioe);
+                } finally {
+                    if (in != null) try { in.close(); } catch (IOException ioe) {}
+                }
+                total += count;
+                _log.logAlways(Log.INFO, "Migrating " + count + " hosts from " + file + " to new hosts database");
+                _lists.add(hostsfile);
+            }
+            if (_log.shouldLog(Log.INFO))
+                _log.info("DB init took " + DataHelper.formatDuration(_context.clock().now() - start));
+            if (total <= 0)
+                _log.error("Warning - initialized database with zero entries");
+            return rv;
+        } catch (RuntimeException e) {
+            throw new IOException(e.toString());
+        }
+    }
+
+    /**
+     *  Read the info block of an existing database.
+     */
+    private BlockFile initExisting(RandomAccessFile raf) throws IOException {
+        long start = _context.clock().now();
+        try {
+            BlockFile bf = new BlockFile(raf, false);
+            // TODO all in one skiplist or separate?
+            SkipList hdr = bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
+            if (hdr == null)
+                throw new IOException("No db header");
+            Properties info = (Properties) hdr.get(PROP_INFO);
+            if (info == null)
+                throw new IOException("No header info");
+            String version = info.getProperty(PROP_VERSION);
+            if (!VERSION.equals(version))
+                throw new IOException("Bad db version: " + version);
+
+            String list = info.getProperty(PROP_LISTS);
+            if (list == null)
+                throw new IOException("No lists");
+            long createdOn = 0;
+            String created = info.getProperty(PROP_CREATED);
+            if (created != null) {
+                try {
+                    createdOn = Long.parseLong(created);
+                } catch (NumberFormatException nfe) {}
+            }
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Found database version " + version + " created " + (new Date(createdOn)).toString() +
+                          " containing lists: " + list);
+
+            List<String> skiplists = getFilenames(list);
+            if (skiplists.isEmpty())
+                skiplists.add(FALLBACK_LIST);
+            _lists.addAll(skiplists);
+            _log.error("DB init took " + DataHelper.formatDuration(_context.clock().now() - start));
+            return bf;
+        } catch (RuntimeException e) {
+            throw new IOException(e.toString());
+        }
+    }
+
+    /**
+     *  Caller must synchronize
+     *  @return entry or null, or throws ioe
+     */
+    private DestEntry getEntry(String listname, String key) throws IOException {
+        try {
+            SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
+            if (sl == null)
+                return null;
+            DestEntry rv = (DestEntry) sl.get(key);
+            // Control memory usage
+////////    _bf.closeIndex(listname);
+            return rv;
+        } catch (IOException ioe) {
+            _log.error("DB Lookup error", ioe);
+            // delete index??
+            throw ioe;
+        } catch (RuntimeException e) {
+            _log.error("DB Lookup error", e);
+            throw new IOException(e.toString());
+        }
+    }
+
+    /**
+     *  Caller must synchronize
+     *  @param source may be null
+     */
+    private void addEntry(BlockFile bf, String listname, String key, Destination dest, String source) throws IOException {
+        try {
+            // catch IOE and delete index??
+            SkipList sl = bf.getIndex(listname, _stringSerializer, _destSerializer);
+            if (sl == null) {
+                //_log.info("Making new skiplist " + listname);
+                sl = bf.makeIndex(listname, _stringSerializer, _destSerializer);
+            }
+            Properties props = new Properties();
+            props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
+            if (source != null)
+                props.setProperty(PROP_SOURCE, source);
+            addEntry(sl, key, dest, props);
+            // Control memory usage
+//////      bf.closeIndex(listname);
+        } catch (IOException ioe) {
+            _log.error("DB add error", ioe);
+            // delete index??
+            throw ioe;
+        } catch (RuntimeException e) {
+            _log.error("DB add error", e);
+            throw new IOException(e.toString());
+        }
+    }
+
+    /**
+     *  Caller must synchronize
+     *  @param source may be null
+     *  @throws RuntimeException
+     */
+    private void addEntry(SkipList sl, String key, Destination dest, String source) {
+        Properties props = new Properties();
+        props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
+        if (source != null)
+            props.setProperty(PROP_SOURCE, source);
+        addEntry(sl, key, dest, props);
+    }
+
+    /**
+     *  Caller must synchronize
+     *  @param props may be null
+     *  @throws RuntimeException
+     */
+    private static void addEntry(SkipList sl, String key, Destination dest, Properties props) {
+        DestEntry de = new DestEntry();
+        de.dest = dest;
+        de.props = props;
+        sl.put(key, de);
+    }
+
+    private static List<String> getFilenames(String list) {
+        StringTokenizer tok = new StringTokenizer(list, ",");
+        List<String> rv = new ArrayList(tok.countTokens());
+        while (tok.hasMoreTokens())
+            rv.add(tok.nextToken());
+        return rv;
+    }
+    
+    /**
+     *  Caller must synchronize
+     *  @return removed object or null
+     *  @throws RuntimeException
+     */
+    private static Object removeEntry(SkipList sl, String key) {
+        return sl.remove(key);
+    }
+
+    ////////// Start NamingService API
+
+    @Override
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        Destination d = super.lookup(hostname, null, null);
+        if (d != null)
+            return d;
+
+        String key = hostname.toLowerCase();
+        synchronized(_bf) {
+            if (_isClosed)
+                return null;
+            for (String list : _lists) { 
+                try {
+                    DestEntry de = getEntry(list, key);
+                    if (de != null) {
+                        d = de.dest;
+                        if (storedOptions != null)
+                            storedOptions.putAll(de.props);
+                        break;
+                    }
+                } catch (IOException ioe) {
+                    break;
+                }
+            }
+        }
+        if (d != null)
+            putCache(hostname, d);
+        return d;
+    }
+
+    /**
+     * @param options If non-null and contains the key "list", add to that list
+     *                (default "hosts.txt")
+     *                Use the key "s" for the source
+     */
+    @Override
+    public boolean put(String hostname, Destination d, Properties options) {
+        return put(hostname, d, options, false);
+    }
+
+    /**
+     * @param options If non-null and contains the key "list", add to that list
+     *                (default "hosts.txt")
+     *                Use the key "s" for the source
+     */
+    @Override
+    public boolean putIfAbsent(String hostname, Destination d, Properties options) {
+        return put(hostname, d, options, true);
+    }
+
+    private boolean put(String hostname, Destination d, Properties options, boolean checkExisting) {
+        String key = hostname.toLowerCase();
+        String listname = FALLBACK_LIST;
+        Properties props = new Properties();
+        if (options != null) {
+            props.putAll(options);
+            String list = options.getProperty("list");
+            if (list != null) {
+                listname = list;
+                props.remove("list");
+            }
+        }
+        props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
+        synchronized(_bf) {
+            if (_isClosed)
+                return false;
+            try {
+                SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
+                if (sl == null)
+                    sl = _bf.makeIndex(listname, _stringSerializer, _destSerializer);
+                boolean changed =  (checkExisting || !_listeners.isEmpty()) && sl.get(key) != null;
+                if (changed && checkExisting)
+                        return false;
+                addEntry(sl, key, d, props);
+                if (changed)
+                    removeCache(hostname);
+                for (NamingServiceListener nsl : _listeners) { 
+                    if (changed)
+                        nsl.entryChanged(this, hostname, d, options);
+                    else
+                        nsl.entryAdded(this, hostname, d, options);
+                }
+                return true;
+            } catch (IOException re) {
+                return false;
+            } catch (RuntimeException re) {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * @param options If non-null and contains the key "list", remove
+     *                from that list (default "hosts.txt", NOT all lists)
+     */
+    @Override
+    public boolean remove(String hostname, Properties options) {
+        String key = hostname.toLowerCase();
+        String listname = FALLBACK_LIST;
+        if (options != null) {
+            String list = options.getProperty("list");
+            if (list != null) {
+                listname = list;
+            }
+        }
+        synchronized(_bf) {
+            if (_isClosed)
+                return false;
+            try {
+                SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
+                if (sl == null)
+                    return false;
+                boolean rv = removeEntry(sl, key) != null;
+                if (rv) {
+                    removeCache(hostname);
+                    for (NamingServiceListener nsl : _listeners) { 
+                        nsl.entryRemoved(this, key);
+                    }
+                }
+                return rv;
+            } catch (IOException ioe) {
+                _log.error("DB remove error", ioe);
+                return false;
+            } catch (RuntimeException re) {
+                _log.error("DB remove error", re);
+                return false;
+            }
+        }
+    }
+
+    /**
+     * @param options If non-null and contains the key "list", get
+     *                from that list (default "hosts.txt", NOT all lists)
+     *                Key "skip": skip that many entries
+     *                Key "limit": max number to return
+     *                Key "search": return only those matching substring
+     *                Key "startsWith": return only those starting with
+     *                                  ("[0-9]" allowed)
+     *                Key "beginWith": start here in the iteration
+     *                Don't use both startsWith and beginWith.
+     *                Search, startsWith, and beginWith values must be lower case.
+     */
+    @Override
+    public Map<String, Destination> getEntries(Properties options) {
+        String listname = FALLBACK_LIST;
+        String search = null;
+        String startsWith = null;
+        String beginWith = null;
+        int limit = Integer.MAX_VALUE;
+        int skip = 0;
+        if (options != null) {
+            String ln = options.getProperty("list");
+            if (ln != null)
+                listname = ln;
+            search = options.getProperty("search");
+            startsWith = options.getProperty("startsWith");
+            beginWith = options.getProperty("beginWith");
+            if (beginWith == null && startsWith != null) {
+                if (startsWith.equals("[0-9]"))
+                    beginWith = "0";
+                else
+                    beginWith = startsWith;
+            }
+            String lim = options.getProperty("limit");
+            try {
+                limit = Integer.parseInt(lim);
+            } catch (NumberFormatException nfe) {}
+            String sk = options.getProperty("skip");
+            try {
+                skip = Integer.parseInt(sk);
+            } catch (NumberFormatException nfe) {}
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Searching " + listname + " beginning with " + beginWith +
+                       " starting with " + startsWith + " search string " + search +
+                       " limit=" + limit + " skip=" + skip);
+        synchronized(_bf) {
+            if (_isClosed)
+                return Collections.EMPTY_MAP;
+            try {
+                SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
+                if (sl == null) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("No skiplist found for lookup in " + listname);
+                    return Collections.EMPTY_MAP;
+                }
+                SkipIterator iter;
+                if (beginWith != null)
+                    iter = sl.find(beginWith);
+                else
+                    iter = sl.iterator();
+                Map<String, Destination> rv = new HashMap();
+                for (int i = 0; i < skip && iter.hasNext(); i++) {
+                    iter.next();
+                }
+                for (int i = 0; i < limit && iter.hasNext(); ) {
+                    String key = (String) iter.nextKey();
+                    if (startsWith != null) {
+                        if (startsWith.equals("[0-9]")) {
+                            if (key.charAt(0) > '9')
+                                break;
+                        } else if (!key.startsWith(startsWith)) {
+                            break;
+                        }
+                    }
+                    DestEntry de = (DestEntry) iter.next();
+                    if (search != null && key.indexOf(search) < 0)
+                        continue;
+                    rv.put(key, de.dest);
+                    i++;
+                }
+                return rv;
+            } catch (IOException ioe) {
+                _log.error("DB lookup error", ioe);
+                return Collections.EMPTY_MAP;
+            } catch (RuntimeException re) {
+                _log.error("DB lookup error", re);
+                return Collections.EMPTY_MAP;
+            }
+        }
+    }
+
+    /**
+     * @param options If non-null and contains the key "list", return the
+     *                size of that list (default "hosts.txt", NOT all lists)
+     */
+    @Override
+    public int size(Properties options) {
+        String listname = FALLBACK_LIST;
+        if (options != null) {
+            String list = options.getProperty("list");
+            if (list != null) {
+                listname = list;
+            }
+        }
+        synchronized(_bf) {
+            if (_isClosed)
+                return 0;
+            try {
+                SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
+                if (sl == null)
+                    return 0;
+                return sl.size();
+            } catch (IOException ioe) {
+                _log.error("DB size error", ioe);
+                return 0;
+            } catch (RuntimeException re) {
+                _log.error("DB size error", re);
+                return 0;
+            }
+        }
+    }
+
+    public void shutdown() {
+        close();
+    }
+
+    ////////// End NamingService API
+
+    private void dumpDB() {
+        synchronized(_bf) {
+            if (_isClosed)
+                _log.error("Database is closed");
+            for (String list : _lists) { 
+                try {
+                    SkipList sl = _bf.getIndex(list, _stringSerializer, _destSerializer);
+                    if (sl == null) {
+                        _log.error("No list found for " + list);
+                        continue;
+                    }
+                    int i = 0;
+                    for (SkipIterator iter = sl.iterator(); iter.hasNext(); ) {
+                         String key = (String) iter.nextKey();
+                         DestEntry de = (DestEntry) iter.next();
+                         _log.error("DB " + list + " key " + key + " val " + de);
+                         i++;
+                    }
+                    _log.error(i + " entries found for " + list);
+                } catch (IOException ioe) {
+                    _log.error("Fail", ioe);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void close() {
+        synchronized(_bf) {
+            try {
+                _bf.close();
+            } catch (IOException ioe) {
+            } catch (RuntimeException e) {
+            }
+            try {
+                _raf.close();
+            } catch (IOException ioe) {
+            }
+            _isClosed = true;
+        }
+    }
+
+    private class Shutdown implements Runnable {
+        public void run() {
+            close();
+        }
+    }
+
+    /**
+     *  UTF-8 Serializer (the one in the lib is US-ASCII).
+     *  Used for all keys.
+     */
+    private static class StringSerializer implements Serializer {
+        public byte[] getBytes(Object o) {
+            try {
+                return ((String) o).getBytes("UTF-8");
+            } catch (UnsupportedEncodingException uee) {
+                throw new RuntimeException("No UTF-8", uee);
+            }
+        }
+
+        public Object construct(byte[] b) {
+            try {
+                return new String(b, "UTF-8");
+            } catch (UnsupportedEncodingException uee) {
+                throw new RuntimeException("No UTF-8", uee);
+            }
+        }
+    }
+
+    /**
+     *  Used for the values in the header skiplist
+     */
+    private static class PropertiesSerializer implements Serializer {
+        public byte[] getBytes(Object o) {
+            Properties p = (Properties) o;
+            return DataHelper.toProperties(p);
+        }
+
+        public Object construct(byte[] b) {
+            Properties rv = new Properties();
+            try {
+                DataHelper.fromProperties(b, 0, rv);
+            } catch (IOException ioe) {
+                return null;
+            } catch (DataFormatException dfe) {
+                return null;
+            }
+            return rv;
+        }
+    }
+
+    /**
+     *  A DestEntry contains Properties and a Destination,
+     *  and is serialized in that order.
+     */
+    private static class DestEntry {
+        /** may be null */
+        public Properties props;
+        /** may not be null */
+        public Destination dest;
+
+        @Override
+        public String toString() {
+            return "DestEntry (" + DataHelper.toString(props) +
+                   ") " + dest.toString();
+        }
+    }
+
+    /**
+     *  Used for the values in the addressbook skiplists
+     */
+    private static class DestEntrySerializer implements Serializer {
+        public byte[] getBytes(Object o) {
+            DestEntry de = (DestEntry) o;
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
+            try {
+                DataHelper.writeProperties(baos, de.props);
+                de.dest.writeBytes(baos);
+            } catch (IOException ioe) {
+                return null;
+            } catch (DataFormatException dfe) {
+                return null;
+            }
+            return baos.toByteArray();
+        }
+
+        public Object construct(byte[] b) {
+            DestEntry rv = new DestEntry();
+            Destination dest = new Destination();
+            rv.dest = dest;
+            ByteArrayInputStream bais = new ByteArrayInputStream(b);
+            try {
+                rv.props = DataHelper.readProperties(bais);
+                dest.readBytes(bais);
+            } catch (IOException ioe) {
+                return null;
+            } catch (DataFormatException dfe) {
+                return null;
+            }
+            return rv;
+        }
+    }
+
+    public static void main(String[] args) {
+        BlockfileNamingService bns = new BlockfileNamingService(I2PAppContext.getGlobalContext());
+        //System.out.println("zzz.i2p: " + bns.lookup("zzz.i2p"));
+        List<String> names = null;
+        try {
+            Properties props = new Properties();
+            DataHelper.loadProps(props, new File("hosts.txt"), true);
+            names = new ArrayList(props.keySet());
+            Collections.shuffle(names);
+        } catch (IOException ioe) {
+            System.out.println("No hosts.txt to test with");
+            bns.close();
+            return;
+        }
+
+        System.out.println("size() reports " + bns.size());
+        System.out.println("getEntries() returns " + bns.getEntries().size());
+
+        System.out.println("Testing with " + names.size() + " hostnames");
+        int found = 0;
+        int notfound = 0;
+        long start = System.currentTimeMillis();
+        for (String name : names) {
+             Destination dest = bns.lookup(name);
+             if (dest != null)
+                 found++;
+             else
+                 notfound++;
+        }
+        System.out.println("BFNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
+        System.out.println("found " + found + " notfound " + notfound);
+        bns.dumpDB();
+        bns.close();
+
+        HostsTxtNamingService htns = new HostsTxtNamingService(I2PAppContext.getGlobalContext());
+        found = 0;
+        notfound = 0;
+        start = System.currentTimeMillis();
+        for (String name : names) {
+             Destination dest = htns.lookup(name);
+             if (dest != null)
+                 found++;
+             else
+                 notfound++;
+        }
+        System.out.println("HTNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
+        System.out.println("found " + found + " notfound " + notfound);
+    }
+}
diff --git a/core/java/src/net/i2p/client/naming/DummyAddressDB.java b/core/java/src/net/i2p/client/naming/DummyAddressDB.java
deleted file mode 100644
index e18a2b7adda9e8ea2bc02393bd225461fe26db9c..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/DummyAddressDB.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package net.i2p.client.naming;
-
-import java.util.Collection;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Address;
-
-/**
- *  @deprecated unused
- */
-public class DummyAddressDB extends AddressDB {
-
-    public DummyAddressDB(I2PAppContext context) {
-        super(context);
-    }
-    
-    @Override
-    public Address get(String hostname) {
-        return null;
-    }
-    
-    @Override
-    public Address put(Address address) {
-        return null;
-    }
-    
-    @Override
-    public Address remove(String hostname) {
-        return null;
-    }
-    
-    @Override
-    public Address remove(Address address) {
-        return null;
-    }
-    
-    @Override
-    public boolean contains(Address address) {
-        return false;
-    }
-    
-    @Override
-    public boolean contains(String hostname) {
-        return false;
-    }
-    
-    @Override
-    public Collection hostnames() {
-        return null;
-    }
-
-}
diff --git a/core/java/src/net/i2p/client/naming/DummyNamingService.java b/core/java/src/net/i2p/client/naming/DummyNamingService.java
index ff0855c279f5515f6a5281c73f7d1d30ff80f1c2..ea79cd04cfa4c389d2bb7e91ee045d2aa8234e4c 100644
--- a/core/java/src/net/i2p/client/naming/DummyNamingService.java
+++ b/core/java/src/net/i2p/client/naming/DummyNamingService.java
@@ -7,24 +7,110 @@
  */
 package net.i2p.client.naming;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
 import net.i2p.I2PAppContext;
 import net.i2p.data.Destination;
 
 /**
- * A Dummy naming service that can only handle base64 destinations.
+ * A Dummy naming service that can only handle base64 and b32 destinations.
  */
 class DummyNamingService extends NamingService {
+
+    private static final int BASE32_HASH_LENGTH = 52;   // 1 + Hash.HASH_LENGTH * 8 / 5
+    public final static String PROP_B32 = "i2p.naming.hostsTxt.useB32";
+    protected static final int CACHE_MAX_SIZE = 32;
+    public static final int DEST_SIZE = 516;                    // Std. Base64 length (no certificate)
+
+    /**
+     *  The LRU cache, with no expiration time.
+     *  Classes should take care to call removeCache() for any entries that
+     *  are invalidated.
+     */
+    private static final Map<String, Destination> _cache = new LHM(CACHE_MAX_SIZE);
+
     /** 
      * The naming service should only be constructed and accessed through the 
      * application context.  This constructor should only be used by the 
      * appropriate application context itself.
      *
      */
-    protected DummyNamingService(I2PAppContext context) { super(context); }
-    private DummyNamingService() { super(null); }
+    protected DummyNamingService(I2PAppContext context) {
+        super(context);
+    }
     
     @Override
-    public Destination lookup(String hostname) {
-        return lookupBase64(hostname);
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        Destination d = getCache(hostname);
+        if (d != null)
+            return d;
+
+        // If it's long, assume it's a key.
+        if (hostname.length() >= 516) {
+            d = lookupBase64(hostname);
+            // What the heck, cache these too
+            putCache(hostname, d);
+            return d;
+        }
+
+        // Try Base32 decoding
+        if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p") &&
+            _context.getBooleanPropertyDefaultTrue(PROP_B32)) {
+            d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH));
+            if (d != null) {
+                putCache(hostname, d);
+                return d;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     *  Provide basic static caching for all services
+     */
+    protected static void putCache(String s, Destination d) {
+        if (d == null)
+            return;
+        synchronized (_cache) {
+            _cache.put(s, d);
+        }
+    }
+
+    /** @return cached dest or null */
+    protected static Destination getCache(String s) {
+        synchronized (_cache) {
+            return _cache.get(s);
+        }
+    }
+
+    /** @since 0.8.5 */
+    protected static void removeCache(String s) {
+        synchronized (_cache) {
+            _cache.remove(s);
+        }
+    }
+
+    /** @since 0.8.1 */
+    protected static void clearCache() {
+        synchronized (_cache) {
+            _cache.clear();
+        }
+    }
+
+    private static class LHM<K, V> extends LinkedHashMap<K, V> {
+        private final int _max;
+
+        public LHM(int max) {
+            super(max, 0.75f, true);
+            _max = max;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+            return size() > _max;
+        }
     }
 }
diff --git a/core/java/src/net/i2p/client/naming/EepGetAndAddNamingService.java b/core/java/src/net/i2p/client/naming/EepGetAndAddNamingService.java
index 085fe17290f6f57760d8cc831cb88e887253cd1e..83baf8a43e7048aa4334f66a164cd4ae69c393a8 100644
--- a/core/java/src/net/i2p/client/naming/EepGetAndAddNamingService.java
+++ b/core/java/src/net/i2p/client/naming/EepGetAndAddNamingService.java
@@ -30,6 +30,7 @@ import net.i2p.data.Destination;
  * i2p.naming.eepget.list=http://stats.i2p/cgi-bin/hostquery.cgi?a=,http://i2host.i2p/cgi-bin/i2hostquery?
  *
  * @author zzz
+ * @deprecated use HostsTxtNamingService.put()
  * @since 0.7.9
  */
 public class EepGetAndAddNamingService extends EepGetNamingService {
diff --git a/core/java/src/net/i2p/client/naming/EepGetNamingService.java b/core/java/src/net/i2p/client/naming/EepGetNamingService.java
index 737e33423869ae32bf09cbd623716dcf18da8085..eeadd6dd4f916bae8d268796af808b8c98c4fbcf 100644
--- a/core/java/src/net/i2p/client/naming/EepGetNamingService.java
+++ b/core/java/src/net/i2p/client/naming/EepGetNamingService.java
@@ -7,6 +7,7 @@ package net.i2p.client.naming;
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Properties;
 import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
@@ -25,6 +26,7 @@ import net.i2p.util.Log;
  * Should be used from MetaNamingService, after HostsTxtNamingService.
  * Cannot be used as the only NamingService! Be sure any naming service hosts
  * are in hosts.txt.
+ * Supports caching, b32, and b64.
  *
  * Sample config to put in configadvanced.jsp (restart required):
  *
@@ -33,11 +35,10 @@ import net.i2p.util.Log;
  * i2p.naming.eepget.list=http://namingservice.i2p/cgi-bin/lkup.cgi?host=,http://i2host.i2p/cgi-bin/i2hostquery?
  *
  */
-public class EepGetNamingService extends NamingService {
+public class EepGetNamingService extends DummyNamingService {
 
     private final static String PROP_EEPGET_LIST = "i2p.naming.eepget.list";
     private final static String DEFAULT_EEPGET_LIST = "http://i2host.i2p/cgi-bin/i2hostquery?";
-    private final static Log _log = new Log(EepGetNamingService.class);
 
     /** 
      * The naming service should only be constructed and accessed through the 
@@ -59,22 +60,13 @@ public class EepGetNamingService extends NamingService {
     }
     
     @Override
-    public Destination lookup(String hostname) {
-        // If it's long, assume it's a key.
-        if (hostname.length() >= DEST_SIZE)
-            return lookupBase64(hostname);
-
-        hostname = hostname.toLowerCase();
-
-        // If you want b32, chain with HostsTxtNamingService
-        if (hostname.length() == 60 && hostname.endsWith(".b32.i2p"))
-            return null;
-
-        // check the cache
-        Destination d = getCache(hostname);
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        Destination d = super.lookup(hostname, null, null);
         if (d != null)
             return d;
 
+        hostname = hostname.toLowerCase();
+
         List URLs = getURLs();
         if (URLs.isEmpty())
             return null;
@@ -103,7 +95,6 @@ public class EepGetNamingService extends NamingService {
     }
 
     // FIXME allow larger Dests for non-null Certs
-    private static final int DEST_SIZE = 516;                    // Std. Base64 length (no certificate)
     private static final int MAX_RESPONSE = DEST_SIZE + 68 + 10; // allow for hostname= and some trailing stuff
     private String fetchAddr(String url, String hostname) {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(MAX_RESPONSE);
diff --git a/core/java/src/net/i2p/client/naming/ExecNamingService.java b/core/java/src/net/i2p/client/naming/ExecNamingService.java
index 118f06eac55c3c8e35825ab48f1ef71794262164..78c902e4f2736f714f59a1ca59ca5239eeb471c2 100644
--- a/core/java/src/net/i2p/client/naming/ExecNamingService.java
+++ b/core/java/src/net/i2p/client/naming/ExecNamingService.java
@@ -5,6 +5,7 @@
 package net.i2p.client.naming;
 
 import java.io.InputStream;
+import java.util.Properties;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Destination;
@@ -27,6 +28,7 @@ import net.i2p.util.Log;
  *
  * Can be used from MetaNamingService, (e.g. after HostsTxtNamingService),
  * or as the sole naming service.
+ * Supports caching, b32, and b64.
  *
  * Sample chained config to put in configadvanced.jsp (restart required):
  *
@@ -40,13 +42,12 @@ import net.i2p.util.Log;
  * i2p.naming.exec.command=/usr/local/bin/i2presolve
  *
  */
-public class ExecNamingService extends NamingService {
+public class ExecNamingService extends DummyNamingService {
 
     private final static String PROP_EXEC_CMD = "i2p.naming.exec.command";
     private final static String DEFAULT_EXEC_CMD = "/usr/local/bin/i2presolve";
     private final static String PROP_SHELL_CMD = "i2p.naming.exec.shell";
     private final static String DEFAULT_SHELL_CMD = "/bin/bash";
-    private final static Log _log = new Log(ExecNamingService.class);
 
     /** 
      * The naming service should only be constructed and accessed through the 
@@ -59,22 +60,13 @@ public class ExecNamingService extends NamingService {
     }
     
     @Override
-    public Destination lookup(String hostname) {
-        // If it's long, assume it's a key.
-        if (hostname.length() >= DEST_SIZE)
-            return lookupBase64(hostname);
-
-        hostname = hostname.toLowerCase();
-
-        // If you want b32, chain with HostsTxtNamingService
-        if (hostname.length() == 60 && hostname.endsWith(".b32.i2p"))
-            return null;
-
-        // check the cache
-        Destination d = getCache(hostname);
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        Destination d = super.lookup(hostname, null, null);
         if (d != null)
             return d;
 
+        hostname = hostname.toLowerCase();
+
         // lookup
         String key = fetchAddr(hostname);	  	
         if (key != null) {
@@ -87,7 +79,6 @@ public class ExecNamingService extends NamingService {
     }
 
     // FIXME allow larger Dests for non-null Certs
-    private static final int DEST_SIZE = 516;                    // Std. Base64 length (no certificate)
     private static final int MAX_RESPONSE = DEST_SIZE + 68 + 10; // allow for hostname= and some trailing stuff
     private String fetchAddr(String hostname) {
         String[] commandArr = new String[3];
diff --git a/core/java/src/net/i2p/client/naming/FilesystemAddressDB.java b/core/java/src/net/i2p/client/naming/FilesystemAddressDB.java
deleted file mode 100644
index 806b9e90add718a48e8b5ebc6f36d42a78c581b8..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/FilesystemAddressDB.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package net.i2p.client.naming;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Properties;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Address;
-import net.i2p.data.DataFormatException;
-import net.i2p.data.DataHelper;
-import net.i2p.util.Log;
-
-/**
- *  @deprecated unused
- */
-public class FilesystemAddressDB extends AddressDB {
-
-    public final static String PROP_ADDRESS_DIR = "i2p.addressdir";
-    public final static String DEFAULT_ADDRESS_DIR = "addressDb";
-    private final static Log _log = new Log(FilesystemAddressDB.class);
-    
-    public FilesystemAddressDB(I2PAppContext context) {
-        super(context);
-        
-        //If the address db directory doesn't exist, create it, using the 
-        //contents of hosts.txt.
-        String dir = _context.getProperty(PROP_ADDRESS_DIR, DEFAULT_ADDRESS_DIR);
-        File addrDir = new File(dir);
-        if (!addrDir.exists()) {
-            addrDir.mkdir();
-            Properties hosts = new Properties();
-            File hostsFile = new File("hosts.txt");
-            if (hostsFile.exists() && hostsFile.canRead()) {
-                try {
-                    DataHelper.loadProps(hosts, hostsFile);
-                } catch (IOException ioe) {
-                    _log.error("Error loading hosts file " + hostsFile, ioe);
-                }
-            }
-            Iterator iter = hosts.keySet().iterator();
-            while (iter.hasNext()) {
-                String hostname = (String)iter.next();
-                Address addr = new Address();
-                addr.setHostname(hostname);
-                addr.setDestination(hosts.getProperty(hostname));
-                put(addr);
-            }
-        }
-    }
-    
-    @Override
-    public Address get(String hostname) {
-        String dir = _context.getProperty(PROP_ADDRESS_DIR, DEFAULT_ADDRESS_DIR);
-        File f = new File(dir, hostname);
-        if (f.exists() && f.canRead()) {
-            Address addr = new Address();
-            try {
-                addr.readBytes(new FileInputStream(f));
-            } catch (FileNotFoundException exp) {
-                return null;
-            } catch (DataFormatException exp) {
-                _log.error(f.getPath() + " is not a valid address file.");
-                return null;
-            } catch (IOException exp) {
-                _log.error("Error reading " + f.getPath());
-                return null;
-            }
-            return addr;
-        } else {
-            _log.warn(f.getPath() + " does not exist.");
-            return null;
-        }
-    }
-    
-    @Override
-    public Address put(Address address) {
-        Address previous = get(address.getHostname());
-        
-        String dir = _context.getProperty(PROP_ADDRESS_DIR, DEFAULT_ADDRESS_DIR);
-        File f = new File(dir, address.getHostname());
-        try {
-            address.writeBytes(new FileOutputStream(f));
-        } catch (Exception exp) {
-            _log.error("Error writing " + f.getPath(), exp);
-        }
-        return previous;
-    }
-    
-    @Override
-    public Address remove(String hostname) {
-        Address previous = get(hostname);
-        
-        String dir = _context.getProperty(PROP_ADDRESS_DIR, DEFAULT_ADDRESS_DIR);       
-        File f = new File(dir, hostname);
-        f.delete();
-        return previous;
-    }
-    
-    @Override
-    public Address remove(Address address) {
-        if (contains(address)) {
-            return remove(address.getHostname());
-        } else {
-            return null;
-        }
-    }
-    
-    @Override
-    public boolean contains(Address address) {
-        Address inDb = get(address.getHostname());
-        return inDb.equals(address);
-    }
-    
-    @Override
-    public boolean contains(String hostname) {
-        return hostnames().contains(hostname);
-    }
-    
-    @Override
-    public Collection hostnames() {
-        String dir = _context.getProperty(PROP_ADDRESS_DIR, DEFAULT_ADDRESS_DIR);
-        File f = new File(dir);
-        return Arrays.asList(f.list());
-    }
-
-}
diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
index a6045d6a27421d35258f0e2f478b018dd544c663..44b47848bd98eed61cc1e1babdd65654b1b5cb3a 100644
--- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
+++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
@@ -7,29 +7,21 @@
  */
 package net.i2p.client.naming;
 
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
-import java.util.Set;
 import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
-import net.i2p.data.DataFormatException;
-import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
-import net.i2p.data.Hash;
-import net.i2p.util.Log;
 
 /**
- * A naming service based on the "hosts.txt" file.
+ * A naming service based on multiple "hosts.txt" files.
+ * Supports .b32.i2p and {b64} lookups.
+ * Supports caching.
+ * All host names are converted to lower case.
  */
-public class HostsTxtNamingService extends NamingService {
+public class HostsTxtNamingService extends MetaNamingService {
 
     /** 
      * The naming service should only be constructed and accessed through the 
@@ -37,162 +29,52 @@ public class HostsTxtNamingService extends NamingService {
      * appropriate application context itself.
      *
      */
-    public HostsTxtNamingService(I2PAppContext context) { super(context); }
-    private HostsTxtNamingService() { super(null); }
+    public HostsTxtNamingService(I2PAppContext context) {
+        super(context, null);
+        for (String name : getFilenames()) {
+            addNamingService(new SingleFileNamingService(context, name), false);
+        }
+    }
     
     /**
      * If this system property is specified, the tunnel will read the
      * given file for hostname=destKey values when resolving names
      */
     public final static String PROP_HOSTS_FILE = "i2p.hostsfilelist";
-    public final static String PROP_B32 = "i2p.naming.hostsTxt.useB32";
 
-    /** default hosts.txt filename */
+    /** default hosts.txt filenames */
     public final static String DEFAULT_HOSTS_FILE = 
         "privatehosts.txt,userhosts.txt,hosts.txt";
 
-    private final static Log _log = new Log(HostsTxtNamingService.class);
-
-    private List getFilenames() {
+    private List<String> getFilenames() {
         String list = _context.getProperty(PROP_HOSTS_FILE, DEFAULT_HOSTS_FILE);
         StringTokenizer tok = new StringTokenizer(list, ",");
-        List rv = new ArrayList(tok.countTokens());
+        List<String> rv = new ArrayList(tok.countTokens());
         while (tok.hasMoreTokens())
             rv.add(tok.nextToken());
         return rv;
     }
     
-    private static final int BASE32_HASH_LENGTH = 52;   // 1 + Hash.HASH_LENGTH * 8 / 5
-
     @Override
-    public Destination lookup(String hostname) {
-        Destination d = getCache(hostname);
-        if (d != null)
-            return d;
-
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
         // If it's long, assume it's a key.
-        if (hostname.length() >= 516) {
-            d = lookupBase64(hostname);
-            // What the heck, cache these too
-            putCache(hostname, d);
-            return d;
-        }
-
-        // Try Base32 decoding
-        if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p") &&
-            Boolean.valueOf(_context.getProperty(PROP_B32, "true")).booleanValue()) {
-            d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH));
-            if (d != null) {
-                putCache(hostname, d);
-                return d;
-            }
-        }
-
-        List filenames = getFilenames();
-        for (int i = 0; i < filenames.size(); i++) { 
-            String hostsfile = (String)filenames.get(i);
-            try {
-                File f = new File(_context.getRouterDir(), hostsfile);
-                if ( (f.exists()) && (f.canRead()) ) {
-                    String key = getKey(f, hostname.toLowerCase());
-                    if ( (key != null) && (key.trim().length() > 0) ) {
-                        d = lookupBase64(key);
-                        putCache(hostname, d);
-                        return d;
-                    }
-                    
-                } else {
-                    _log.warn("Hosts file " + hostsfile + " does not exist.");
-                }
-            } catch (Exception ioe) {
-                _log.error("Error loading hosts file " + hostsfile, ioe);
-            }
-            // not found, continue to the next file
-        }
-        return null;
+        if (hostname.length() >= DEST_SIZE)
+            return lookupBase64(hostname);
+        return super.lookup(hostname.toLowerCase(), lookupOptions, storedOptions);
     }
-    
+
     @Override
-    public String reverseLookup(Destination dest) {
-        String destkey = dest.toBase64();
-        List filenames = getFilenames();
-        for (int i = 0; i < filenames.size(); i++) { 
-            String hostsfile = (String)filenames.get(i);
-            Properties hosts = new Properties();
-            try {
-                File f = new File(_context.getRouterDir(), hostsfile);
-                if ( (f.exists()) && (f.canRead()) ) {
-                    DataHelper.loadProps(hosts, f, true);
-                    Set keyset = hosts.keySet();
-                    Iterator iter = keyset.iterator();
-                    while (iter.hasNext()) {
-                        String host = (String)iter.next();
-                        String key = hosts.getProperty(host);
-                        if (destkey.equals(key))
-                            return host;
-                    }
-                }
-            } catch (Exception ioe) {
-                _log.error("Error loading hosts file " + hostsfile, ioe);
-            }
-        }
-        return null;
+    public boolean put(String hostname, Destination d, Properties options) {
+        return super.put(hostname.toLowerCase(), d, options);
     }
 
-    /** @deprecated unused */
     @Override
-    public String reverseLookup(Hash h) {
-        List filenames = getFilenames();
-        for (int i = 0; i < filenames.size(); i++) { 
-            String hostsfile = (String)filenames.get(i);
-            Properties hosts = new Properties();
-            try {
-                File f = new File(_context.getRouterDir(), hostsfile);
-                if ( (f.exists()) && (f.canRead()) ) {
-                    DataHelper.loadProps(hosts, f, true);
-                    Set keyset = hosts.keySet();
-                    Iterator iter = keyset.iterator();
-                    while (iter.hasNext()) {
-                        String host = (String)iter.next();
-                        String key = hosts.getProperty(host);
-                        try {
-                            Destination destkey = new Destination();
-                            destkey.fromBase64(key);
-                            if (h.equals(destkey.calculateHash()))
-                                return host;
-                        } catch (DataFormatException dfe) {}
-                    }
-                }
-            } catch (Exception ioe) {
-                _log.error("Error loading hosts file " + hostsfile, ioe);
-            }
-        }
-        return null;
+    public boolean putIfAbsent(String hostname, Destination d, Properties options) {
+        return super.putIfAbsent(hostname.toLowerCase(), d, options);
     }
 
-    /**
-     *  Better than DataHelper.loadProps(), doesn't load the whole file into memory,
-     *  and stops when it finds a match.
-     *
-     *  @param host lower case
-     *  @since 0.7.13
-     */
-    private static String getKey(File file, String host) throws IOException {
-        BufferedReader in = null;
-        try {
-            in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"), 16*1024);
-            String line = null;
-            while ( (line = in.readLine()) != null) {
-                if (!line.toLowerCase().startsWith(host + '='))
-                    continue;
-                if (line.indexOf('#') > 0)  // trim off any end of line comment
-                    line = line.substring(0, line.indexOf('#')).trim();
-                int split = line.indexOf('=');
-                return line.substring(split+1);   //.trim() ??????????????
-            }
-        } finally {
-            if (in != null) try { in.close(); } catch (IOException ioe) {}
-        }
-        return null;
+    @Override
+    public boolean remove(String hostname, Properties options) {
+        return super.remove(hostname.toLowerCase(), options);
     }
 }
diff --git a/core/java/src/net/i2p/client/naming/MetaNamingService.java b/core/java/src/net/i2p/client/naming/MetaNamingService.java
index 8f98532c607a410b0afe5f432fa3675d1dd9138d..483409e84e134db516aa6414c6f37b5f855ee012 100644
--- a/core/java/src/net/i2p/client/naming/MetaNamingService.java
+++ b/core/java/src/net/i2p/client/naming/MetaNamingService.java
@@ -2,61 +2,195 @@ package net.i2p.client.naming;
 
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.Iterator;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Properties;
 import java.util.StringTokenizer;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Destination;
 
-public class MetaNamingService extends NamingService {
+/**
+ * A naming service of multiple naming services.
+ * Supports .b32.i2p and {b64} lookups.
+ * Supports caching.
+ */
+public class MetaNamingService extends DummyNamingService {
     
     private final static String PROP_NAME_SERVICES = "i2p.nameservicelist";
     private final static String DEFAULT_NAME_SERVICES = 
-        "net.i2p.client.naming.PetNameNamingService,net.i2p.client.naming.HostsTxtNamingService";
-    private List _services;
+        "net.i2p.client.naming.HostsTxtNamingService";
+
+    protected final List<NamingService> _services;
     
+    /**
+     *  Adds the services from the i2p.nameservicelist property, in order, as chained services.
+     */
     public MetaNamingService(I2PAppContext context) {
         super(context);
-        
         String list = _context.getProperty(PROP_NAME_SERVICES, DEFAULT_NAME_SERVICES);
         StringTokenizer tok = new StringTokenizer(list, ",");
-        _services = new ArrayList(tok.countTokens());
+        _services = new CopyOnWriteArrayList();
         while (tok.hasMoreTokens()) {
             try {
                 Class cls = Class.forName(tok.nextToken());
                 Constructor con = cls.getConstructor(new Class[] { I2PAppContext.class });
-                _services.add(con.newInstance(new Object[] { context }));
+                addNamingService((NamingService)con.newInstance(new Object[] { context }), false);
             } catch (Exception ex) {
-                _services.add(new DummyNamingService(context)); // fallback
             }
         }
     }
     
+    /**
+     *  @param services if non-null, services to be added. If null, this will only handle b32 and b64,
+     *                  until addNamingService() is called later.
+     *  @since 0.8.5
+     */
+    public MetaNamingService(I2PAppContext context, List<NamingService> services) {
+        super(context);
+        _services = new CopyOnWriteArrayList();
+        if (services != null) {
+            for (NamingService ns : services) {
+                addNamingService(ns, false);
+            }
+        }
+    }
+    
+    @Override
+    public boolean addNamingService(NamingService ns, boolean head) {
+        if (head)
+            _services.add(0, ns);
+        else
+            _services.add(ns);
+        return true;
+    }
+
+    @Override
+    public List<NamingService> getNamingServices() {
+        return Collections.unmodifiableList(_services);
+    }
+
+    @Override
+    public boolean removeNamingService(NamingService ns) {
+        return  _services.remove(ns);
+    }
+
+    @Override
+    public void registerListener(NamingServiceListener nsl) {
+        for (NamingService ns : _services) { 
+            ns.registerListener(nsl);
+        }
+    }
+
+    @Override
+    public void unregisterListener(NamingServiceListener nsl) {
+        for (NamingService ns : _services) { 
+            ns.unregisterListener(nsl);
+        }
+    }
+
     @Override
-    public Destination lookup(String hostname) {
-        Iterator iter = _services.iterator();
-        while (iter.hasNext()) {
-            NamingService ns = (NamingService)iter.next();
-            Destination dest = ns.lookup(hostname);
-            if (dest != null) {
-                return dest;
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        // cache check is in super()
+        Destination d = super.lookup(hostname, null, null);
+        if (d != null)
+            return d;
+
+        for (NamingService ns : _services) { 
+            d = ns.lookup(hostname, lookupOptions, storedOptions);
+            if (d != null) {
+                putCache(hostname, d);
+                return d;
             }
         }
-        return lookupBase64(hostname);
+        return null;
     }
     
     @Override
-    public String reverseLookup(Destination dest) {
-        Iterator iter = _services.iterator();
-        while (iter.hasNext()) {
-            NamingService ns = (NamingService)iter.next();
-            String hostname = ns.reverseLookup(dest);
-            if (hostname != null) {
-                return hostname;
+    public String reverseLookup(Destination dest, Properties options) {
+        for (NamingService ns : _services) { 
+            String host = ns.reverseLookup(dest, options);
+            if (host != null) {
+                return host;
             }
         }
         return null;
     }
 
+    /**
+     *  Stores in the last service
+     */
+    @Override
+    public boolean put(String hostname, Destination d, Properties options) {
+        if (_services.isEmpty())
+            return false;
+        boolean rv = _services.get(_services.size() - 1).put(hostname, d, options);
+        // overwrite any previous entry in case it changed
+        if (rv)
+            putCache(hostname, d);
+        return rv;
+    }
+
+    /**
+     *  Stores in the last service
+     */
+    @Override
+    public boolean putIfAbsent(String hostname, Destination d, Properties options) {
+        if (_services.isEmpty())
+            return false;
+        boolean rv = _services.get(_services.size() - 1).putIfAbsent(hostname, d, options);
+        if (rv)
+            putCache(hostname, d);
+        return rv;
+    }
+
+    /**
+     *  Removes from all services
+     */
+    @Override
+    public boolean remove(String hostname, Properties options) {
+        boolean rv = false;
+        for (NamingService ns : _services) { 
+            if (ns.remove(hostname, options))
+                rv = true;
+        }
+        if (rv)
+            removeCache(hostname);
+        return rv;
+    }
+
+    /**
+     *  All services aggregated
+     */
+    @Override
+    public Map<String, Destination> getEntries(Properties options) {
+        Map<String, Destination> rv = new HashMap();
+        for (NamingService ns : _services) { 
+             rv.putAll(ns.getEntries(options));
+        }
+        return rv;
+    }
+
+    /**
+     *  All services aggregated
+     */
+    @Override
+    public int size(Properties options) {
+        int rv = 0;
+        for (NamingService ns : _services) { 
+             int s = ns.size(options);
+             if (s > 0)
+                 rv += s;
+        }
+        return rv;
+    }
+
+    public void shutdown() {
+        for (NamingService ns : _services) { 
+            ns.shutdown();
+        }
+    }
 }
diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java
index fc9a5341400a896af1335ab1472d2e9ec38e54b0..ba26d45ed0b8f8dbc0fabdd2e2b5b31a5fc46617 100644
--- a/core/java/src/net/i2p/client/naming/NamingService.java
+++ b/core/java/src/net/i2p/client/naming/NamingService.java
@@ -9,9 +9,13 @@ package net.i2p.client.naming;
 
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
@@ -24,16 +28,14 @@ import net.i2p.util.Log;
  */
 public abstract class NamingService {
 
-    private final static Log _log = new Log(NamingService.class);
-    protected I2PAppContext _context;
-    private /* FIXME final FIXME */ HashMap _cache;
+    protected final Log _log;
+    protected final I2PAppContext _context;
+    protected final Set<NamingServiceListener> _listeners;
+    protected final Set<NamingServiceUpdater> _updaters;
 
     /** what classname should be used as the naming service impl? */
     public static final String PROP_IMPL = "i2p.naming.impl";
     private static final String DEFAULT_IMPL = "net.i2p.client.naming.HostsTxtNamingService";
-
-    protected static final int CACHE_MAX_SIZE = 16;
-
     
     /** 
      * The naming service should only be constructed and accessed through the 
@@ -43,9 +45,9 @@ public abstract class NamingService {
      */
     protected NamingService(I2PAppContext context) {
         _context = context;
-        _cache = new HashMap(CACHE_MAX_SIZE);
-    }
-    private NamingService() { // nop
+        _log = context.logManager().getLog(getClass());
+        _listeners = new CopyOnWriteArraySet();
+        _updaters = new CopyOnWriteArraySet();
     }
     
     /**
@@ -53,7 +55,9 @@ public abstract class NamingService {
      * @return the Destination for this host name, or
      * <code>null</code> if name is unknown.
      */
-    public abstract Destination lookup(String hostname);
+    public Destination lookup(String hostname) {
+        return lookup(hostname, null, null);
+    }
 
     /**
      * Reverse look up a destination
@@ -61,10 +65,12 @@ public abstract class NamingService {
      * if none is known. It is safe for subclasses to always return
      * <code>null</code> if no reverse lookup is possible.
      */
-    public String reverseLookup(Destination dest) { return null; };
+    public String reverseLookup(Destination dest) {
+        return reverseLookup(dest, null);
+    }
 
     /** @deprecated unused */
-    public String reverseLookup(Hash h) { return null; };
+    public String reverseLookup(Hash h) { return null; }
 
     /**
      * Check if host name is valid Base64 encoded dest and return this
@@ -82,103 +88,354 @@ public abstract class NamingService {
         }
     }
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName();
+    }
+
+    ///// New API Starts Here
+
     /**
-     * Get a naming service instance. This method ensures that there
-     * will be only one naming service instance (singleton) as well as
-     * choose the implementation from the "i2p.naming.impl" system
-     * property.
+     *  @return Class simple name by default
+     *  @since 0.8.5
      */
-    public static final synchronized NamingService createInstance(I2PAppContext context) {
-        NamingService instance = null;
-        String impl = context.getProperty(PROP_IMPL, DEFAULT_IMPL);
-        try {
-            Class cls = Class.forName(impl);
-            Constructor con = cls.getConstructor(new Class[] { I2PAppContext.class });
-            instance = (NamingService)con.newInstance(new Object[] { context });
-        } catch (Exception ex) {
-            _log.error("Cannot loadNaming service implementation", ex);
-            instance = new DummyNamingService(context); // fallback
-        }
-        return instance;
+    public String getName() {
+        return getClass().getSimpleName();
     }
 
     /**
-     *  Provide basic caching for the service
-     *  The service may override the age and/or size limit
+     *  @return NamingService-specific options or null
+     *  @since 0.8.5
      */
-    /** Don't know why a dest would ever change but keep this short anyway */
-    protected static final long CACHE_MAX_AGE = 7*60*1000;
+    public Properties getConfiguration() {
+        return null;
+    }
 
-    private class CacheEntry {
-        public Destination dest;
-        public long exp;
-        public CacheEntry(Destination d) {
-            dest = d;
-            exp = _context.clock().now() + CACHE_MAX_AGE;
-        }
-        public boolean isExpired() {
-            return exp < _context.clock().now();
-        }
+    /**
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean setConfiguration(Properties p) {
+        return true;
     }
 
+    // These are for daisy chaining (MetaNamingService)
+
     /**
-     * Clean up when full.
-     * Don't bother removing old entries unless full.
-     * Caller must synchronize on _cache.
-     */
-    private void cacheClean() {
-        if (_cache.size() < CACHE_MAX_SIZE)
-            return;
-        boolean full = true;
-        Object oldestKey = null;
-        long oldestExp = Long.MAX_VALUE;
-        ArrayList deleteList = new ArrayList(CACHE_MAX_SIZE);
-        for (Iterator iter = _cache.entrySet().iterator(); iter.hasNext(); ) {
-            Map.Entry entry = (Map.Entry) iter.next();
-            CacheEntry ce = (CacheEntry) entry.getValue();
-            if (ce.isExpired()) {
-                deleteList.add(entry.getKey());
-                full = false;
-                continue;
-            }
-            if (oldestKey == null || ce.exp < oldestExp) {
-                oldestKey = entry.getKey();
-                oldestExp = ce.exp;
-            }
-        }
-        if (full && oldestKey != null)
-            deleteList.add(oldestKey);
-        for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) {
-            _cache.remove(iter.next());
-        }
+     *  @return chained naming services or null
+     *  @since 0.8.5
+     */
+    public List<NamingService> getNamingServices() {
+        return null;
     }
 
-    protected void putCache(String s, Destination d) {
-        if (d == null)
-            return;
-        synchronized (_cache) {
-            _cache.put(s, new CacheEntry(d));
-            cacheClean();
+    /**
+     *  @return parent naming service or null if this is the root
+     *  @since 0.8.5
+     */
+    public NamingService getParent() {
+        return null;
+    }
+
+    /**
+     * Only for chaining-capable NamingServices. Add to end of the list.
+     * @return success
+     */
+    public boolean addNamingService(NamingService ns) {
+        return addNamingService(ns, false);
+    }
+
+
+    /**
+     * Only for chaining-capable NamingServices
+     * @param head or tail
+     * @return success
+     */
+    public boolean addNamingService(NamingService ns, boolean head) {
+        return false;
+    }
+
+    /**
+     *  Only for chaining-capable NamingServices
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean removeNamingService(NamingService ns) {
+        return false;
+    }
+
+    // options would be used to specify public / private / master ...
+    // or should we just daisy chain 3 HostsTxtNamingServices ?
+    // that might be better... then addressbook only talks to the 'router' HostsTxtNamingService
+
+    /**
+     *  @return number of entries or -1 if unknown
+     *  @since 0.8.5
+     */
+    public int size() {
+        return size(null);
+    }
+
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return number of entries (matching the options if non-null) or -1 if unknown
+     *  @since 0.8.5
+     */
+    public int size(Properties options) {
+        return -1;
+    }
+
+    /**
+     *  @return all mappings
+     *          or empty Map if none;
+     *          Returned Map is not necessarily sorted, implementation dependent
+     *  @since 0.8.5
+     */
+    public Map<String, Destination> getEntries() {
+        return getEntries(null);
+    }
+
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return all mappings (matching the options if non-null)
+     *          or empty Map if none;
+     *          Returned Map is not necessarily sorted, implementation dependent
+     *  @since 0.8.5
+     */
+    public Map<String, Destination> getEntries(Properties options) {
+        return Collections.EMPTY_MAP;
+    }
+
+    /**
+     *  This may be more or less efficient than getEntries()
+     *  @param options NamingService-specific, can be null
+     *  @return all mappings (matching the options if non-null)
+     *          or empty Map if none;
+     *          Returned Map is not necessarily sorted, implementation dependent
+     *  @since 0.8.5
+     */
+    public Map<String, String> getBase64Entries(Properties options) {
+        return Collections.EMPTY_MAP;
+    }
+
+    /**
+     *  @return all known host names
+     *          or empty Set if none;
+     *          Returned Set is not necessarily sorted, implementation dependent
+     *  @since 0.8.5
+     */
+    public Set<String> getNames() {
+        return getNames(null);
+    }
+
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return all known host names (matching the options if non-null)
+     *          or empty Set if none;
+     *          Returned Set is not necessarily sorted, implementation dependent
+     *  @since 0.8.5
+     */
+    public Set<String> getNames(Properties options) {
+        return Collections.EMPTY_SET;
+    }
+
+    /**
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean put(String hostname, Destination d) {
+        return put(hostname, d, null);
+    }
+
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean put(String hostname, Destination d, Properties options) {
+        return false;
+    }
+
+    /**
+     *  Fails if entry previously exists
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean putIfAbsent(String hostname, Destination d) {
+        return putIfAbsent(hostname, d, null);
+    }
+
+    /**
+     *  Fails if entry previously exists
+     *  @param options NamingService-specific, can be null
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean putIfAbsent(String hostname, Destination d, Properties options) {
+        return false;
+    }
+
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean putAll(Map<String, Destination> entries, Properties options) {
+        boolean rv = true;
+        for (Map.Entry<String, Destination> entry : entries.entrySet()) {
+            if (!put(entry.getKey(), entry.getValue(), options))
+                rv = false;
         }
+        return rv;
+    }
+
+    /**
+     *  Fails if entry did not previously exist
+     *  @param d may be null if only options are changing
+     *  @param options NamingService-specific, can be null
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean update(String hostname, Destination d, Properties options) {
+        return false;
+    }
+
+    /**
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean remove(String hostname) {
+        return remove(hostname, null);
     }
 
-    protected Destination getCache(String s) {
-        synchronized (_cache) {
-            CacheEntry ce = (CacheEntry) _cache.get(s);
-            if (ce == null)
-                return null;
-            if (ce.isExpired()) {
-                _cache.remove(s);
-                return null;
-            }
-            return ce.dest;
+    /**
+     *  @param options NamingService-specific, can be null
+     *  @return success
+     *  @since 0.8.5
+     */
+    public boolean remove(String hostname, Properties options) {
+        return false;
+    }
+
+    /**
+     *  Ask any registered updaters to update now
+     *  @param options NamingService- or updater-specific, may be null
+     *  @since 0.8.5
+     */
+    public void requestUpdate(Properties options) {
+        for (NamingServiceUpdater nsu : _updaters) {
+            nsu.update(options);
         }
     }
 
-    /** @since 0.8.1 */
-    public void clearCache() {
-        synchronized (_cache) {
-            _cache.clear();
+    /**
+     *  @since 0.8.5
+     */
+    public void registerListener(NamingServiceListener nsl) {
+        _listeners.add(nsl);
+    }
+
+    /**
+     *  @since 0.8.5
+     */
+    public void unregisterListener(NamingServiceListener nsl) {
+        _listeners.remove(nsl);
+    }
+
+    /**
+     *  @since 0.8.6
+     */
+    public void registerUpdater(NamingServiceUpdater nsu) {
+        _updaters.add(nsu);
+    }
+
+    /**
+     *  @since 0.8.6
+     */
+    public void unregisterUpdater(NamingServiceUpdater nsu) {
+        _updaters.remove(nsu);
+    }
+
+    /**
+     *  Same as lookup(hostname) but with in and out options
+     *  Note that whether this (and lookup(hostname)) resolve B32 addresses is
+     *  NamingService-specific.
+     *  @param lookupOptions input parameter, NamingService-specific, can be null
+     *  @param storedOptions output parameter, NamingService-specific, any stored properties will be added if non-null
+     *  @return dest or null
+     *  @since 0.8.5
+     */
+    public abstract Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions);
+
+    /**
+     *  Same as reverseLookup(dest) but with options
+     *  @param options NamingService-specific, can be null
+     *  @return host name or null
+     *  @since 0.8.5
+     */
+    public String reverseLookup(Destination d, Properties options) {
+        return null;
+    }
+
+    /**
+     *  Lookup a Base 32 address. This may require the router to fetch the LeaseSet,
+     *  which may take quite a while.
+     *  @param hostname must be {52 chars}.b32.i2p
+     *  @param timeout in seconds; <= 0 means use router default
+     *  @return dest or null
+     *  @since 0.8.5
+     */
+    public Destination lookupBase32(String hostname, int timeout) {
+        return null;
+    }
+
+    /**
+     *  Same as lookupB32 but with the SHA256 Hash precalculated
+     *  @param timeout in seconds; <= 0 means use router default
+     *  @return dest or null
+     *  @since 0.8.5
+     */
+    public Destination lookup(Hash hash, int timeout) {
+        return null;
+    }
+
+    /**
+     *  Parent will call when added.
+     *  If this is the root naming service, the core will start it.
+     *  Should not be called by others.
+     *  @since 0.8.5
+     */
+    public void start() {}
+
+    /**
+     *  Parent will call when removed.
+     *  If this is the root naming service, the core will stop it.
+     *  Should not be called by others.
+     *  @since 0.8.5
+     */
+    public void shutdown() {}
+
+    //// End New API
+
+    /**
+     * Get a naming service instance. This method ensures that there
+     * will be only one naming service instance (singleton) as well as
+     * choose the implementation from the "i2p.naming.impl" system
+     * property.
+     */
+    public static final synchronized NamingService createInstance(I2PAppContext context) {
+        NamingService instance = null;
+        String impl = context.getProperty(PROP_IMPL, DEFAULT_IMPL);
+        try {
+            Class cls = Class.forName(impl);
+            Constructor con = cls.getConstructor(new Class[] { I2PAppContext.class });
+            instance = (NamingService)con.newInstance(new Object[] { context });
+        } catch (Exception ex) {
+            Log log = context.logManager().getLog(NamingService.class);
+            log.error("Cannot load naming service " + impl, ex);
+            instance = new DummyNamingService(context); // fallback
         }
+        return instance;
     }
+
 }
diff --git a/core/java/src/net/i2p/client/naming/NamingServiceListener.java b/core/java/src/net/i2p/client/naming/NamingServiceListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..92bb88f82b5f5b7dfd863f5c611630b1595b59b8
--- /dev/null
+++ b/core/java/src/net/i2p/client/naming/NamingServiceListener.java
@@ -0,0 +1,28 @@
+package net.i2p.client.naming;
+
+import java.util.Properties;
+
+import net.i2p.data.Destination;
+
+/**
+ * @since 0.8.6
+ */
+public interface NamingServiceListener {
+
+    /** also called when a NamingService is added or removed */
+    public void configurationChanged(NamingService ns);
+
+    /**
+     *  @param options NamingService-specific, can be null
+     */
+    public void entryAdded(NamingService ns, String hostname, Destination dest, Properties options);
+
+    /**
+     *  @param dest null if unchanged
+     *  @param options NamingService-specific, can be null
+     */
+    public void entryChanged(NamingService ns, String hostname, Destination dest, Properties options);
+
+    public void entryRemoved(NamingService ns, String hostname);
+}
+
diff --git a/core/java/src/net/i2p/client/naming/NamingServiceUpdater.java b/core/java/src/net/i2p/client/naming/NamingServiceUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1bc6632752d2082a015a5baab5d3b0f29abfc45
--- /dev/null
+++ b/core/java/src/net/i2p/client/naming/NamingServiceUpdater.java
@@ -0,0 +1,16 @@
+package net.i2p.client.naming;
+
+import java.util.Properties;
+
+/**
+ * @since 0.8.6
+ */
+public interface NamingServiceUpdater {
+
+    /**
+     *  Should not block.
+     *  @param options Updater-specific, may be null
+     */
+    public void update(Properties options);
+}
+
diff --git a/core/java/src/net/i2p/client/naming/PetName.java b/core/java/src/net/i2p/client/naming/PetName.java
deleted file mode 100644
index cb0df4ab96ceefdadce22e3d31946571a4881a88..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/PetName.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package net.i2p.client.naming;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import net.i2p.data.DataHelper;
-
-/**
- *  deprecated unused but can be instantiated through I2PAppContext
- */
-public class PetName {
-    private String _name;
-    private String _network;
-    private String _protocol;
-    private List _groups;
-    private boolean _isPublic;
-    private String _location;
-    
-    public PetName() {
-        this(null, null, null, null);
-    }
-    public PetName(String name, String network, String protocol, String location) {
-        _name = name;
-        _network = network;
-        _protocol = protocol;
-        _location = location;
-        _groups = new ArrayList();
-        _isPublic = false;
-    }
-    /**
-     * @param dbLine name:network:protocol:isPublic:group1,group2,group3:location
-     */
-    public PetName(String dbLine) {
-        _groups = new ArrayList();
-        StringTokenizer tok = new StringTokenizer(dbLine, ":\n", true);
-        int tokens = tok.countTokens();
-        //System.out.println("Tokens: " + tokens);
-        if (tokens < 7) {
-            return;
-        }
-        String s = tok.nextToken();
-        if (":".equals(s)) {
-            _name = null;
-        } else {
-            _name = s;
-            s = tok.nextToken(); // skip past the :
-        }
-        s = tok.nextToken();
-        if (":".equals(s)) {
-            _network = null;
-        } else {
-            _network = s;
-            s = tok.nextToken(); // skip past the :
-        }
-        s = tok.nextToken();
-        if (":".equals(s)) {
-            _protocol = null;
-        } else {
-            _protocol = s;
-            s = tok.nextToken(); // skip past the :
-        }
-        s = tok.nextToken();
-        if (":".equals(s)) {
-            _isPublic = false;
-        } else {
-            if ("true".equals(s))
-                _isPublic = true;
-            else
-                _isPublic = false;
-            s = tok.nextToken(); // skip past the :
-        }
-        s = tok.nextToken();
-        if (":".equals(s)) {
-            // noop
-        } else {
-            StringTokenizer gtok = new StringTokenizer(s, ",");
-            while (gtok.hasMoreTokens())
-                _groups.add(gtok.nextToken().trim());
-            s = tok.nextToken(); // skip past the :
-        }
-        while (tok.hasMoreTokens()) {
-            if (_location == null)
-                _location = tok.nextToken();
-            else
-                _location = _location + tok.nextToken();
-        }
-    }
-    
-    public String getName() { return _name; }
-    public String getNetwork() { return _network; }
-    public String getProtocol() { return _protocol; }
-    public String getLocation() { return _location; }
-    public boolean getIsPublic() { return _isPublic; }
-    public int getGroupCount() { return _groups.size(); }
-    public String getGroup(int i) { return (String)_groups.get(i); }
-    
-    public void setName(String name) { _name = name; }
-    public void setNetwork(String network) { _network = network; }
-    public void setProtocol(String protocol) { _protocol = protocol; }
-    public void setLocation(String location) { _location = location; }
-    public void setIsPublic(boolean pub) { _isPublic = pub; }
-    public void addGroup(String name) { 
-        if ( (name != null) && (name.length() > 0) && (!_groups.contains(name)) )
-            _groups.add(name);
-    }
-    public void removeGroup(String name) { _groups.remove(name); }
-    public void setGroups(String groups) {
-        if (groups != null) {
-            _groups.clear();
-            StringTokenizer tok = new StringTokenizer(groups, ", \t");
-            while (tok.hasMoreTokens())
-                addGroup(tok.nextToken().trim());
-        } else {
-            _groups.clear();
-        }
-    }
-    public boolean isMember(String group) {
-        for (int i = 0; i < getGroupCount(); i++)
-            if (getGroup(i).equals(group))
-                return true;
-        return false;
-    }
-    
-    @Override
-    public String toString() {
-        StringBuilder buf = new StringBuilder(256);
-        if (_name != null) buf.append(_name.trim());
-        buf.append(':');
-        if (_network != null) buf.append(_network.trim());
-        buf.append(':');
-        if (_protocol != null) buf.append(_protocol.trim());
-        buf.append(':').append(_isPublic).append(':');
-        if (_groups != null) {
-            for (int i = 0; i < _groups.size(); i++) {
-                buf.append(((String)_groups.get(i)).trim());
-                if (i + 1 < _groups.size())
-                    buf.append(',');
-            }
-        }
-        buf.append(':');
-        if (_location != null) buf.append(_location.trim());
-        return buf.toString();
-    }
-    
-    @Override
-    public boolean equals(Object obj) {
-        if ( (obj == null) || !(obj instanceof PetName) ) return false;
-        PetName pn = (PetName)obj;
-        return DataHelper.eq(_name, pn._name) &&
-               DataHelper.eq(_location, pn._location) &&
-               DataHelper.eq(_network, pn._network) &&
-               DataHelper.eq(_protocol, pn._protocol);
-    }
-    @Override
-    public int hashCode() {
-        int rv = 0;
-        rv += DataHelper.hashCode(_name);
-        rv += DataHelper.hashCode(_location);
-        rv += DataHelper.hashCode(_network);
-        rv += DataHelper.hashCode(_protocol);
-        return rv;
-    }
-    
-    public static void main(String args[]) {
-        test("a:b:c:true:e:f");
-        test("a:::true::d");
-        test("a:::true::");
-        test("a:b::true::");
-        test(":::trye::");
-        test("a:b:c:true:e:http://foo.bar");
-    }
-    private static void test(String line) {
-        PetName pn = new PetName(line);
-        String val = pn.toString();
-        System.out.println("OK? " + val.equals(line) + ": " + line + " [" + val + "]");
-    }
-}
diff --git a/core/java/src/net/i2p/client/naming/PetNameDB.java b/core/java/src/net/i2p/client/naming/PetNameDB.java
deleted file mode 100644
index cff2ab56ccddf16404715b0106f715984e70d1ed..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/PetNameDB.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package net.i2p.client.naming;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-
-/**
- *  deprecated unused but can be instantiated through I2PAppContext
- */
-public class PetNameDB {
-    /** name (String) to PetName mapping */
-    private final Map _names;
-    private String _path;
-    
-    public PetNameDB() {
-        _names = Collections.synchronizedMap(new HashMap());
-    }
-
-    public PetName getByName(String name) { 
-        if ( (name == null) || (name.length() <= 0) ) return null;
-        return (PetName)_names.get(name.toLowerCase()); 
-    }
-    public void add(PetName pn) { 
-        if ( (pn == null) || (pn.getName() == null) ) return;
-        _names.put(pn.getName().toLowerCase(), pn); 
-    }
-    public void clear() { _names.clear(); }
-    public boolean contains(PetName pn) { return _names.containsValue(pn); }
-    public boolean containsName(String name) { 
-        if ( (name == null) || (name.length() <= 0) ) return false;
-        return _names.containsKey(name.toLowerCase()); 
-    }
-    public boolean isEmpty() { return _names.isEmpty(); }
-    public Iterator iterator() { return new ArrayList(_names.values()).iterator(); }
-    public void remove(PetName pn) { 
-        if (pn != null) _names.remove(pn.getName().toLowerCase());
-    }
-    public void removeName(String name) { 
-        if (name != null) _names.remove(name.toLowerCase()); 
-    }
-    public int size() { return _names.size(); }
-    public Set getNames() { return new HashSet(_names.keySet()); }
-    public List getGroups() {
-        List rv = new ArrayList();
-        for (Iterator iter = iterator(); iter.hasNext(); ) {
-            PetName name = (PetName)iter.next();
-            for (int i = 0; i < name.getGroupCount(); i++)
-                if (!rv.contains(name.getGroup(i)))
-                    rv.add(name.getGroup(i));
-        }
-        return rv;
-    }
-    
-    public PetName getByLocation(String location) { 
-        if (location == null) return null;
-        synchronized (_names) {
-            for (Iterator iter = iterator(); iter.hasNext(); ) {
-                PetName name = (PetName)iter.next();
-                if ( (name.getLocation() != null) && (name.getLocation().trim().equals(location.trim())) )
-                    return name;
-            }
-        }
-        return null;
-    }
-    
-    public void load(String location) throws IOException {
-        _path = location;
-        File f = new File(location);
-        if (!f.exists()) return;
-        BufferedReader in = null;
-        try {
-            in = new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"));
-            String line = null;
-            while ( (line = in.readLine()) != null) {
-                PetName name = new PetName(line);
-                if (name.getName() != null)
-                    add(name);
-            }
-        } finally {
-            in.close();
-        }
-    }
-    
-    public void store(String location) throws IOException {
-        Writer out = null;
-        try {
-            out = new OutputStreamWriter(new FileOutputStream(location), "UTF-8");
-            for (Iterator iter = iterator(); iter.hasNext(); ) {
-                PetName name = (PetName)iter.next();
-                if (name != null)
-                    out.write(name.toString() + "\n");
-            }
-        } finally {
-            out.close();
-        }
-    }
-    
-    public void store() throws IOException {
-        if (_path != null) {
-            store(_path);
-        }
-    }
-}
diff --git a/core/java/src/net/i2p/client/naming/PetNameNamingService.java b/core/java/src/net/i2p/client/naming/PetNameNamingService.java
deleted file mode 100644
index a052315f39410664e92bdbcf012e1b051fc39dd2..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/client/naming/PetNameNamingService.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.i2p.client.naming;
-
-import java.io.IOException;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Destination;
-
-/**
- *  @deprecated unused
- */
-public class PetNameNamingService extends NamingService {
-
-    private PetNameDB _petnameDb;
-    public final static String PROP_PETNAME_FILE = "i2p.petnamefile";
-    public final static String DEFAULT_PETNAME_FILE = "petnames.txt";
-    
-    public PetNameNamingService(I2PAppContext context) {
-        super(context);
-        _petnameDb = _context.petnameDb();
-        String file = _context.getProperty(PROP_PETNAME_FILE, DEFAULT_PETNAME_FILE);
-
-        //If the petnamedb file doesn't exist, create it, using the 
-        //contents of hosts.txt.
-//        File nameFile = new File(file);
-//        if (!nameFile.exists()) {
-//            Properties hosts = new Properties();
-//            File hostsFile = new File("hosts.txt");
-//            if (hostsFile.exists() && hostsFile.canRead()) {
-//                try {
-//                    DataHelper.loadProps(hosts, hostsFile);
-//                } catch (IOException ioe) {
-//                }
-//            }
-//            Iterator iter = hosts.keySet().iterator();
-//            while (iter.hasNext()) {
-//                String hostname = (String)iter.next();
-//                PetName pn = new PetName(hostname, "i2p", "http", hosts.getProperty(hostname));
-//                _petnameDb.set(hostname, pn);
-//            }
-//            try {
-//                _petnameDb.store(file);
-//            } catch (IOException ioe) {
-//            }
-//        }
-        
-        try {
-            _petnameDb.load(file);
-        } catch (IOException ioe) {
-        }
-    }
-    
-    @Override
-    public Destination lookup(String hostname) {
-        PetName name = _petnameDb.getByName(hostname);
-        if (name != null && name.getNetwork().equalsIgnoreCase("i2p")) {
-            return lookupBase64(name.getLocation());
-        } else {
-            return lookupBase64(hostname);
-        }
-    }
-    
-    @Override
-    public String reverseLookup(Destination dest) {
-        return _petnameDb.getByLocation(dest.toBase64()).getName();
-    }
-}
diff --git a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..231ca491cf633e3895ce213bd29d6945f325aeba
--- /dev/null
+++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
@@ -0,0 +1,470 @@
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by mihi in 2004 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ */
+package net.i2p.client.naming;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+import net.i2p.util.SecureFile;
+import net.i2p.util.SecureFileOutputStream;
+
+/**
+ * A naming service based on a single file using the "hosts.txt" format.
+ * Supports adds, removes, and listeners.
+ *
+ * All methods here are case-sensitive.
+ * Conversion to lower case is done in HostsTxtNamingService.
+ *
+ * This does NOT provide .b32.i2p or {b64} resolution.
+ * It also does not do any caching.
+ * Use from HostsTxtNamingService or chain with another NamingService
+ * via MetaNamingService if you need those features.
+ *
+ * @since 0.8.5
+ */
+public class SingleFileNamingService extends NamingService {
+
+    private final File _file;
+    private final ReentrantReadWriteLock _fileLock;
+    /** cached number of entries */
+    private int _size;
+    /** last write time */
+    private long _lastWrite;
+    private volatile boolean _isClosed;
+
+    public SingleFileNamingService(I2PAppContext context, String filename) {
+        super(context);
+        File file = new File(filename);
+        if (!file.isAbsolute())
+            file = new File(context.getRouterDir(), filename);
+        _file = file;
+        _fileLock = new ReentrantReadWriteLock(true);
+    }
+
+    /**
+     *  @return the base file name
+     */
+    @Override
+    public String getName() {
+        return _file.getAbsolutePath();
+    }
+
+    /** 
+     *  @param hostname case-sensitive; caller should convert to lower case
+     *  @param lookupOptions ignored
+     *  @param storedOptions ignored
+     */
+    @Override
+    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
+        try {
+            String key = getKey(hostname);
+            if (key != null)
+                return lookupBase64(key);
+        } catch (Exception ioe) {
+            if (_file.exists())
+                _log.error("Error loading hosts file " + _file, ioe);
+            else if (_log.shouldLog(Log.WARN))
+                _log.warn("Error loading hosts file " + _file, ioe);
+        }
+        return null;
+    }
+    
+    /** 
+     *  @param options ignored
+     */
+    @Override
+    public String reverseLookup(Destination dest, Properties options) {
+        String destkey = dest.toBase64();
+        BufferedReader in = null;
+        getReadLock();
+        try {
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+            String line = null;
+            while ( (line = in.readLine()) != null) {
+                if (line.startsWith("#"))
+                    continue;
+                if (line.indexOf('#') > 0)  // trim off any end of line comment
+                    line = line.substring(0, line.indexOf('#')).trim();
+                int split = line.indexOf('=');
+                if (split <= 0)
+                    continue;
+                if (destkey.equals(line.substring(split + 1)))
+                    return line.substring(0, split);
+            }
+            return null;
+        } catch (Exception ioe) {
+            if (_file.exists())
+                _log.error("Error loading hosts file " + _file, ioe);
+            else if (_log.shouldLog(Log.WARN))
+                _log.warn("Error loading hosts file " + _file, ioe);
+            return null;
+        } finally {
+            releaseReadLock();
+        }
+    }
+
+    /**
+     *  Better than DataHelper.loadProps(), doesn't load the whole file into memory,
+     *  and stops when it finds a match.
+     *
+     *  @param host case-sensitive; caller should convert to lower case
+     */
+    private String getKey(String host) throws IOException {
+        BufferedReader in = null;
+        getReadLock();
+        try {
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+            String line = null;
+            String search = host + '=';
+            while ( (line = in.readLine()) != null) {
+                if (!line.startsWith(search))
+                    continue;
+                if (line.indexOf('#') > 0)  // trim off any end of line comment
+                    line = line.substring(0, line.indexOf('#')).trim();
+                int split = line.indexOf('=');
+                return line.substring(split+1);   //.trim() ??????????????
+            }
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            releaseReadLock();
+        }
+        return null;
+    }
+
+    /** 
+     *  @param hostname case-sensitive; caller should convert to lower case
+     *  @param options ignored
+     */
+    @Override
+    public boolean put(String hostname, Destination d, Properties options) {
+        // try easy way first, most adds are not replaces
+        if (putIfAbsent(hostname, d, options))
+            return true;
+        if (!getWriteLock())
+            return false;
+        BufferedReader in = null;
+        BufferedWriter out = null;
+        try {
+            if (_isClosed)
+                return false;
+            File tmp = SecureFile.createTempFile("temp-", ".tmp", _file.getAbsoluteFile().getParentFile());
+            out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8"));
+            if (_file.exists()) {
+                in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+                String line = null;
+                String search = hostname + '=';
+                while ( (line = in.readLine()) != null) {
+                    if (line.startsWith(search))
+                        continue;
+                    out.write(line);
+                    out.newLine();
+                }
+                in.close();
+            }
+            out.write(hostname);
+            out.write('=');
+            out.write(d.toBase64());
+            out.newLine();
+            out.close();
+            boolean success = rename(tmp, _file);
+            if (success) {
+                for (NamingServiceListener nsl : _listeners) { 
+                    nsl.entryChanged(this, hostname, d, options);
+                }
+            }
+            return success;
+        } catch (IOException ioe) {
+            if (in != null) try { in.close(); } catch (IOException e) {}
+            if (out != null) try { out.close(); } catch (IOException e) {}
+            _log.error("Error adding " + hostname, ioe);
+            return false;
+        } finally { releaseWriteLock(); }
+    }
+
+    /** 
+     *  @param hostname case-sensitive; caller should convert to lower case
+     *  @param options ignored
+     */
+    @Override
+    public boolean putIfAbsent(String hostname, Destination d, Properties options) {
+        OutputStream out = null;
+        if (!getWriteLock())
+            return false;
+        try {
+            if (_isClosed)
+                return false;
+            // simply check if present, and if not, append
+            try {
+                if (getKey(hostname) != null)
+                    return false;
+            } catch (IOException ioe) {
+                if (_file.exists()) {
+                    _log.error("Error adding " + hostname, ioe);
+                    return false;
+                }
+                // else new file
+            }
+            out = new SecureFileOutputStream(_file, true);
+            // FIXME fails if previous last line didn't have a trailing \n
+            out.write(hostname.getBytes("UTF-8"));
+            out.write('=');
+            out.write(d.toBase64().getBytes());
+            out.write('\n');
+            out.close();
+            for (NamingServiceListener nsl : _listeners) { 
+                nsl.entryAdded(this, hostname, d, options);
+            }
+            return true;
+        } catch (IOException ioe) {
+            if (out != null) try { out.close(); } catch (IOException e) {}
+            _log.error("Error adding " + hostname, ioe);
+            return false;
+        } finally { releaseWriteLock(); }
+    }
+
+    /** 
+     *  @param hostname case-sensitive; caller should convert to lower case
+     *  @param options ignored
+     */
+    @Override
+    public boolean remove(String hostname, Properties options) {
+        BufferedReader in = null;
+        BufferedWriter out = null;
+        if (!getWriteLock())
+            return false;
+        try {
+            if (!_file.exists())
+                return false;
+            if (_isClosed)
+                return false;
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+            File tmp = SecureFile.createTempFile("temp-", ".tmp", _file.getAbsoluteFile().getParentFile());
+            out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8"));
+            String line = null;
+            String search = hostname + '=';
+            boolean success = false;
+            while ( (line = in.readLine()) != null) {
+                if (line.startsWith(search)) {
+                    success = true;
+                    continue;
+                }
+                out.write(line);
+                out.newLine();
+            }
+            in.close();
+            out.close();
+            if (!success) {
+                tmp.delete();
+                return false;
+            }
+            success = rename(tmp, _file);
+            if (success) {
+                for (NamingServiceListener nsl : _listeners) { 
+                    nsl.entryRemoved(this, hostname);
+                }
+            }
+            return success;
+        } catch (IOException ioe) {
+            if (in != null) try { in.close(); } catch (IOException e) {}
+            if (out != null) try { out.close(); } catch (IOException e) {}
+            _log.error("Error removing " + hostname, ioe);
+            return false;
+        } finally {
+            releaseWriteLock();
+        }
+    }
+
+    /**
+     * @param options As follows:
+     *                Key "search": return only those matching substring
+     *                Key "startsWith": return only those starting with
+     *                                  ("[0-9]" allowed)
+     */
+    @Override
+    public Map<String, Destination> getEntries(Properties options) {
+        if (!_file.exists())
+            return Collections.EMPTY_MAP;
+        String searchOpt = null;
+        String startsWith = null;
+        if (options != null) {
+            searchOpt = options.getProperty("search");
+            startsWith = options.getProperty("startsWith");
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Searching " + " starting with " + startsWith + " search string " + searchOpt);
+        BufferedReader in = null;
+        getReadLock();
+        try {
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+            String line = null;
+            Map<String, Destination> rv = new HashMap();
+            while ( (line = in.readLine()) != null) {
+                if (line.length() <= 0)
+                    continue;
+                if (startsWith != null) {
+                    if (startsWith.equals("[0-9]")) {
+                        if (line.charAt(0) < '0' || line.charAt(0) > '9')
+                            continue;
+                    } else if (!line.startsWith(startsWith)) {
+                        continue;
+                    }
+                }
+                if (line.startsWith("#"))
+                    continue;
+                if (line.indexOf('#') > 0)  // trim off any end of line comment
+                    line = line.substring(0, line.indexOf('#')).trim();
+                int split = line.indexOf('=');
+                if (split <= 0)
+                    continue;
+                String key = line.substring(0, split);
+                if (searchOpt != null && key.indexOf(searchOpt) < 0)
+                    continue;
+                String b64 = line.substring(split+1);   //.trim() ??????????????
+                try {
+                    Destination dest = new Destination(b64);
+                    rv.put(key, dest);
+                } catch (DataFormatException dfe) {}
+            }
+            if (searchOpt == null && startsWith == null) {
+                _lastWrite = _file.lastModified();
+                _size = rv.size();
+            }
+            return rv;
+        } catch (IOException ioe) {
+            _log.error("getEntries error", ioe);
+            return Collections.EMPTY_MAP;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            releaseReadLock();
+        }
+    }
+
+    /** 
+     *  @param options ignored
+     */
+    @Override
+    public int size(Properties options) {
+        if (!_file.exists())
+            return 0;
+        BufferedReader in = null;
+        getReadLock();
+        try {
+            if (_file.lastModified() <= _lastWrite)
+                return _size;
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+            String line = null;
+            int rv = 0;
+            while ( (line = in.readLine()) != null) {
+                if (line.startsWith("#") || line.length() <= 0)
+                    continue;
+                rv++;
+            }
+            _lastWrite = _file.lastModified();
+            _size = rv;
+            return rv;
+        } catch (IOException ioe) {
+            _log.error("size() error", ioe);
+            return -1;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            releaseReadLock();
+        }
+    }
+
+    public void shutdown() {
+        if (!getWriteLock())
+            return;
+        try {
+            _isClosed = true;
+        } finally {
+            releaseWriteLock();
+        }
+    }
+
+    private static boolean rename(File from, File to) {
+        boolean success = false;
+        boolean isWindows = System.getProperty("os.name").startsWith("Win");
+        // overwrite fails on windows
+        if (!isWindows)
+            success = from.renameTo(to);
+        if (!success) {
+            to.delete();
+            success = from.renameTo(to);
+            if (!success) {
+                // hard way
+                success = FileUtil.copy(from.getAbsolutePath(), to.getAbsolutePath(), true, true);
+                from.delete();
+            }
+        }
+        return success;
+    }
+
+    private void getReadLock() {
+        _fileLock.readLock().lock();
+    }
+
+    private void releaseReadLock() {
+        _fileLock.readLock().unlock();
+    }
+
+    /** @return true if the lock was acquired */
+    private boolean getWriteLock() {
+        try {
+            boolean rv = _fileLock.writeLock().tryLock(10000, TimeUnit.MILLISECONDS);
+            if ((!rv) && _log.shouldLog(Log.WARN))
+                _log.warn("no lock, size is: " + _fileLock.getQueueLength(), new Exception("rats"));
+            return rv;
+        } catch (InterruptedException ie) {}
+        return false;
+    }
+
+    private void releaseWriteLock() {
+        _fileLock.writeLock().unlock();
+    }
+
+    public static void main(String[] args) {
+        NamingService ns = new SingleFileNamingService(I2PAppContext.getGlobalContext(), "hosts.txt");
+        Destination d = new Destination();
+        try {
+            d.readBytes(new byte[387], 0);
+        } catch (DataFormatException dfe) {}
+        boolean b = ns.put("aaaaa", d);
+        System.out.println("Test 1 pass? " + b);
+        b = ns.put("bbbbb", d);
+        System.out.println("Test 2 pass? " + b);
+        b = ns.remove("aaaaa");
+        System.out.println("Test 3 pass? " + b);
+        b = ns.lookup("aaaaa") == null;
+        System.out.println("Test 4 pass? " + b);
+        b = ns.lookup("bbbbb") != null;
+        System.out.println("Test 5 pass? " + b);
+        b = !ns.putIfAbsent("bbbbb", d);
+        System.out.println("Test 6 pass? " + b);
+    }
+}
diff --git a/core/java/src/net/metanotion/README-I2P.txt b/core/java/src/net/metanotion/README-I2P.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4106a143a5a4ac9cb7b15aa51ad96402b7e6f629
--- /dev/null
+++ b/core/java/src/net/metanotion/README-I2P.txt
@@ -0,0 +1,33 @@
+Version 0.1.1 from http://www.metanotion.net/software/sandbox/block.html
+
+License: See any source file.
+
+Changes for i2p:
+
+- BSkipList has an option to not keep everything in memory.
+  When this option is enabled, we use the new IBSkipSpan instead of
+  BSkipSpan. IBSkipSpan has the following changes:
+   * Only the first key in the span, and no values, are stored in memory
+   * put() and remove() read the span keys and values in from disk first
+   * flush() nulls out the keys and values after flushing to disk
+   * get() does a linear search through the keys on disk
+
+- The metaIndex is stored in-memory. All "user" skiplists are not
+  stored in-memory.
+
+- Default span size changed from 127 to 16
+
+- Use I2P random source
+
+- Return the previous SkipList if still open from a call to getIndex()
+
+- Add a closeIndex() method
+
+- Commented out some System.out.println()
+
+- Convert Errors without message or cause to RuntimeExceptions with a message and cause
+
+
+TODO:
+
+- Change PAGESIZE from default 1024 to 4096? No, wastes too much disk.
diff --git a/core/java/src/net/metanotion/io/RAIFile.java b/core/java/src/net/metanotion/io/RAIFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..c947a7291d43092933b46a830eda4ccb45e38727
--- /dev/null
+++ b/core/java/src/net/metanotion/io/RAIFile.java
@@ -0,0 +1,136 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+public class RAIFile implements RandomAccessInterface, DataInput, DataOutput {
+	private File f;
+	private RandomAccessFile delegate;
+	private boolean r=false, w=false;
+
+	public RAIFile(RandomAccessFile file) throws FileNotFoundException {
+		this.f = null;
+		this.delegate = file;
+	}
+
+	public RAIFile(File file, boolean read, boolean write) throws FileNotFoundException {
+		this.f = file;
+		this.r = read;
+		this.w = write;
+		String mode = "";
+		if(this.r) { mode += "r"; }
+		if(this.w) { mode += "w"; }
+		this.delegate = new RandomAccessFile(file, mode);
+	}
+
+	public long getFilePointer()		throws IOException { return delegate.getFilePointer(); }
+	public long length()				throws IOException { return delegate.length(); }
+	public int read()					throws IOException { return delegate.read(); }
+	public int read(byte[] b)			throws IOException { return delegate.read(b); }
+	public int read(byte[] b, int off, int len) throws IOException { return delegate.read(b,off,len); }
+	public void seek(long pos)			throws IOException { delegate.seek(pos); }
+	public void setLength(long newLength) throws IOException { delegate.setLength(newLength); }
+
+	// Closeable Methods
+	// TODO May need to change.
+	public void close()					throws IOException { delegate.close(); }
+
+	// DataInput Methods
+	public boolean readBoolean()		throws IOException { return delegate.readBoolean(); }
+	public byte readByte()				throws IOException { return delegate.readByte(); }
+	public char readChar()				throws IOException { return delegate.readChar(); }
+	public double readDouble()			throws IOException { return delegate.readDouble(); }
+	public float readFloat()			throws IOException { return delegate.readFloat(); }
+	public void readFully(byte[] b)		throws IOException { delegate.readFully(b); }
+	public void readFully(byte[] b, int off, int len) throws IOException { delegate.readFully(b,off,len); }
+	public int readInt()				throws IOException { return delegate.readInt(); }
+	public String readLine()			throws IOException { return delegate.readLine(); }
+	public long readLong()				throws IOException { return delegate.readLong(); }
+	public short readShort()			throws IOException { return delegate.readShort(); }
+	public int readUnsignedByte()		throws IOException { return delegate.readUnsignedByte(); }
+	public int readUnsignedShort()		throws IOException { return delegate.readUnsignedShort(); }
+
+	/** Read a UTF encoded string
+	 	I would delegate here. But Java's read/writeUTF combo suck.
+	 	A signed 2 byte length is not enough.
+	 	This reads a 4 byte length.
+	 	The upper byte MUST be zero, if its not, then its not this method and has used an
+	 	extensible length encoding.
+	 	This is followed by the bytes of the UTF encoded string, as
+	 	returned by String.getBytes("UTF-8");
+	*/
+	public String readUTF()				throws IOException {
+		int len = delegate.readInt();
+		if((len < 0) || (len >= 16777216)) { throw new IOException("Bad Length Encoding"); }
+		byte[] bytes = new byte[len];
+		int l = delegate.read(bytes);
+		if(l==-1) { throw new IOException("EOF while reading String"); }
+		String s = new String(bytes, "UTF-8");
+		return s;
+	}
+
+	public int skipBytes(int n)			throws IOException { return delegate.skipBytes(n); }
+
+	// DataOutput Methods
+	public void write(int b)			throws IOException { delegate.write(b); }
+	public void write(byte[] b)			throws IOException { delegate.write(b); }
+	public void write(byte[] b, int off, int len) throws IOException { delegate.write(b,off,len); }
+	public void writeBoolean(boolean v)	throws IOException { delegate.writeBoolean(v); }
+	public void writeByte(int v)		throws IOException { delegate.writeByte(v); }
+	public void writeShort(int v)		throws IOException { delegate.writeShort(v); }
+	public void writeChar(int v)		throws IOException { delegate.writeChar(v); }
+	public void writeInt(int v)			throws IOException {  delegate.writeInt(v); }
+	public void writeLong(long v)		throws IOException {  delegate.writeLong(v); }
+	public void writeFloat(float v)		throws IOException { delegate.writeFloat(v); }
+	public void writeDouble(double v)	throws IOException { delegate.writeDouble(v); }
+	public void writeBytes(String s)	throws IOException { delegate.writeBytes(s); }
+	public void writeChars(String s)	throws IOException { delegate.writeChars(s); }
+
+	/** Write a UTF encoded string
+	 	I would delegate here. But Java's read/writeUTF combo suck.
+	 	A signed 2 byte length is not enough.
+	 	This writes a 4 byte length.
+	 	The upper byte MUST be zero, if its not, then its not this method and has used an
+	 	extensible length encoding.
+	 	This is followed by the bytes of the UTF encoded string, as
+	 	returned by String.getBytes("UTF-8");
+	*/
+	public void writeUTF(String str)	throws IOException {
+		byte[] string = str.getBytes("UTF-8");
+		if(string.length >= 16777216) { throw new IOException("String to long for encoding type"); }
+		delegate.writeInt(string.length);
+		delegate.write(string);
+	}
+}
diff --git a/core/java/src/net/metanotion/io/RandomAccessInterface.java b/core/java/src/net/metanotion/io/RandomAccessInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..227e36c66618478af4ca730da990798e77c87825
--- /dev/null
+++ b/core/java/src/net/metanotion/io/RandomAccessInterface.java
@@ -0,0 +1,77 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io;
+
+import java.io.IOException;
+
+public interface RandomAccessInterface {
+	public long getFilePointer() throws IOException;
+	public long length() throws IOException;
+	public int read() throws IOException;
+	public int read(byte[] b) throws IOException;
+	public int read(byte[] b, int off, int len) throws IOException;
+	public void seek(long pos) throws IOException;
+	public void setLength(long newLength) throws IOException;
+
+	// Closeable Methods
+	public void close() throws IOException;
+
+	// DataInput Methods
+	public boolean readBoolean() throws IOException;
+	public byte readByte() throws IOException;
+	public char readChar() throws IOException;
+	public double readDouble() throws IOException;
+	public float readFloat() throws IOException;
+	public void readFully(byte[] b) throws IOException;
+	public void readFully(byte[] b, int off, int len) throws IOException;
+	public int readInt() throws IOException;
+	public String readLine() throws IOException;
+	public long readLong() throws IOException;
+	public short readShort() throws IOException;
+	public int readUnsignedByte() throws IOException;
+	public int readUnsignedShort() throws IOException;
+	public String readUTF() throws IOException;
+	public int skipBytes(int n) throws IOException;
+
+	// DataOutput Methods
+	public void write(int b) throws IOException;
+	public void write(byte[] b) throws IOException;
+	public void write(byte[] b, int off, int len) throws IOException;
+	public void writeBoolean(boolean v) throws IOException;
+	public void writeByte(int v) throws IOException;
+	public void writeShort(int v) throws IOException;
+	public void writeChar(int v) throws IOException;
+	public void writeInt(int v) throws IOException;
+	public void writeLong(long v) throws IOException;
+	public void writeFloat(float v) throws IOException;
+	public void writeDouble(double v) throws IOException;
+	public void writeBytes(String s) throws IOException;
+	public void writeChars(String s) throws IOException;
+	public void writeUTF(String str) throws IOException;
+}
diff --git a/core/java/src/net/metanotion/io/SerialStreams.java b/core/java/src/net/metanotion/io/SerialStreams.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8c430e93a08657b7f0a9aa55486192865ae6e64
--- /dev/null
+++ b/core/java/src/net/metanotion/io/SerialStreams.java
@@ -0,0 +1,62 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import net.metanotion.io.Serializer;
+
+public abstract class SerialStreams implements Serializer {
+	public byte[] getBytes(Object o) {
+		try {
+			ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			DataOutputStream dos = new DataOutputStream(baos);
+			writeOut(dos, o);
+			return baos.toByteArray();
+		} catch (IOException ioe) { throw new Error(); }
+	}
+
+	public Object construct(byte[] b) {
+		try {
+			ByteArrayInputStream bais = new ByteArrayInputStream(b);
+			DataInputStream dis = new DataInputStream(bais);
+			return readIn(dis);
+		} catch (IOException ioe) {
+			ioe.printStackTrace();
+			throw new Error();
+		}
+	}
+
+	abstract public void writeOut(DataOutputStream dos, Object o) throws IOException;
+	abstract public Object readIn(DataInputStream dis) throws IOException;
+}
diff --git a/core/java/src/net/metanotion/io/Serializer.java b/core/java/src/net/metanotion/io/Serializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..40cab225524ed172b9d3ca55b00fb1aa30cbf011
--- /dev/null
+++ b/core/java/src/net/metanotion/io/Serializer.java
@@ -0,0 +1,34 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io;
+
+public interface Serializer {
+	public byte[] getBytes(Object o);
+	public Object construct(byte[] b);
+}
diff --git a/core/java/src/net/metanotion/io/block/BlockFile.java b/core/java/src/net/metanotion/io/block/BlockFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..12f46b34c2134de00e63a0d628debc97c89fb08d
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/BlockFile.java
@@ -0,0 +1,314 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+import net.metanotion.io.RAIFile;
+import net.metanotion.io.RandomAccessInterface;
+import net.metanotion.io.Serializer;
+import net.metanotion.io.data.IntBytes;
+import net.metanotion.io.data.LongBytes;
+import net.metanotion.io.data.NullBytes;
+import net.metanotion.io.data.StringBytes;
+
+import net.metanotion.io.block.index.BSkipList;
+import net.metanotion.util.skiplist.SkipList;
+
+class CorruptFileException extends IOException { }
+class BadFileFormatException extends IOException { }
+class BadVersionException extends IOException { }
+
+public class BlockFile {
+	public static final long PAGESIZE = 1024;
+	public static final long OFFSET_MOUNTED = 20;
+
+	public RandomAccessInterface file;
+
+	private long magicBytes = 0x3141deadbeef0100L;
+	private long fileLen = PAGESIZE * 2;
+	private int freeListStart = 0;
+	private short mounted = 0;
+	public short spanSize = 16;
+
+	private BSkipList metaIndex = null;
+	private HashMap openIndices = new HashMap();
+
+	private void mount() throws IOException {
+		file.seek(BlockFile.OFFSET_MOUNTED);
+		mounted = 1;
+		file.writeShort(mounted);
+	}
+
+	private void writeSuperBlock() throws IOException {
+		file.seek(0);
+		file.writeLong(	magicBytes);
+		file.writeLong(	fileLen);
+		file.writeInt(	freeListStart);
+		file.writeShort(mounted);
+		file.writeShort(spanSize);
+	}
+
+	private void readSuperBlock() throws IOException {
+		file.seek(0);
+		magicBytes		= file.readLong();
+		fileLen			= file.readLong();
+		freeListStart	= file.readInt();
+		mounted			= file.readShort();
+		spanSize		= file.readShort();
+	}
+
+	public static void main(String args[]) {
+		try {
+			RAIFile raif = new RAIFile(new File(args[0]), true, true);
+			BlockFile bf = new BlockFile(raif, true);
+
+			//bf.metaIndex.delete();
+			bf.makeIndex("foo", new NullBytes(), new NullBytes());
+
+
+			BSkipList b = bf.getIndex("foo", new NullBytes(), new NullBytes());
+			System.out.println(bf.allocPage());
+
+			bf.close();
+			raif.close();
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	public int writeMultiPageData(byte[] data, int page, int[] curPageOff, int[] nextPage) throws IOException {
+		int pageCounter = curPageOff[0];
+		int curNextPage = nextPage[0];
+		int curPage = page;
+		int dct = 0;
+		while(dct < data.length) {
+			int len = ((int) BlockFile.PAGESIZE) - pageCounter;
+			if(len <= 0) {
+				if(curNextPage==0) {
+					curNextPage = this.allocPage();
+					BlockFile.pageSeek(this.file, curNextPage);
+					this.file.writeInt(0);
+					BlockFile.pageSeek(this.file, curPage);
+					this.file.writeInt(curNextPage);
+				}
+				BlockFile.pageSeek(this.file, curNextPage);
+				curPage = curNextPage;
+				curNextPage = this.file.readInt();
+				pageCounter = 4;
+				len = ((int) BlockFile.PAGESIZE) - pageCounter;
+			}
+			this.file.write(data, dct, Math.min(len, data.length - dct));
+			pageCounter += Math.min(len, data.length - dct);
+			dct += Math.min(len, data.length - dct);
+		}
+		nextPage[0] = curNextPage;
+		curPageOff[0] = pageCounter;
+		return curPage;
+	}
+
+	public int readMultiPageData(byte[] arr, int page, int[] curPageOff, int[] nextPage) throws IOException {
+		int pageCounter = curPageOff[0];
+		int curNextPage = nextPage[0];
+		int curPage = page;
+		int dct = 0;
+		int res;
+		while(dct < arr.length) {
+			int len = ((int) BlockFile.PAGESIZE) - pageCounter;
+			if(len <= 0) {
+				BlockFile.pageSeek(this.file, curNextPage);
+				curPage = curNextPage;
+				curNextPage = this.file.readInt();
+				pageCounter = 4;
+				len = ((int) BlockFile.PAGESIZE) - pageCounter;
+			}
+			res = this.file.read(arr, dct, Math.min(len, arr.length - dct));
+			if(res == -1) { throw new IOException(); }
+			pageCounter += Math.min(len, arr.length - dct);
+			dct += res;
+		}
+		nextPage[0] = curNextPage;
+		curPageOff[0] = pageCounter;
+		return curPage;
+	}
+
+	public BlockFile(RandomAccessInterface rai) throws IOException { this(rai, false); }
+	public BlockFile(RandomAccessFile raf) throws IOException { this(new RAIFile(raf), false); }
+	public BlockFile(RandomAccessFile raf, boolean init) throws IOException { this(new RAIFile(raf), init); }
+	public BlockFile(File f, boolean init) throws IOException { this(new RAIFile(f, true, true), init); }
+
+	public BlockFile(RandomAccessInterface rai, boolean init) throws IOException {
+		if(rai==null) { throw new NullPointerException(); }
+		
+		file = rai;
+
+		if(init) {
+			file.setLength(fileLen);
+			writeSuperBlock();
+			BSkipList.init(this, 2, spanSize);
+		}
+
+		readSuperBlock();
+		if(magicBytes != 0x3141deadbeef0100L) {
+			if((magicBytes & 0x3141deadbeef0000L) == 0x3141deadbeef0000L) {
+				throw new BadVersionException();
+			} else {
+				throw new BadFileFormatException();
+			}
+		}
+//		if(mounted != 0) { throw new CorruptFileException(); }
+		if(fileLen != file.length()) { throw new CorruptFileException(); }
+		mount();
+
+		metaIndex = new BSkipList(spanSize, this, 2, new StringBytes(), new IntBytes());
+	}
+
+
+	public static void pageSeek(RandomAccessInterface file, int page) throws IOException { file.seek((((long)page) - 1L) * BlockFile.PAGESIZE ); }
+
+	public int allocPage() throws IOException {
+		if(freeListStart != 0) {
+			FreeListBlock flb = new FreeListBlock(file, freeListStart);
+			if(flb.len > 0) {
+				flb.len = flb.len - 1;
+				int page = flb.branches[flb.len];
+				flb.writeBlock();
+				return page;
+			} else {
+				freeListStart = flb.nextPage;
+				writeSuperBlock();
+				return flb.page;
+			}
+		}
+		long offset = file.length();
+		fileLen = offset + BlockFile.PAGESIZE;
+		file.setLength(fileLen);
+		writeSuperBlock();
+		return ((int) ((long) (offset / BlockFile.PAGESIZE))) + 1;
+	}
+
+	public void freePage(int page) throws IOException {
+		System.out.println("Free Page " + page);
+		if(freeListStart == 0) {
+			freeListStart = page;
+			FreeListBlock.initPage(file, page);
+			writeSuperBlock();
+			return;
+		}
+		FreeListBlock flb = new FreeListBlock(file, freeListStart);
+		if(flb.isFull()) {
+			FreeListBlock.initPage(file, page);
+			if(flb.nextPage == 0) {
+				flb.nextPage = page;
+				flb.writeBlock();
+				return;
+			} else {
+				flb = new FreeListBlock(file, page);
+				flb.nextPage = freeListStart;
+				flb.writeBlock();
+				freeListStart = page;
+				writeSuperBlock();
+				return;
+			}
+		}
+		flb.addPage(page);
+		flb.writeBlock();
+	}
+
+	public BSkipList getIndex(String name, Serializer key, Serializer val) throws IOException {
+		// added I2P
+		BSkipList bsl = (BSkipList) openIndices.get(name);
+		if (bsl != null)
+			return bsl;
+
+		Integer page = (Integer) metaIndex.get(name);
+		if (page == null) { return null; }
+		bsl = new BSkipList(spanSize, this, page.intValue(), key, val, true);
+		openIndices.put(name, bsl);
+		return bsl;
+	}
+
+	public BSkipList makeIndex(String name, Serializer key, Serializer val) throws IOException {
+		if(metaIndex.get(name) != null) { throw new IOException("Index already exists"); }
+		int page = allocPage();
+		metaIndex.put(name, new Integer(page));
+		BSkipList.init(this, page, spanSize);
+		BSkipList bsl = new BSkipList(spanSize, this, page, key, val, true);
+		openIndices.put(name, bsl);
+		return bsl;
+	}
+
+	public void delIndex(String name) throws IOException {
+		Integer page = (Integer) metaIndex.remove(name);
+		if (page == null) { return; }
+		NullBytes nb = new NullBytes();
+		BSkipList bsl = new BSkipList(spanSize, this, page.intValue(), nb, nb, true);
+		bsl.delete();
+	}
+
+	/**
+	 *  Added I2P
+	 */
+	public void closeIndex(String name) {
+		BSkipList bsl = (BSkipList) openIndices.remove(name);
+		if (bsl != null)
+			bsl.flush();
+	}
+
+	/**
+	 *  Note (I2P)
+         *  Does NOT close the RAF / RAI.
+	 */
+	public void close() throws IOException {
+		// added I2P
+		if (metaIndex == null)
+			return;
+
+		metaIndex.close();
+		metaIndex = null;
+
+		Set oi = openIndices.keySet();
+		Iterator i = oi.iterator();
+		Object k;
+		while(i.hasNext()) {
+			k = i.next();
+			BSkipList bsl = (BSkipList) openIndices.get(k);
+			bsl.close();
+		}
+
+		// Unmount.
+		file.seek(BlockFile.OFFSET_MOUNTED);
+		file.writeShort(0);
+	}
+}
diff --git a/core/java/src/net/metanotion/io/block/FreeListBlock.java b/core/java/src/net/metanotion/io/block/FreeListBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..aec2a933490495d2444d29214d4cd02e1adfe739
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/FreeListBlock.java
@@ -0,0 +1,89 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block;
+
+import java.io.IOException;
+
+import net.metanotion.io.RandomAccessInterface;
+
+public class FreeListBlock {
+	public int page;
+	public int nextPage;
+	public int len;
+	public int[] branches = null;
+	public RandomAccessInterface file;
+
+	public FreeListBlock(RandomAccessInterface file, int startPage) throws IOException {
+		this.file = file;
+		this.page = startPage;
+		BlockFile.pageSeek(file, startPage);
+		nextPage = file.readInt();
+		len = file.readInt();
+		if(len > 0) {
+			branches = new int[len];
+			for(int i=0;i<len;i++) {
+				branches[i] = file.readInt();
+			}
+		}
+	}
+
+	public void writeBlock() throws IOException {
+		BlockFile.pageSeek(file, page);
+		file.writeInt(nextPage);
+		if(len > 0) {
+			file.writeInt(len);
+			for(int i=0;i<len;i++) { file.writeInt(branches[i]); }
+		} else {
+			file.writeInt(0);
+		}
+	}
+
+	public boolean isFull() {
+		int cells = (int) ((BlockFile.PAGESIZE - 8) / 4);
+		if(cells - len > 0) { return false; }
+		return true;
+	}
+
+	public void addPage(int page) {
+		int[] t = new int[len + 1];
+		if(len > 0) {
+			for(int i=0;i<len;i++) { t[i] = branches[i]; }
+		}
+		t[len] = page;
+		len++;
+		branches = t;
+	}
+
+	public static void initPage(RandomAccessInterface file, int page) throws IOException {
+		BlockFile.pageSeek(file, page);
+		file.writeInt(0);
+		file.writeInt(0);
+	}
+}
+
diff --git a/core/java/src/net/metanotion/io/block/index/BSkipLevels.java b/core/java/src/net/metanotion/io/block/index/BSkipLevels.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e42b0c7c6fae0f584b4f7b2113524660e98e4c2
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/index/BSkipLevels.java
@@ -0,0 +1,112 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block.index;
+
+import java.io.IOException;
+
+import net.metanotion.io.RandomAccessInterface;
+import net.metanotion.io.block.BlockFile;
+import net.metanotion.util.skiplist.SkipList;
+import net.metanotion.util.skiplist.SkipLevels;
+import net.metanotion.util.skiplist.SkipSpan;
+
+public class BSkipLevels extends SkipLevels {
+	public int levelPage;
+	public int spanPage;
+	public BlockFile bf;
+
+	protected BSkipLevels() { }
+	public BSkipLevels(BlockFile bf, int levelPage, BSkipList bsl) throws IOException {
+		this.levelPage = levelPage;
+		this.bf = bf;
+
+		BlockFile.pageSeek(bf.file, levelPage);
+
+		bsl.levelHash.put(new Integer(this.levelPage), this);
+
+		int maxLen = bf.file.readShort();
+		int nonNull = bf.file.readShort();
+		spanPage = bf.file.readInt();
+		bottom = (BSkipSpan) bsl.spanHash.get(new Integer(spanPage));
+
+		this.levels = new BSkipLevels[maxLen];
+		int lp;
+		for(int i=0;i<nonNull;i++) {
+			lp = bf.file.readInt();
+			if(lp != 0) {
+				levels[i] = (BSkipLevels) bsl.levelHash.get(new Integer(lp));
+				if(levels[i] == null) {
+					levels[i] = new BSkipLevels(bf, lp, bsl);
+					bsl.levelHash.put(new Integer(lp), levels[i]);
+				}
+			} else {
+				levels[i] = null;
+			}
+		}
+
+	}
+
+	public static void init(BlockFile bf, int page, int spanPage, int maxHeight) throws IOException {
+		BlockFile.pageSeek(bf.file, page);
+		bf.file.writeShort((short) maxHeight);
+		bf.file.writeShort(0);
+		bf.file.writeInt(spanPage);
+	}
+
+	public void flush() {
+		try {
+			BlockFile.pageSeek(bf.file, levelPage);
+			bf.file.writeShort((short) levels.length);
+			int i=0;
+			for(i=0;i<levels.length;i++) { if(levels[i] == null) { break; } }
+			bf.file.writeShort(i);
+			bf.file.writeInt(((BSkipSpan) bottom).page);
+			for(i=0;i<levels.length;i++) {
+				if(levels[i]==null) { break; }
+				bf.file.writeInt(((BSkipLevels) levels[i]).levelPage);
+			}
+		} catch (IOException ioe) { throw new RuntimeException("Error writing to database", ioe); }
+	}
+
+	public void killInstance() {
+		try {
+			bf.freePage(levelPage);
+		} catch (IOException ioe) { throw new RuntimeException("Error freeing database page", ioe); }
+	}
+
+	public SkipLevels newInstance(int levels, SkipSpan ss, SkipList sl) {
+		try {
+			BSkipSpan bss = (BSkipSpan) ss;
+			BSkipList bsl = (BSkipList) sl;
+			int page = bf.allocPage();
+			BSkipLevels.init(bf, page, bss.page, levels);
+			return new BSkipLevels(bf, page, bsl);
+		} catch (IOException ioe) { throw new RuntimeException("Error creating database page", ioe); }
+	}
+}
diff --git a/core/java/src/net/metanotion/io/block/index/BSkipList.java b/core/java/src/net/metanotion/io/block/index/BSkipList.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7c8741e073b1f0b5492e658557c196b52990731
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/index/BSkipList.java
@@ -0,0 +1,161 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block.index;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Random;
+
+import net.metanotion.io.RandomAccessInterface;
+import net.metanotion.io.Serializer;
+import net.metanotion.io.block.BlockFile;
+import net.metanotion.util.skiplist.*;
+
+public class BSkipList extends SkipList {
+	public int firstSpanPage = 0;
+	public int firstLevelPage = 0;
+	public int skipPage = 0;
+	public BlockFile bf;
+
+	public HashMap spanHash = new HashMap();
+	public HashMap levelHash = new HashMap();
+
+	private final boolean fileOnly;
+
+	public BSkipList(int spanSize, BlockFile bf, int skipPage, Serializer key, Serializer val) throws IOException {
+		this(spanSize, bf, skipPage, key, val, false);
+	}
+
+	public BSkipList(int spanSize, BlockFile bf, int skipPage, Serializer key, Serializer val, boolean fileOnly) throws IOException {
+		if(spanSize < 1) { throw new RuntimeException("Span size too small"); }
+
+		this.skipPage = skipPage;
+		this.bf = bf;
+
+		BlockFile.pageSeek(bf.file, skipPage);
+		firstSpanPage = bf.file.readInt();
+		firstLevelPage = bf.file.readInt();
+		size = bf.file.readInt();
+		spans = bf.file.readInt();
+		//System.out.println(size + " " + spans); 
+
+		this.fileOnly = fileOnly;
+		if (fileOnly)
+			first = new IBSkipSpan(bf, this, firstSpanPage, key, val);
+		else
+			first = new BSkipSpan(bf, this, firstSpanPage, key, val);
+		stack = new BSkipLevels(bf, firstLevelPage, this);
+		//rng = new Random(System.currentTimeMillis());
+	}
+
+	public void close() {
+		//System.out.println("Closing index " + size + " and " + spans);
+		flush();
+		first = null;
+		stack = null;
+	}
+
+	public void flush() {
+		try {
+			BlockFile.pageSeek(bf.file, skipPage);
+			bf.file.writeInt(firstSpanPage);
+			bf.file.writeInt(firstLevelPage);
+			bf.file.writeInt(size);
+			bf.file.writeInt(spans);
+			
+		} catch (IOException ioe) { throw new RuntimeException("Error writing to database", ioe); }
+	}
+
+	public void delete() throws IOException {
+		SkipLevels curLevel = stack, nextLevel;
+		while(curLevel != null) {
+			nextLevel = curLevel.levels[0];
+			curLevel.killInstance();
+			curLevel = nextLevel;
+		}
+
+		SkipSpan curSpan = first, nextSpan;
+		while(curSpan != null) {
+			nextSpan = curSpan.next;
+			curSpan.killInstance();
+			curSpan = nextSpan;
+		}
+
+		bf.freePage(skipPage);
+	}
+
+	public static void init(BlockFile bf, int page, int spanSize) throws IOException {
+		int firstSpan = bf.allocPage();
+		int firstLevel = bf.allocPage();
+		BlockFile.pageSeek(bf.file, page);
+		bf.file.writeInt(firstSpan);
+		bf.file.writeInt(firstLevel);
+		bf.file.writeInt(0);
+		bf.file.writeInt(1);
+		BSkipSpan.init(bf, firstSpan, spanSize);
+		BSkipLevels.init(bf, firstLevel, firstSpan, 4);
+	}
+
+	public int maxLevels() {
+		int max = super.maxLevels();
+		int cells = (int) ((BlockFile.PAGESIZE - 8) / 4);
+		return (max > cells) ? cells : max;
+	}
+
+	@Override
+	public SkipIterator iterator() {
+		if (!this.fileOnly)
+			return super.iterator();
+		return new IBSkipIterator(first, 0);
+	}
+
+	@Override
+	public SkipIterator min() {
+		return iterator();
+	}
+
+	@Override
+	public SkipIterator max() {
+		if (!this.fileOnly)
+			return super.max();
+		SkipSpan ss = stack.getEnd();
+		return new IBSkipIterator(ss, ss.nKeys - 1);
+	}
+
+	@Override
+	public SkipIterator find(Comparable key) {
+		if (!this.fileOnly)
+			return super.find(key);
+		int[] search = new int[1];
+		SkipSpan ss = stack.getSpan(stack.levels.length - 1, key, search);
+		if(search[0] < 0) { search[0] = -1 * (search[0] + 1); }
+		return new IBSkipIterator(ss, search[0]);
+	}
+
+}
diff --git a/core/java/src/net/metanotion/io/block/index/BSkipSpan.java b/core/java/src/net/metanotion/io/block/index/BSkipSpan.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ade78b0282dda9676eca7eb6f02b160fb155860
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/index/BSkipSpan.java
@@ -0,0 +1,233 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block.index;
+
+import java.io.IOException;
+
+import net.metanotion.io.RandomAccessInterface;
+import net.metanotion.io.Serializer;
+import net.metanotion.io.block.BlockFile;
+import net.metanotion.io.data.NullBytes;
+import net.metanotion.util.skiplist.SkipList;
+import net.metanotion.util.skiplist.SkipSpan;
+
+public class BSkipSpan extends SkipSpan {
+
+	protected BlockFile bf;
+	protected int page;
+	protected int overflowPage;
+
+	protected int prevPage;
+	protected int nextPage;
+	protected Serializer keySer;
+	protected Serializer valSer;
+
+	// I2P
+	protected int spanSize;
+
+	public static void init(BlockFile bf, int page, int spanSize) throws IOException {
+		BlockFile.pageSeek(bf.file, page);
+		bf.file.writeInt(0);
+		bf.file.writeInt(0);
+		bf.file.writeInt(0);
+		bf.file.writeShort((short) spanSize);
+		bf.file.writeShort(0);
+	}
+
+	public SkipSpan newInstance(SkipList sl) {
+		try {
+			int newPage = bf.allocPage();
+			init(bf, newPage, bf.spanSize);
+			return new BSkipSpan(bf, (BSkipList) sl, newPage, keySer, valSer);
+		} catch (IOException ioe) { throw new RuntimeException("Error creating database page", ioe); }
+	}
+
+	public void killInstance() {
+		try {
+			int curPage = overflowPage;
+			int next;
+			while(curPage != 0) {
+				BlockFile.pageSeek(bf.file, curPage);
+				next = bf.file.readInt();
+				bf.freePage(curPage);
+				curPage = next;
+			}
+			bf.freePage(page);
+		} catch (IOException ioe) { throw new RuntimeException("Error freeing database page", ioe); }
+	}
+
+	public void flush() {
+		try {
+			BlockFile.pageSeek(bf.file, page);
+			bf.file.writeInt(overflowPage);
+			bf.file.writeInt((prev != null) ? ((BSkipSpan) prev).page : 0);
+			bf.file.writeInt((next != null) ? ((BSkipSpan) next).page : 0);
+			bf.file.writeShort((short) keys.length);
+			bf.file.writeShort((short) nKeys);
+
+			int ksz, vsz;
+			int curPage = this.page;
+			int[] curNextPage = new int[1];
+			curNextPage[0] = this.overflowPage;
+			int[] pageCounter = new int[1];
+			pageCounter[0] = 16;
+			byte[] keyData;
+			byte[] valData;
+
+			for(int i=0;i<nKeys;i++) {
+				if((pageCounter[0] + 4) > BlockFile.PAGESIZE) {
+					if(curNextPage[0] == 0) {
+						curNextPage[0] = bf.allocPage();
+						BlockFile.pageSeek(bf.file, curNextPage[0]);
+						bf.file.writeInt(0);
+						BlockFile.pageSeek(bf.file, curPage);
+						bf.file.writeInt(curNextPage[0]);
+					}
+					BlockFile.pageSeek(bf.file, curNextPage[0]);
+					curPage = curNextPage[0];
+					curNextPage[0] = bf.file.readInt();
+					pageCounter[0] = 4;
+				}
+				keyData = this.keySer.getBytes(keys[i]);
+				valData = this.valSer.getBytes(vals[i]);
+				pageCounter[0] += 4;
+				bf.file.writeShort(keyData.length);
+				bf.file.writeShort(valData.length);
+				curPage = bf.writeMultiPageData(keyData, curPage, pageCounter, curNextPage);
+				curPage = bf.writeMultiPageData(valData, curPage, pageCounter, curNextPage);
+			}
+			BlockFile.pageSeek(bf.file, this.page);
+			this.overflowPage = bf.file.readInt();
+		} catch (IOException ioe) { throw new RuntimeException("Error writing to database", ioe); }
+	}
+
+	private static void load(BSkipSpan bss, BlockFile bf, BSkipList bsl, int spanPage, Serializer key, Serializer val) throws IOException {
+		loadInit(bss, bf, bsl, spanPage, key, val);
+		bss.loadData();
+	}
+
+	/**
+	 * I2P - first half of load()
+	 * Only read the span headers
+	 */
+	protected static void loadInit(BSkipSpan bss, BlockFile bf, BSkipList bsl, int spanPage, Serializer key, Serializer val) throws IOException {
+		bss.bf = bf;
+		bss.page = spanPage;
+		bss.keySer = key;
+		bss.valSer = val;
+
+		bsl.spanHash.put(new Integer(spanPage), bss);
+
+		BlockFile.pageSeek(bf.file, spanPage);
+
+		bss.overflowPage = bf.file.readInt();
+		bss.prevPage = bf.file.readInt();
+		bss.nextPage = bf.file.readInt();
+		bss.spanSize = bf.file.readShort();
+		bss.nKeys = bf.file.readShort();
+	}
+
+	/**
+	 * I2P - second half of load()
+	 * Load the whole span's keys and values into memory
+	 */
+	protected void loadData() throws IOException {
+		this.keys = new Comparable[this.spanSize];
+		this.vals = new Object[this.spanSize];
+
+		int ksz, vsz;
+		int curPage = this.page;
+		int[] curNextPage = new int[1];
+		curNextPage[0] = this.overflowPage;
+		int[] pageCounter = new int[1];
+		pageCounter[0] = 16;
+//		System.out.println("Span Load " + sz + " nKeys " + nKeys + " page " + curPage);
+		for(int i=0;i<this.nKeys;i++) {
+			if((pageCounter[0] + 4) > BlockFile.PAGESIZE) {
+				BlockFile.pageSeek(this.bf.file, curNextPage[0]);
+				curPage = curNextPage[0];
+				curNextPage[0] = this.bf.file.readInt();
+				pageCounter[0] = 4;
+			}
+			ksz = this.bf.file.readShort();
+			vsz = this.bf.file.readShort();
+			pageCounter[0] +=4;
+			byte[] k = new byte[ksz];
+			byte[] v = new byte[vsz];
+			curPage = this.bf.readMultiPageData(k, curPage, pageCounter, curNextPage);
+			curPage = this.bf.readMultiPageData(v, curPage, pageCounter, curNextPage);
+//			System.out.println("i=" + i + ", Page " + curPage + ", offset " + pageCounter[0] + " ksz " + ksz + " vsz " + vsz);
+			this.keys[i] = (Comparable) this.keySer.construct(k);
+			this.vals[i] = this.valSer.construct(v);
+		}
+
+	}
+
+	protected BSkipSpan() { }
+	public BSkipSpan(BlockFile bf, BSkipList bsl, int spanPage, Serializer key, Serializer val) throws IOException {
+		BSkipSpan.load(this, bf, bsl, spanPage, key, val);
+		this.next = null;
+		this.prev = null;
+
+		BSkipSpan bss = this;
+		BSkipSpan temp;
+		int np = nextPage;
+		while(np != 0) {
+			temp = (BSkipSpan) bsl.spanHash.get(new Integer(np));
+			if(temp != null) {
+				bss.next = temp;
+				break;
+			}
+			bss.next = new BSkipSpan();
+			bss.next.next = null;
+			bss.next.prev = bss;
+			bss = (BSkipSpan) bss.next;
+			
+			BSkipSpan.load(bss, bf, bsl, np, key, val);
+			np = bss.nextPage;
+		}
+
+		bss = this;
+		np = prevPage;
+		while(np != 0) {
+			temp = (BSkipSpan) bsl.spanHash.get(new Integer(np));
+			if(temp != null) {
+				bss.next = temp;
+				break;
+			}
+			bss.prev = new BSkipSpan();
+			bss.prev.next = bss;
+			bss.prev.prev = null;
+			bss = (BSkipSpan) bss.prev;
+			
+			BSkipSpan.load(bss, bf, bsl, np, key, val);
+			np = bss.prevPage;
+		}
+	}
+}
diff --git a/core/java/src/net/metanotion/io/block/index/IBSkipIterator.java b/core/java/src/net/metanotion/io/block/index/IBSkipIterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..a43df79c8361c8d0827a6f8e7ed80471ce341588
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/index/IBSkipIterator.java
@@ -0,0 +1,137 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block.index;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+import net.metanotion.util.skiplist.SkipIterator;
+import net.metanotion.util.skiplist.SkipSpan;
+
+/**
+	I2P
+	Overridden to load the span when required and null out the keys and values
+	when the iterator leaves the span.
+	If the caller does not iterate all the way through, the last span
+	will remain in memory.
+*/
+public class IBSkipIterator extends SkipIterator {
+
+	public IBSkipIterator(SkipSpan ss, int index) {
+		super(ss, index);
+	}
+
+	/**
+	 * @return the next value, and advances the index
+	 * @throws NoSuchElementException
+	 * @throws RuntimeException on IOE
+	 */
+	@Override
+	public Object next() {
+		Object o;
+		if(index < ss.nKeys) {
+			if (ss.vals == null) {
+				try {
+					((IBSkipSpan)ss).seekAndLoadData();
+				} catch (IOException ioe) {
+					throw new RuntimeException("Error in iterator", ioe);
+				}
+			}
+			o = ss.vals[index];
+		} else {
+			throw new NoSuchElementException();
+		}
+
+		if(index < (ss.nKeys-1)) {
+			index++;
+		} else if(ss.next != null) {
+			ss.keys = null;
+			ss.vals = null;
+			ss = ss.next;
+			index = 0;
+		} else {
+			ss.keys = null;
+			ss.vals = null;
+			index = ss.nKeys;
+		}
+		return o;
+	}
+
+	/**
+	 * The key. Does NOT advance the index.
+	 * @return the key for which the value will be returned in the subsequent call to next()
+	 * @throws NoSuchElementException
+	 * @throws RuntimeException on IOE
+	 */
+	@Override
+	public Comparable nextKey() {
+		Comparable c;
+		if(index < ss.nKeys) {
+			if (ss.keys == null) {
+				try {
+					((IBSkipSpan)ss).seekAndLoadData();
+				} catch (IOException ioe) {
+					throw new RuntimeException("Error in iterator", ioe);
+				}
+			}
+			return ss.keys[index];
+		}
+		throw new NoSuchElementException();
+	}
+
+	/**
+	 * @return the previous value, and decrements the index
+	 * @throws NoSuchElementException
+	 * @throws RuntimeException on IOE
+	 */
+	@Override
+	public Object previous() {
+		if(index > 0) {
+			index--;
+		} else if(ss.prev != null) {
+			ss.keys = null;
+			ss.vals = null;
+			ss = ss.prev;
+			if(ss.nKeys <= 0) { throw new NoSuchElementException(); }
+			index = (ss.nKeys - 1);
+		} else {
+			ss.keys = null;
+			ss.vals = null;
+			throw new NoSuchElementException();
+		}
+		if (ss.vals == null) {
+			try {
+				((IBSkipSpan)ss).seekAndLoadData();
+			} catch (IOException ioe) {
+				throw new RuntimeException("Error in iterator", ioe);
+			}
+		}
+		return ss.vals[index];
+	}
+}
diff --git a/core/java/src/net/metanotion/io/block/index/IBSkipSpan.java b/core/java/src/net/metanotion/io/block/index/IBSkipSpan.java
new file mode 100644
index 0000000000000000000000000000000000000000..315b180de749441e424ffe32d41cf3a63db6e31e
--- /dev/null
+++ b/core/java/src/net/metanotion/io/block/index/IBSkipSpan.java
@@ -0,0 +1,304 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.block.index;
+
+import java.io.IOException;
+
+import net.metanotion.io.Serializer;
+import net.metanotion.io.block.BlockFile;
+import net.metanotion.util.skiplist.SkipList;
+import net.metanotion.util.skiplist.SkipSpan;
+
+/**
+ * I2P version of BSkipSpan
+ *
+ * BSkipSpan stores all keys and values in-memory, backed by the file.
+ * IBSkipSpan stores only the first key, and no values, in-memory.
+ *
+ * For a get(), here we do a linear search through the span in the file 
+ * and load only the found value (super() does a binary search in-memory).
+ *
+ * For a put() or remove(), we load all keys and values for the span from
+ * the file, make the modification, flush() out the keys and values,
+ * and null out the keys and values in-memory.
+ *
+ * Recommended span size is 16.
+ *
+ * @author zzz
+ */
+public class IBSkipSpan extends BSkipSpan {
+
+	private Comparable firstKey;
+	private static final boolean DEBUG = false;
+
+	@Override
+	public SkipSpan newInstance(SkipList sl) {
+		if (DEBUG)
+			System.err.println("Splitting page " + this.page + " containing " + this.nKeys + '/' + this.spanSize);
+		try {
+			int newPage = bf.allocPage();
+			init(bf, newPage, bf.spanSize);
+			SkipSpan rv = new IBSkipSpan(bf, (BSkipList) sl, newPage, keySer, valSer);
+			// this is called after a split, so we need the data arrays initialized
+			rv.keys = new Comparable[bf.spanSize];
+			rv.vals = new Object[bf.spanSize];
+			return rv;
+		} catch (IOException ioe) { throw new RuntimeException("Error creating database page", ioe); }
+	}
+
+	/**
+	 * Flush to disk and null out in-memory keys and values, saving only the first key
+	 */
+	@Override
+	public void flush() {
+		super.flush();
+		if (nKeys > 0)
+			this.firstKey = keys[0];
+		else
+			this.firstKey = null;
+		this.keys = null;
+		this.vals = null;
+		if (DEBUG)
+			System.err.println("Flushed data for page " + this.page + " containing " + this.nKeys + '/' + this.spanSize);
+	}
+
+	/**
+	 * I2P - second half of load()
+	 * Load the whole span's keys and values into memory
+	 */
+	@Override
+	protected void loadData() throws IOException {
+		super.loadData();
+		if (this.nKeys > 0)
+			this.firstKey = this.keys[0];
+		if (DEBUG)
+			System.err.println("Loaded data for page " + this.page + " containing " + this.nKeys + '/' + this.spanSize + " first key: " + this.firstKey);
+	}
+
+	/**
+	 * Must already be seeked to the end of the span header
+         * via loadInit() or seekData()
+	 */
+	private void loadFirstKey() throws IOException {
+		if (this.nKeys <= 0)
+			return;
+		int ksz;
+		int curPage = this.page;
+		int[] curNextPage = new int[1];
+		curNextPage[0] = this.overflowPage;
+		int[] pageCounter = new int[1];
+		pageCounter[0] = 16;
+		ksz = this.bf.file.readShort();
+		this.bf.file.skipBytes(2);  //vsz
+		pageCounter[0] +=4;
+		byte[] k = new byte[ksz];
+		curPage = this.bf.readMultiPageData(k, curPage, pageCounter, curNextPage);
+		this.firstKey = (Comparable) this.keySer.construct(k);
+		if (DEBUG)
+			System.err.println("Loaded header for page " + this.page + " containing " + this.nKeys + '/' + this.spanSize + " first key: " + this.firstKey);
+	}
+
+	/**
+	 * Seek past the span header
+	 */
+	private void seekData() throws IOException {
+		BlockFile.pageSeek(this.bf.file, this.page);
+		// 3 ints and 2 shorts
+		this.bf.file.skipBytes(16);
+	}
+
+	/**
+	 * Seek to the start of the span and load the data
+	 * Package private so BSkipIterator can call it
+	 */
+	void seekAndLoadData() throws IOException {
+		seekData();
+		loadData();
+	}
+
+	/**
+	 * Linear search through the span in the file for the value.
+	 */
+	private Object getData(Comparable key) throws IOException {
+		seekData();
+		int ksz, vsz;
+		int curPage = this.page;
+		int[] curNextPage = new int[1];
+		curNextPage[0] = this.overflowPage;
+		int[] pageCounter = new int[1];
+		pageCounter[0] = 16;
+		//System.out.println("Span Load " + sz + " nKeys " + nKeys + " page " + curPage);
+		for(int i=0;i<this.nKeys;i++) {
+			if((pageCounter[0] + 4) > BlockFile.PAGESIZE) {
+				BlockFile.pageSeek(this.bf.file, curNextPage[0]);
+				curPage = curNextPage[0];
+				curNextPage[0] = this.bf.file.readInt();
+				pageCounter[0] = 4;
+			}
+			ksz = this.bf.file.readShort();
+			vsz = this.bf.file.readShort();
+			pageCounter[0] +=4;
+			byte[] k = new byte[ksz];
+			byte[] v = new byte[vsz];
+			curPage = this.bf.readMultiPageData(k, curPage, pageCounter, curNextPage);
+			curPage = this.bf.readMultiPageData(v, curPage, pageCounter, curNextPage);
+			//System.out.println("i=" + i + ", Page " + curPage + ", offset " + pageCounter[0] + " ksz " + ksz + " vsz " + vsz);
+			Comparable ckey = (Comparable) this.keySer.construct(k);
+			int diff = ckey.compareTo(key);
+			if (diff == 0) {
+				//System.err.println("Found " + key + " at " + i + " (first: " + this.firstKey + ')');
+				return this.valSer.construct(v);
+			}
+			if (diff > 0) {
+				//System.err.println("NOT Found " + key + " at " + i + " (first: " + this.firstKey + " current: " + ckey + ')');
+				return null;
+			}
+		}
+		//System.err.println("NOT Found " + key + " at end (first: " + this.firstKey + ')');
+		return null;
+	}
+
+	protected IBSkipSpan() { }
+
+	public IBSkipSpan(BlockFile bf, BSkipList bsl, int spanPage, Serializer key, Serializer val) throws IOException {
+		if (DEBUG)
+			System.err.println("New ibss page " + spanPage);
+		BSkipSpan.loadInit(this, bf, bsl, spanPage, key, val);
+		loadFirstKey();
+		this.next = null;
+		this.prev = null;
+
+		IBSkipSpan bss = this;
+		IBSkipSpan temp;
+		int np = nextPage;
+		while(np != 0) {
+			temp = (IBSkipSpan) bsl.spanHash.get(new Integer(np));
+			if(temp != null) {
+				bss.next = temp;
+				break;
+			}
+			bss.next = new IBSkipSpan();
+			bss.next.next = null;
+			bss.next.prev = bss;
+			bss = (IBSkipSpan) bss.next;
+			
+			BSkipSpan.loadInit(bss, bf, bsl, np, key, val);
+			bss.loadFirstKey();
+			np = bss.nextPage;
+		}
+
+		bss = this;
+		np = prevPage;
+		while(np != 0) {
+			temp = (IBSkipSpan) bsl.spanHash.get(new Integer(np));
+			if(temp != null) {
+				bss.next = temp;
+				break;
+			}
+			bss.prev = new IBSkipSpan();
+			bss.prev.next = bss;
+			bss.prev.prev = null;
+			bss = (IBSkipSpan) bss.prev;
+			
+			BSkipSpan.loadInit(bss, bf, bsl, np, key, val);
+			bss.loadFirstKey();
+			np = bss.prevPage;
+		}
+	}
+
+	/**
+         * Does not call super, we always store first key here
+	 */
+	@Override
+	public Comparable firstKey() {
+		return this.firstKey;
+	}
+
+	/**
+	 * Load whole span from file, do the operation, flush out, then null out in-memory data again.
+	 * This is called only via SkipList.find()
+	 */
+	@Override
+	public SkipSpan getSpan(Comparable key, int[] search) {
+		try {
+			seekAndLoadData();
+		} catch (IOException ioe) {
+			throw new RuntimeException("Error reading database", ioe);
+		}
+		SkipSpan rv = super.getSpan(key, search);
+		this.keys = null;
+		this.vals = null;
+		return rv;
+	}
+
+	/**
+	 * Linear search if in file, Binary search if in memory
+	 */
+	@Override
+	public Object get(Comparable key) {
+		try {
+			if (nKeys == 0) { return null; }
+			if (this.next != null && this.next.firstKey().compareTo(key) <= 0)
+				return next.get(key);
+			return getData(key);
+		} catch (IOException ioe) {
+			throw new RuntimeException("Error reading database", ioe);
+		}
+	}
+
+	/**
+	 * Load whole span from file, do the operation, flush out, then null out in-memory data again.
+	 */
+	@Override
+	public SkipSpan put(Comparable key, Object val, SkipList sl)	{
+		try {
+			seekAndLoadData();
+		} catch (IOException ioe) {
+			throw new RuntimeException("Error reading database", ioe);
+		}
+		SkipSpan rv = super.put(key, val, sl);
+		// flush() nulls out the data
+		return rv;
+	}
+
+	/**
+	 * Load whole span from file, do the operation, flush out, then null out in-memory data again.
+	 */
+	@Override
+	public Object[] remove(Comparable key, SkipList sl) {
+		try {
+			seekAndLoadData();
+		} catch (IOException ioe) {
+			throw new RuntimeException("Error reading database", ioe);
+		}
+		Object[] rv = super.remove(key, sl);
+		// flush() nulls out the data
+		return rv;
+	}
+}
diff --git a/core/java/src/net/metanotion/io/data/IntBytes.java b/core/java/src/net/metanotion/io/data/IntBytes.java
new file mode 100644
index 0000000000000000000000000000000000000000..489ae296cbf3567a1fae55459e11086ae4b4608d
--- /dev/null
+++ b/core/java/src/net/metanotion/io/data/IntBytes.java
@@ -0,0 +1,51 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.data;
+
+import net.metanotion.io.Serializer;
+
+public class IntBytes implements Serializer {
+	public byte[] getBytes(Object o) {
+		byte[] b = new byte[4];
+		int v = ((Integer) o).intValue();
+ 		b[0] = (byte)(0xff & (v >> 24));
+ 		b[1] = (byte)(0xff & (v >> 16));
+		b[2] = (byte)(0xff & (v >>  8));
+ 		b[3] = (byte)(0xff & v);
+ 		return b;
+	}
+
+	public Object construct(byte[] b) {
+		int v = (((int)(b[0] & 0xff) << 24) |
+				 ((int)(b[1] & 0xff) << 16) |
+				 ((int)(b[2] & 0xff) <<  8) |
+				 ((int)(b[3] & 0xff)));
+		return new Integer(v);
+	}
+}
diff --git a/core/java/src/net/metanotion/io/data/LongBytes.java b/core/java/src/net/metanotion/io/data/LongBytes.java
new file mode 100644
index 0000000000000000000000000000000000000000..1223a8799e3fd144a52bdde874d009e8e79bcf26
--- /dev/null
+++ b/core/java/src/net/metanotion/io/data/LongBytes.java
@@ -0,0 +1,59 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.data;
+
+import net.metanotion.io.Serializer;
+
+public class LongBytes implements Serializer {
+	public byte[] getBytes(Object o) {
+		byte[] b = new byte[8];
+		long v = ((Long) o).longValue();
+ 		b[0] = (byte)(0xff & (v >> 56));
+		b[1] = (byte)(0xff & (v >> 48));
+ 		b[2] = (byte)(0xff & (v >> 40));
+ 		b[3] = (byte)(0xff & (v >> 32));
+ 		b[4] = (byte)(0xff & (v >> 24));
+ 		b[5] = (byte)(0xff & (v >> 16));
+		b[6] = (byte)(0xff & (v >>  8));
+ 		b[7] = (byte)(0xff & v);
+ 		return b;
+	}
+
+	public Object construct(byte[] b) {
+		long v =(((long)(b[0] & 0xff) << 56) |
+				 ((long)(b[1] & 0xff) << 48) |
+				 ((long)(b[2] & 0xff) << 40) |
+				 ((long)(b[3] & 0xff) << 32) |
+				 ((long)(b[4] & 0xff) << 24) |
+				 ((long)(b[5] & 0xff) << 16) |
+				 ((long)(b[6] & 0xff) <<  8) |
+				 ((long)(b[7] & 0xff)));
+		return new Long(v);
+	}
+}
diff --git a/core/java/src/net/metanotion/io/data/NullBytes.java b/core/java/src/net/metanotion/io/data/NullBytes.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3849b0ed588bbf9d31c25efa4f93ce2135a387c
--- /dev/null
+++ b/core/java/src/net/metanotion/io/data/NullBytes.java
@@ -0,0 +1,36 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.data;
+
+import net.metanotion.io.Serializer;
+
+public class NullBytes implements Serializer {
+	public byte[] getBytes(Object o) { return null; }
+	public Object construct(byte[] b) { return null; }
+}
diff --git a/core/java/src/net/metanotion/io/data/StringBytes.java b/core/java/src/net/metanotion/io/data/StringBytes.java
new file mode 100644
index 0000000000000000000000000000000000000000..18740b7b5a942c677de3e96fe29e86f4ed6a0a5a
--- /dev/null
+++ b/core/java/src/net/metanotion/io/data/StringBytes.java
@@ -0,0 +1,47 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.io.data;
+
+import java.io.UnsupportedEncodingException;
+
+import net.metanotion.io.Serializer;
+
+public class StringBytes implements Serializer {
+	public byte[] getBytes(Object o) {
+		try {
+			return ((String) o).getBytes("US-ASCII");
+		} catch (UnsupportedEncodingException uee) { throw new Error("Unsupported Encoding"); }
+	}
+
+	public Object construct(byte[] b) {
+		try {
+			return new String(b, "US-ASCII");
+		} catch (UnsupportedEncodingException uee) { throw new Error("Unsupported Encoding"); }
+	}
+}
diff --git a/core/java/src/net/metanotion/package.html b/core/java/src/net/metanotion/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..82e22b737a5028ab3e9b1cc7e177360ae2f87646
--- /dev/null
+++ b/core/java/src/net/metanotion/package.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<link rel="home" title="Home" href="http://www.metanotion.net/software/sandbox/" />
+
+		<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
+		<meta name="robots" content="all" />
+
+		<title>BlockFile</title>
+	</head>
+	<body>
+		<h1>Metanotion BlockFile Database</h1>
+		<p>A 100% Java 1.3, BSD Licensed, embeddable single file database engine in 32KB. This database was designed for PDA based and J2ME applications.</p>
+
+		<h2>Table of Contents</h2>
+		<ul>
+			<li><a href="#features">Features</a></li>
+			<li><a href="#unfeatures">Unfeatures</a></li>
+			<li><a href="#future">Future Plans</a></li>
+			<li><a href="#design">What kind of database is this?</a></li>
+			<li><a href="#examples">Examples and API</a></li>
+			<li><a href="#download">Download</a></li>
+		</ul>
+		
+		<a name="features"><h2>Features</h2></a>
+		<ul>
+			<li>100% Java 1.3. No JNI.</li>
+			<li>Will work with any "file" as long as you can approximate something like <a href="http://java.sun.com/j2se/1.3/docs/api/java/io/RandomAccessFile.html">java.io.RandomAccessFile</a>, you can use this.</li>
+			<li>BSD Licensed. Yes, this means you can use it for free in a commercial project. However, if you base some really cool mobile technology startup on this code we'll gladly accept stock options...</p>
+			<li>No dependence on file API's(useful for mobile apps)</li>
+			<li>Small. 32KB in a JAR file. &lt;2000 lines of code.</li>
+			<li>Reasonably fast. This is used in an app running on a sub 200MHz StrongARM PocketPC, and quite handily deals with 70,000 records. The load time is a little slow, but its been tested with a <a href="http://java.sun.com/javame/reference/apis.jsp">CDC 1.0/Personal Profile</a> device.
+			</li>
+		</ul>
+
+		<a name="unfeatures"><h2>Unfeatures</h2></a>
+		<p>A good, ACID database is a nice thing to work with. Unfortunately, in the goal to make this small, fast, and work with minimal dependencies, something had to give. So I list things which this database will likely never have. Of course, since it is BSD Licensed, patches welcome...</p>
+		
+		<ul>
+			<li>No transactions.</li>
+			<li>No SQL.</li>
+			<li>No JDBC.</li>
+			<li>No use of reflection or automagical serialization tricks.</li>
+		</ul>
+
+		<a name="future"><h2>Future Plans</h2></a>
+		<p>There are still bugs(none known...). The app that this was written for is still in testing, but we should most of the issues sorted by the time we deploy it in a few weeks(early November, 2006). Some loading speed issues on large record sets, and memory usage could still be improved. All this and feedback from other uses will direct this products evolution.</p>
+		<p>What is currently up here is not "1.0" code, but we will release a labeled "1.0" version once we feel happy with the state of the codebase.</p>
+	
+		<a name="design"><h2>What KIND of database is this?</h2></a>
+		<p>You probably store at least part of your application data in memory in a class from the <a href="http://java.sun.com/j2se/1.4.2/docs/guide/collections/">Java Collections Framework</a>. The BlockFile database stores data in a <a href="http://en.wikipedia.org/wiki/Skip_list">Skip</a> <a href="http://eternallyconfuzzled.com/tuts/skip.html">List</a> that almost implements <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/SortedMap.html">java.util.SortedMap</a>. You can create and store as many named(with a string) SkipList in the database as you want.</p>
+		<p>To serialize your data, you have to either extend our SerialStreams class or implement our Serializer interface. We could have done something cool and fancy with reflection(and other cool stuff with Java 1.5), but that would probably not do the Right Thing&trade; most of the time.  As you can see, there's not a lot to it anyway:</p>
+		<h3>net.metanotion.io.SerialStreams</h3>
+<pre>
+public abstract class SerialStreams implements Serializer {
+// ...
+	abstract public void writeOut(DataOutputStream dos, Object o) throws IOException;
+	abstract public Object readIn(DataInputStream dis) throws IOException;
+}
+</pre>
+		<h3>net.metanotion.io.Serializer</h3>
+<pre>
+public interface Serializer {
+	public byte[] getBytes(Object o);
+	public Object construct(byte[] b);
+}
+</pre>
+
+		<p>Now, about those skip lists. They implement a <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/ListIterator.html">java.util.ListIterator</a> so you can get "nearby" values, and you can use anything for a key that implements <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Comparable.html">java.lang.Comparable</a>. So, here's the interface to a SkipList:
+<pre>
+public class SkipList {
+	...
+	public void put(Comparable key, Object val) ...
+	public Object remove(Comparable key) ...
+	public Object get(Comparable key) ...
+	public ListIterator iterator() ...
+	public ListIterator min() ...
+	public ListIterator max() ...
+	// Find the first key bigger than or equal to key,
+	// or the biggest key less than key if there is no bigger or equal.
+	public ListIterator find(Comparable key) ...
+}
+</pre>
+</p>
+
+		<a name="examples"><h2>Examples</h2></a>
+		<p>Better documentation is forthcoming, but there really isn't much to know. The entire public interface to the library is on this page. Where possible, it sticks to idiomatic Java and standard interfaces.</p>
+		<ul>
+			<li>Open a database:
+<pre>
+import net.metanotion.io.block.BlockFile;
+...
+	try {
+		BlockFile db = new BlockFile(new File("my.db"), false); // true will create
+	} catch (IOException ioe) {
+		System.out.println("Bummer");
+	}
+</pre>
+			<li>
+			<li>Load or Create a SkipList:
+<pre>
+import net.metanotion.util.skiplist.SkipList;
+import net.metanotion.io.Serializer;
+...
+class KeySerializer implements Serializer ...
+class ValueSerializer implements Serializer ...
+...
+// Open preexisting
+SkipList index = db.getIndex("My Index", new KeySerializer(), new ValueSerializer());
+// Create
+SkipList index = db.makeIndex("My Index", new KeySerializer(), new ValueSerializer());
+</pre>
+			</li>
+		</ul>
+
+		<h3>net.metanotion.io.block.BlockFile</h3>
+All the public interface methods:
+<pre>
+public class BlockFile implements Closeable {
+	public BlockFile(RandomAccessInterface rai) ...
+	public BlockFile(RandomAccessFile raf) ...
+	public BlockFile(RandomAccessFile raf, boolean init) ...
+	public BlockFile(File f, boolean init) ...
+	public BlockFile(RandomAccessInterface rai, boolean init) ...
+
+	public SkipList getIndex(String name, Serializer key, Serializer val) ...
+	public SkipList makeIndex(String name, Serializer key, Serializer val) ...
+	public void delIndex(String name) ...
+
+	public void close() ...
+}
+</pre>
+
+		<h3>What's this "net.metanotion.io.RandomAccessInterface"?</h3>
+		<p>Basically, its an interface version of <a href="http://java.sun.com/j2se/1.3/docs/api/java/io/RandomAccessFile.html">java.io.RandomAccessFile</a>(which itself implements <a href="http://java.sun.com/j2se/1.3/docs/api/java/io/DataInput.html">DataInput</a>, <a href="http://java.sun.com/j2se/1.3/docs/api/java/io/DataOutput.html">DataOutput</a> and a few methods for getting/setting the file pointer).</p>
+		
+		<p>So, in other words, if you can provide an implementation of this interface, you can use the BlockFile database. This frees it from dependence on the RandomAccessFile class. If you don't see why this is useful and you're going to be using "files" on PDA's and phone's, well, you'll understand soon enough...</p>
+
+		<a name="download"><h2>Download</h2></a>
+		<h3>Bugfix and cleanup Release 10/6/2006</h2>
+		<p>An unnecessary class was removed, some junk methods removed, and a couple of JDK compatability issues were fixed. The StringBytes class was switched to ASCII(from UTF-8) for better compatibility.</p>
+		<ul>
+			<li><a href="BlockFile.2006.10.06.jar">BlockFile binary JAR, version 0.1.1</a></li>
+			<li><a href="BlockFile.src.2006.10.06.zip">BlockFile source code</a></li>
+		</ul>
+		
+		<h3>Initial Release 9/28/2006</h3>
+		<ul>
+			<li><a href="BlockFile.2006.09.28.jar">BlockFile binary JAR, version 0.1</a></li>
+			<li><a href="BlockFile.src.2006.09.28.zip">BlockFile source code</a></li>
+		</ul>
+
+		<hr />
+		<center>&copy; 2006 <a href="http://www.metanotion.net/">Metanotion Software</a></center>
+	</body>
+</html>
\ No newline at end of file
diff --git a/core/java/src/net/metanotion/util/skiplist/SkipIterator.java b/core/java/src/net/metanotion/util/skiplist/SkipIterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc723050c3cb1b05ffa7c01cee08431836312ca0
--- /dev/null
+++ b/core/java/src/net/metanotion/util/skiplist/SkipIterator.java
@@ -0,0 +1,121 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.util.skiplist;
+
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+
+/**	A basic iterator for a skip list.
+ 	This is not a complete ListIterator, in particular, since the
+ 	skip list is a map and is therefore indexed by Comparable objects instead
+ 	of int's, the nextIndex and previousIndex methods are not really relevant.
+
+	To be clear, this is an iterator through the values.
+	To get the key, call nextKey() BEFORE calling next().
+*/
+public class SkipIterator implements ListIterator {
+	protected SkipSpan ss;
+	protected int index;
+
+	protected SkipIterator() { }
+	public SkipIterator(SkipSpan ss, int index) {
+		if(ss==null) { throw new NullPointerException(); }
+		this.ss = ss;
+		this.index = index;
+	}
+
+	public boolean hasNext() {
+		if(index < ss.nKeys) { return true; }
+		return false;
+	}
+
+	/**
+	 * @return the next value, and advances the index
+	 * @throws NoSuchElementException
+	 */
+	public Object next() {
+		Object o;
+		if(index < ss.nKeys) {
+			o = ss.vals[index];
+		} else {
+			throw new NoSuchElementException();
+		}
+
+		if(index < (ss.nKeys-1)) {
+			index++;
+		} else if(ss.next != null) {
+			ss = ss.next;
+			index = 0;
+		} else {
+			index = ss.nKeys;
+		}
+		return o;
+	}
+
+	/**
+         * The key. Does NOT advance the index.
+	 * @return the key for which the value will be returned in the subsequent call to next()
+	 * @throws NoSuchElementException
+	 */
+	public Comparable nextKey() {
+		Comparable c;
+		if(index < ss.nKeys) { return ss.keys[index]; }
+		throw new NoSuchElementException();
+	}
+
+	public boolean hasPrevious() {
+		if(index > 0) { return true; }
+		if((ss.prev != null) && (ss.prev.nKeys > 0)) { return true; }
+		return false;
+	}
+
+	/**
+	 * @return the previous value, and decrements the index
+	 * @throws NoSuchElementException
+	 */
+	public Object previous() {
+		if(index > 0) {
+			index--;
+		} else if(ss.prev != null) {
+			ss = ss.prev;
+			if(ss.nKeys <= 0) { throw new NoSuchElementException(); }
+			index = (ss.nKeys - 1);
+		}
+		return ss.vals[index];
+	}
+
+
+	// Optional methods
+	public void add(Object o)	{ throw new UnsupportedOperationException(); }
+	public void remove()		{ throw new UnsupportedOperationException(); }
+	public void set(Object o)	{ throw new UnsupportedOperationException(); }
+	public int nextIndex()		{ throw new UnsupportedOperationException(); }
+	public int previousIndex()	{ throw new UnsupportedOperationException(); }
+
+}
diff --git a/core/java/src/net/metanotion/util/skiplist/SkipLevels.java b/core/java/src/net/metanotion/util/skiplist/SkipLevels.java
new file mode 100644
index 0000000000000000000000000000000000000000..e837127df5b0a647cdf9947b9346c15c9f62ff37
--- /dev/null
+++ b/core/java/src/net/metanotion/util/skiplist/SkipLevels.java
@@ -0,0 +1,168 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.util.skiplist;
+
+public class SkipLevels {
+	/*	"Next" pointers
+		The highest indexed level is the "highest" level in the list.
+		The "bottom" level is the direct pointer to a SkipSpan.
+	*/
+	public SkipLevels[] levels;
+	public SkipSpan bottom;
+
+	public SkipLevels newInstance(int levels, SkipSpan ss, SkipList sl) { return new SkipLevels(levels, ss); }
+	public void killInstance() { }
+	public void flush() { }
+
+	protected SkipLevels() { }
+	public SkipLevels(int size, SkipSpan span) {
+		if(size < 1) { throw new Error("Invalid Level Skip size"); }
+		levels = new SkipLevels[size];
+		bottom = span;
+	}
+
+	public void print() {
+		SkipLevels prev = null;
+		SkipLevels max = null;
+		System.out.print("SL:" + key() + "::");
+		for(int i=0;i<levels.length;i++) {
+			if(levels[i] != null) {
+				max = levels[i];
+				System.out.print(i + "->" + levels[i].key() + " ");
+			} else {
+				System.out.print(i + "->() ");
+			}
+		}
+		System.out.print("\n");
+		if(levels[0] != null) {
+			levels[0].print();
+		}
+	}
+
+	public SkipSpan getEnd() {
+		for(int i=(levels.length - 1);i>=0;i--) {
+			if(levels[i] != null) { return levels[i].getEnd(); }
+		}
+		return bottom.getEnd();
+	}
+
+	public SkipSpan getSpan(int start, Comparable key, int[] search) {
+		for(int i=Math.min(start, levels.length - 1);i>=0;i--) {
+			if((levels[i] != null) && (levels[i].key().compareTo(key) <= 0)) {
+				return levels[i].getSpan(i,key,search);
+			}
+		}
+		return bottom.getSpan(key, search);
+	}
+
+	public Comparable key() { return bottom.firstKey(); }
+
+	public Object get(int start, Comparable key) {
+		for(int i=Math.min(start, levels.length - 1);i>=0;i--) {
+			if((levels[i] != null) && (levels[i].key().compareTo(key) <= 0)) {
+				return levels[i].get(i,key);
+			}
+		}
+		return bottom.get(key);
+	}
+
+	public Object[] remove(int start, Comparable key, SkipList sl) {
+		SkipSpan ss = null;
+		Object[] res = null;
+		SkipLevels slvls = null;
+		for(int i=Math.min(start, levels.length - 1);i>=0;i--) {
+			if(levels[i] != null) {
+				int cmp = levels[i].key().compareTo(key);
+				if((cmp < 0) || ((i==0) && (cmp <= 0)))  {
+					res = levels[i].remove(i, key, sl);
+					if((res != null) && (res[1] != null)) {
+						slvls = (SkipLevels) res[1];
+						if(levels.length >= slvls.levels.length) { res[1] = null; }
+						for(int j=0;j<(Math.min(slvls.levels.length,levels.length));j++) {
+							if(levels[j] == slvls) {
+								levels[j] = slvls.levels[j];
+							}
+						}
+						this.flush();
+					}
+					return res;
+				}
+			}
+		}
+		res = bottom.remove(key, sl);
+		if((res!=null) && (res[1] != null)) {
+			if(res[1] == bottom) {
+				res[1] = this;
+			} else {
+				res[1] = null;
+			}
+		}
+		if((bottom.nKeys == 0) && (sl.first != bottom)) { this.killInstance(); }
+		return res;
+	}
+
+	public SkipLevels put(int start, Comparable key, Object val, SkipList sl) {
+		SkipSpan ss = null;
+		SkipLevels slvls = null;
+		for(int i=Math.min(start, levels.length - 1);i>=0;i--) {
+			if((levels[i] != null) && (levels[i].key().compareTo(key) <= 0)) {
+				slvls = levels[i].put(i, key, val, sl);
+				if(slvls != null) {
+					for(int j=i+1;j<(Math.min(slvls.levels.length,levels.length));j++) {
+						slvls.levels[j] = levels[j];
+						levels[j] = slvls;
+					}
+					if(levels.length < slvls.levels.length) {
+						this.flush();
+						return slvls;
+					}
+				}
+				this.flush();
+				return null;
+			}
+		}
+		ss = bottom.put(key,val,sl);
+		if(ss!=null) {
+			int height = sl.generateColHeight();
+			if(height != 0) {
+				slvls = this.newInstance(height, ss, sl);
+				for(int i=0;i<(Math.min(height,levels.length));i++) {
+					slvls.levels[i] = levels[i];
+					levels[i] = slvls;
+				}
+			}
+			this.flush();
+			if(levels.length >= height) { return null; }
+			return slvls;
+		} else {
+			return null;
+		}
+	}
+}
+
diff --git a/core/java/src/net/metanotion/util/skiplist/SkipList.java b/core/java/src/net/metanotion/util/skiplist/SkipList.java
new file mode 100644
index 0000000000000000000000000000000000000000..66982982bf108254fdeec578e3a46d8e7447a2ee
--- /dev/null
+++ b/core/java/src/net/metanotion/util/skiplist/SkipList.java
@@ -0,0 +1,321 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.util.skiplist;
+
+import java.util.Random;
+
+import net.i2p.util.RandomSource;
+
+public class SkipList {
+	protected SkipSpan first;
+	protected SkipLevels stack;
+	// I2P mod
+	public static final Random rng = RandomSource.getInstance();
+
+	public int size=0;
+	public int spans=0;
+
+	public void flush() { }
+	protected SkipList() { }
+
+	public SkipList(int span) {
+		if(span < 1) { throw new Error("Span size too small"); }
+		first = new SkipSpan(span);
+		stack = new SkipLevels(1, first);
+		spans = 1;
+		//rng = new Random(System.currentTimeMillis());
+	}
+
+	public int size() { return size; }
+
+	public int maxLevels() {
+		int hob = 0, s = spans;
+		while(spans > 0) {
+			hob++;
+			spans = spans / 2;
+		}
+		return (hob > 4) ? hob : 4;
+	}
+
+	public int generateColHeight() {
+		int bits = rng.nextInt();
+		boolean cont = true;
+		int res=0;
+		for(res=0; cont; res++) {
+			cont = ((bits % 2) == 0) ? true : false;
+			bits = bits / 2;
+		}
+		return Math.max(0, Math.min(res, maxLevels()));
+	}
+
+	public void put(Comparable key, Object val)	{
+		if(key == null) { throw new NullPointerException(); }
+		if(val == null) { throw new NullPointerException(); }
+		SkipLevels slvls = stack.put(stack.levels.length - 1, key, val, this);
+		if(slvls != null) {
+			SkipLevels[] levels = new SkipLevels[slvls.levels.length];
+			for(int i=0;i < slvls.levels.length; i++) {
+				if(i < stack.levels.length) {
+					levels[i] = stack.levels[i];
+				} else {
+					levels[i] = slvls;
+				}
+			}
+			stack.levels = levels;
+			stack.flush();
+			flush();
+		}
+	}
+
+	public Object remove(Comparable key) {
+		if(key == null) { throw new NullPointerException(); }
+		Object[] res = stack.remove(stack.levels.length - 1, key, this);
+		if(res != null) {
+			if(res[1] != null) {
+				SkipLevels slvls = (SkipLevels) res[1];
+				for(int i=0;i < slvls.levels.length; i++) {
+					if(stack.levels[i] == slvls) {
+						stack.levels[i] = slvls.levels[i];
+					}
+				}
+				stack.flush();
+			}
+			flush();
+			return res[0];
+		}
+		return null;
+	}
+
+	public void printSL() {
+		System.out.println("List size " + size + " spans " + spans);
+		stack.print();
+	}
+
+	public void print() {
+		System.out.println("List size " + size + " spans " + spans);
+		first.print();
+	}
+
+	public Object get(Comparable key) {
+		if(key == null) { throw new NullPointerException(); }
+		return stack.get(stack.levels.length - 1, key);
+	}
+
+	public SkipIterator iterator() { return new SkipIterator(first, 0); }
+
+	public SkipIterator min() { return new SkipIterator(first, 0); }
+
+	public SkipIterator max() {
+		SkipSpan ss = stack.getEnd();
+		return new SkipIterator(ss, ss.nKeys - 1);
+	}
+
+	/** @return an iterator where nextKey() is the first one greater than or equal to 'key' */
+	public SkipIterator find(Comparable key) {
+		int[] search = new int[1];
+		SkipSpan ss = stack.getSpan(stack.levels.length - 1, key, search);
+		if(search[0] < 0) { search[0] = -1 * (search[0] + 1); }
+		return new SkipIterator(ss, search[0]);
+	}
+
+
+	// Levels adjusted to guarantee O(log n) search
+	// This is expensive proportional to the number of spans.
+	public void balance() {
+		// TODO Skip List Balancing Algorithm
+	}
+
+
+
+/*
+	Basic Error generating conditions to test
+		insert into empty
+		insert into non empty
+		remove from empty
+		remove from non-empty a non-existant key
+		get from empty
+		get from non-empty a non-existant key
+
+		Repeat, with splits induced, and collapse induced.
+*/
+/*****
+	public static void main(String args[]) {
+		SkipList sl = new SkipList(3);
+		sl.put(".1", "1");
+		sl.remove("2");
+		sl.remove("1");
+		sl.put(".1", "1-1");
+		sl.put(".2", "2");
+		sl.put(".3", "3");
+*****/
+/*		System.out.println("\n#1");
+		sl.print();
+*/
+/*****
+
+		sl.put(".4", "4");
+*****/
+/*		System.out.println("\n#2");
+		sl.print();
+
+		sl.remove("1");
+		System.out.println("\n#2.1");
+		sl.print();
+		sl.remove("2");
+		System.out.println("\n#2.2");
+		sl.print();
+		sl.remove("3");
+		System.out.println("\n#2.3");
+		sl.print();
+		sl.remove("4");
+
+		System.out.println("\n#3");
+		sl.print();
+*/
+/******
+		sl.put(".1", "1-2");
+		sl.put(".2", "2-1");
+		sl.put(".3", "3-1");
+		sl.put(".4", "4-1");
+//		System.out.println("\n#4");
+//		sl.print();
+		sl.put(".5", "5-1");
+		sl.put(".6", "6-1");
+		sl.put(".7", "7-1");
+
+//		System.out.println("\n#5");
+//		sl.print();
+
+//		sl.remove("5");
+		sl.put(".5", "5-2");
+//		System.out.println("\n#6");
+//		sl.print();
+
+		sl.put(".8", "8");
+		sl.put(".9", "9");
+		sl.put("10", "10");
+		sl.put("11", "11");
+		sl.put("12", "12");
+		sl.put("13", "13");
+		sl.put("14", "14");
+		sl.put("15", "15");
+		sl.put("16", "16");
+		sl.put("17", "17");
+		sl.put("18", "18");
+		sl.put("19", "19");
+		sl.put("20", "20");
+		sl.put("21", "21");
+		sl.put("22", "22");
+		sl.put("23", "23");
+		sl.put("24", "24");
+		sl.put("25", "25");
+		sl.put("26", "26");
+		sl.put("27", "27");
+		sl.put("28", "28");
+		sl.put("29", "29");
+		sl.put("30", "30");
+		sl.put("31", "31");
+		sl.put("32", "32");
+		sl.put("33", "33");
+		sl.put("34", "34");
+		sl.put("35", "35");
+		sl.put("36", "36");
+		sl.put("37", "37");
+		sl.put("38", "38");
+		sl.put("39", "39");
+		sl.put("40", "40");
+
+//		System.out.println("\n#7");
+//		sl.print();
+		System.out.println("GET " + sl.get("10"));
+		System.out.println("GET " + sl.get("12"));
+		System.out.println("GET " + sl.get("32"));
+		System.out.println("GET " + sl.get("33"));
+		System.out.println("GET " + sl.get("37"));
+		System.out.println("GET " + sl.get("40"));
+
+		sl.printSL();
+
+		sl.remove("33");
+		sl.printSL();
+		sl.remove("34");
+		sl.printSL();
+		sl.remove("36");
+		sl.printSL();
+		sl.remove("35");
+		sl.printSL();
+
+//		System.out.println("\n#8");
+		sl.print();
+		System.out.println("GET " + sl.get("10"));
+		System.out.println("GET " + sl.get("12"));
+		System.out.println("GET " + sl.get("32"));
+		System.out.println("GET " + sl.get("33"));
+		System.out.println("GET " + sl.get("37"));
+		System.out.println("GET " + sl.get("40"));
+
+		System.out.println("Height " + sl.stack.levels.length);
+
+		SkipIterator si = sl.iterator();
+		for(int i=0;i<5;i++) {
+			System.out.println("Iterator: " + si.next());
+		}
+		for(int i=0;i<3;i++) {
+			System.out.println("Iterator: " + si.previous());
+		}
+
+		System.out.println("Find 10");
+		si = sl.find("10");
+		for(int i=0;i<5;i++) {
+			System.out.println("Iterator: " + si.next());
+		}
+		for(int i=0;i<3;i++) {
+			System.out.println("Iterator: " + si.previous());
+		}
+
+		System.out.println("Find 34");
+		si = sl.find("34");
+		for(int i=0;i<3;i++) {
+			System.out.println("Iterator: " + si.previous());
+		}
+		for(int i=0;i<5;i++) {
+			System.out.println("Iterator: " + si.next());
+		}
+
+		System.out.println("Max");
+		si = sl.max();
+		for(int i=0;i<3;i++) {
+			System.out.println("Iterator: " + si.previous());
+		}
+		for(int i=0;i<5;i++) {
+			System.out.println("Iterator: " + si.next());
+		}
+	}
+*****/
+}
diff --git a/core/java/src/net/metanotion/util/skiplist/SkipSpan.java b/core/java/src/net/metanotion/util/skiplist/SkipSpan.java
new file mode 100644
index 0000000000000000000000000000000000000000..894ee86327f020fb1c82128bb24819728ea68fba
--- /dev/null
+++ b/core/java/src/net/metanotion/util/skiplist/SkipSpan.java
@@ -0,0 +1,272 @@
+/*
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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 net.metanotion.util.skiplist;
+
+public class SkipSpan {
+	public int nKeys = 0;
+	public Comparable[] keys;
+	public Object[] vals;
+	public SkipSpan next, prev;
+
+	public SkipSpan newInstance(SkipList sl) { return new SkipSpan(keys.length); }
+	public void killInstance() { }
+	public void flush() { }
+
+	protected SkipSpan() { }
+	public SkipSpan(int size) {
+		keys = new Comparable[size];
+		vals = new Object[size];
+	}
+
+	public void print() {
+		System.out.println("Span");
+		for(int i=0;i<nKeys;i++) {
+			System.out.println("\t" + keys[i] + " => " + vals[i]);
+		}
+		if(next != null) { next.print(); }
+	}
+
+	private int binarySearch(Comparable key) {
+ 		int high = nKeys - 1;
+ 		int low = 0;
+ 		int cur;
+ 		int cmp;
+ 		while(low <= high) {
+ 			cur = (low + high) >>> 1;
+ 			cmp = keys[cur].compareTo(key);
+ 			if(cmp > 0) {
+ 				high = cur - 1;
+ 			} else if(cmp < 0) {
+ 				low = cur + 1;
+ 			} else {
+ 				return cur;
+ 			}
+ 		}
+ 		return (-1 * (low + 1));
+	}
+
+	public SkipSpan getEnd() {
+		if(next == null) { return this; }
+		return next.getEnd();
+	}
+
+	public SkipSpan getSpan(Comparable key, int[] search) {
+		if(nKeys == 0) {
+			search[0] = -1;
+			return this;
+		}
+
+		if(keys[nKeys - 1].compareTo(key) < 0) {
+			if(next == null) {
+				search[0] = (-1 * (nKeys - 1)) - 1;
+				return this;
+			}
+			return next.getSpan(key, search);
+		}
+		search[0] = binarySearch(key);
+		return this;
+	}
+
+	public Object get(Comparable key) {
+		if(nKeys == 0) { return null; }
+		if(keys[nKeys - 1].compareTo(key) < 0) {
+			if(next == null) { return null; }
+			return next.get(key);
+		}
+		int loc = binarySearch(key);
+		if(loc < 0) { return null; }
+		return vals[loc];
+	}
+
+	private void pushTogether(int hole) {
+		for(int i=hole;i<(nKeys - 1);i++) {
+			keys[i] = keys[i+1];
+			vals[i] = vals[i+1];
+		}
+		nKeys--;
+	}
+
+	private void pushApart(int start) {
+		for(int i=(nKeys-1);i>=start;i--) {
+			keys[i+1] = keys[i];
+			vals[i+1] = vals[i];
+		}
+		nKeys++;
+	}
+
+	private void split(int loc, Comparable key, Object val, SkipList sl) {
+		SkipSpan right = newInstance(sl);
+		sl.spans++;
+
+		if(this.next != null) { this.next.prev = right; }
+		right.next = this.next;
+		right.prev = this;
+		this.next = right;
+
+		int start = ((keys.length+1)/2);
+		for(int i=start;i < keys.length; i++) {
+			try {
+			right.keys[i-start] = keys[i];
+			right.vals[i-start] = vals[i];
+			right.nKeys++;
+			this.nKeys--;
+			} catch (ArrayIndexOutOfBoundsException e) {
+				System.out.println("i " + i + " start " + start);
+				System.out.println("key: " + keys[i].toString());
+				throw e;
+			}
+		}
+		if(loc >= start) {
+			right.pushApart(loc - start);
+			right.keys[loc - start] = key;
+			right.vals[loc - start] = val;
+		} else {
+			pushApart(loc);
+			keys[loc] = key;
+			vals[loc] = val;
+		}
+		this.flush();
+		this.next.flush();
+	}
+
+	private SkipSpan insert(int loc, Comparable key, Object val, SkipList sl) {
+		sl.size++;
+		if(nKeys == keys.length) {
+			// split.
+			split(loc, key, val, sl);
+			return next;
+		} else {
+			pushApart(loc);
+			keys[loc] = key;
+			vals[loc] = val;
+			this.flush();
+			return null;
+		}
+	}
+
+	public SkipSpan put(Comparable key, Object val, SkipList sl)	{
+		if(nKeys == 0) {
+			sl.size++;
+			keys[0] = key;
+			vals[0] = val;
+			nKeys++;
+			this.flush();
+			return null;
+		}
+		int loc = binarySearch(key);
+		if(loc < 0) {
+			loc = -1 * (loc + 1);
+			if(next != null) {
+				int cmp = next.firstKey().compareTo(key);
+				if((loc >= nKeys) && (cmp > 0)) {
+					// It fits in between this span and the next
+					// Try to avoid a split...
+					if(nKeys == keys.length) {
+						if(next.nKeys == keys.length) {
+							return insert(loc, key, val, sl);
+						} else {
+							return next.put(key, val, sl);
+						}
+					} else {
+						return insert(loc, key, val, sl);
+					}
+				} else {
+					// Its either clearly in the next span or this span.
+					if(cmp > 0) {
+						return insert(loc, key, val, sl);
+					} else {
+						return next.put(key, val, sl);
+					}
+				}
+			} else {
+				// There is no next span, So
+				// either it goes here, or causes a split.
+				return insert(loc, key, val, sl);
+			}
+		} else {
+			// Key already exists. Overwrite value.
+			vals[loc] = val;
+			this.flush();
+			return null;
+		}
+	}
+
+	public Object[] remove(Comparable key, SkipList sl) {
+		if(nKeys == 0) { return null; }
+		if(keys[nKeys - 1].compareTo(key) < 0) {
+			if(next == null) { return null; }
+			return next.remove(key, sl);
+		}
+		int loc = binarySearch(key);
+		if(loc < 0) { return null; }
+		Object o = vals[loc];
+		Object[] res = new Object[2];
+		res[0] = o;
+		sl.size--;
+		if(nKeys == 1) {
+			if(sl.spans > 1) { sl.spans--; }
+			if((this.prev == null) && (this.next != null)) {
+				res[1] = this.next;
+				// We're the first node in the list...
+				for(int i=0;i<next.nKeys;i++) {
+					keys[i] = next.keys[i];
+					vals[i] = next.vals[i];
+				}
+				nKeys = next.nKeys;
+				SkipSpan nn = next.next;
+				next.killInstance();
+				this.flush();
+				this.next = nn;
+			} else {
+				res[1] = this;
+				if(this.prev != null) {
+					this.prev.next = this.next;
+					this.prev.flush();
+				}
+				if(this.next != null) {
+					this.next.prev = this.prev;
+					this.next.flush();
+				}
+				this.next = null;
+				this.prev = null;
+				nKeys = 0;
+				this.killInstance();
+			}
+		} else {
+			pushTogether(loc);
+			this.flush();
+		}
+		return res;
+	}
+
+	/** I2P */
+	public Comparable firstKey() {
+		return keys[0];
+	}
+}
diff --git a/licenses/LICENSE-BlockFile.txt b/licenses/LICENSE-BlockFile.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d98565b1a4e37c2165b5d6eb5b9f7ab7e63b7447
--- /dev/null
+++ b/licenses/LICENSE-BlockFile.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2006, Matthew Estes
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+	* 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.
+	* Neither the name of Metanotion Software nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR
+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.
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 79f05bbd461aaea61b96fefe952fe34c47500120..f21d5f6a18f10e046990174871e6d77042f77b43 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -934,6 +934,7 @@ public class Router {
             }
         }
         try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the client manager", t); }
+        try { _context.namingService().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the naming service", t); }
         try { _context.jobQueue().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the job queue", t); }
         //try { _context.adminManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the admin manager", t); }        
         try { _context.statPublisher().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the stats manager", t); }