diff --git a/apps/susidns/src/WEB-INF/web-template.xml b/apps/susidns/src/WEB-INF/web-template.xml
index 599826246b..1937a5b14f 100644
--- a/apps/susidns/src/WEB-INF/web-template.xml
+++ b/apps/susidns/src/WEB-INF/web-template.xml
@@ -42,6 +42,11 @@
/index
+
+ i2p.susi.dns.jsp.export_jsp
+ /export
+
+
30
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
index 5a147a66a3..2faefb8531 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
@@ -21,12 +21,15 @@
package i2p.susi.dns;
+import java.io.IOException;
+import java.io.Writer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.SortedMap;
import net.i2p.client.naming.NamingService;
import net.i2p.data.DataFormatException;
@@ -196,7 +199,8 @@ public class NamingServiceBean extends AddressbookBean
}
}
AddressBean array[] = list.toArray(new AddressBean[list.size()]);
- Arrays.sort( array, sorter );
+ if (!(results instanceof SortedMap))
+ Arrays.sort( array, sorter );
entries = array;
message = generateLoadMessage();
@@ -351,4 +355,21 @@ public class NamingServiceBean extends AddressbookBean
rv.setProperties(outProps);
return rv;
}
+
+ /**
+ * @since 0.9.20
+ */
+ public void export(Writer out) throws IOException {
+ 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 (search != null && search.length() > 0)
+ searchProps.setProperty("search", search.toLowerCase(Locale.US));
+ getNamingService().export(out, searchProps);
+ // No post-filtering for hosts.txt naming services. It is what it is.
+ }
}
diff --git a/apps/susidns/src/jsp/export.jsp b/apps/susidns/src/jsp/export.jsp
new file mode 100644
index 0000000000..39440ecf42
--- /dev/null
+++ b/apps/susidns/src/jsp/export.jsp
@@ -0,0 +1,35 @@
+<%
+/*
+ * This file is part of susidns project, see http://susi.i2p/
+ *
+ * Copyright (C) 2005
+ *
+ * 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
+ *
+ */
+
+ // http://www.crazysquirrel.com/computing/general/form-encoding.jspx
+ if (request.getCharacterEncoding() == null)
+ request.setCharacterEncoding("UTF-8");
+%>
+<%@page pageEncoding="UTF-8"%>
+<%@page trimDirectiveWhitespaces="true"%>
+<%@ page contentType="text/plain"%>
+
+
+
+<%
+ book.export(out);
+%>
diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
index d7860a727e..ec12eb792d 100644
--- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
@@ -18,11 +18,14 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import java.util.StringTokenizer;
+import java.util.TreeMap;
import net.i2p.I2PAppContext;
import net.i2p.data.DataFormatException;
@@ -860,7 +863,7 @@ public class BlockfileNamingService extends DummyNamingService {
iter = sl.find(beginWith);
else
iter = sl.iterator();
- Map rv = new HashMap();
+ Map rv = new TreeMap();
for (int i = 0; i < skip && iter.hasNext(); i++) {
// don't bother validating here
iter.next();
@@ -896,6 +899,188 @@ public class BlockfileNamingService extends DummyNamingService {
}
}
+ /**
+ * @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.
+ * @since 0.9.20
+ */
+ @Override
+ public Map getBase64Entries(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) {}
+ }
+ synchronized(_bf) {
+ if (_isClosed)
+ return Collections.emptyMap();
+ 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.emptyMap();
+ }
+ SkipIterator iter;
+ if (beginWith != null)
+ iter = sl.find(beginWith);
+ else
+ iter = sl.iterator();
+ Map rv = new TreeMap();
+ for (int i = 0; i < skip && iter.hasNext(); i++) {
+ // don't bother validating here
+ 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 (!validate(key, de, listname))
+ continue;
+ if (search != null && key.indexOf(search) < 0)
+ continue;
+ rv.put(key, de.dest.toBase64());
+ i++;
+ }
+ return rv;
+ } catch (IOException ioe) {
+ _log.error("DB lookup error", ioe);
+ return Collections.emptyMap();
+ } catch (RuntimeException re) {
+ _log.error("DB lookup error", re);
+ return Collections.emptyMap();
+ } finally {
+ deleteInvalid();
+ }
+ }
+ }
+
+ /**
+ * @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.
+ * @since 0.9.20
+ */
+ @Override
+ public Set getNames(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) {}
+ }
+ synchronized(_bf) {
+ if (_isClosed)
+ return Collections.emptySet();
+ 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.emptySet();
+ }
+ SkipIterator iter;
+ if (beginWith != null)
+ iter = sl.find(beginWith);
+ else
+ iter = sl.iterator();
+ Set rv = new HashSet();
+ 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;
+ }
+ }
+ if (search != null && key.indexOf(search) < 0)
+ continue;
+ rv.add(key);
+ i++;
+ }
+ return rv;
+ } catch (IOException ioe) {
+ _log.error("DB lookup error", ioe);
+ return Collections.emptySet();
+ } catch (RuntimeException re) {
+ _log.error("DB lookup error", re);
+ return Collections.emptySet();
+ }
+ }
+ }
+
/**
* @param options ignored
* @since 0.8.9
diff --git a/core/java/src/net/i2p/client/naming/MetaNamingService.java b/core/java/src/net/i2p/client/naming/MetaNamingService.java
index fab433a8df..ffae355a57 100644
--- a/core/java/src/net/i2p/client/naming/MetaNamingService.java
+++ b/core/java/src/net/i2p/client/naming/MetaNamingService.java
@@ -1,5 +1,7 @@
package net.i2p.client.naming;
+import java.io.IOException;
+import java.io.Writer;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashMap;
@@ -179,6 +181,19 @@ public class MetaNamingService extends DummyNamingService {
return rv;
}
+ /**
+ * All services aggregated
+ * @since 0.9.20
+ */
+ @Override
+ public Map getBase64Entries(Properties options) {
+ Map rv = new HashMap();
+ for (NamingService ns : _services) {
+ rv.putAll(ns.getBase64Entries(options));
+ }
+ return rv;
+ }
+
/**
* All services aggregated
*/
@@ -191,6 +206,17 @@ public class MetaNamingService extends DummyNamingService {
return rv;
}
+ /**
+ * All services aggregated.
+ * Duplicates not removed (for efficiency)
+ * @since 0.9.20
+ */
+ public void export(Writer out, Properties options) throws IOException {
+ for (NamingService ns : _services) {
+ export(out, options);
+ }
+ }
+
/**
* All services aggregated
*/
diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java
index 001cc421b9..f45ca87c1e 100644
--- a/core/java/src/net/i2p/client/naming/NamingService.java
+++ b/core/java/src/net/i2p/client/naming/NamingService.java
@@ -7,12 +7,16 @@
*/
package net.i2p.client.naming;
+import java.io.IOException;
+import java.io.Writer;
import java.lang.reflect.Constructor;
import java.util.Collections;
+import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArraySet;
import net.i2p.I2PAppContext;
@@ -235,14 +239,75 @@ public abstract class NamingService {
* Warning - This will bring the whole database into memory
* if options is null, empty, or unsupported, use with caution.
*
+ * This implementation calls getEntries(options) and returns a SortedMap.
+ * Subclasses should override if they store base64 natively.
+ *
* @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.7
+ * @since 0.8.7, implemented in 0.9.20
*/
public Map getBase64Entries(Properties options) {
- return Collections.emptyMap();
+ Map entries = getEntries(options);
+ if (entries.size() <= 0)
+ return Collections.emptyMap();
+ Map rv = new TreeMap();
+ for (Map.Entry e : entries.entrySet()) {
+ rv.put(e.getKey(), e.getValue().toBase64());
+ }
+ return rv;
+ }
+
+ /**
+ * Export in a hosts.txt format.
+ * Output is not necessarily sorted, implementation dependent.
+ * Output may or may not contain comment lines, implementation dependent.
+ * Caller must close writer.
+ *
+ * This implementation calls getBase64Entries().
+ * Subclasses should override if they store in a hosts.txt format natively.
+ *
+ * @since 0.9.20
+ */
+ public void export(Writer out) throws IOException {
+ export(out, null);
+ }
+
+ /**
+ * Export in a hosts.txt format.
+ * Output is not necessarily sorted, implementation dependent.
+ * Output may or may not contain comment lines, implementation dependent.
+ * Caller must close writer.
+ *
+ * This implementation calls getBase64Entries(options).
+ * Subclasses should override if they store in a hosts.txt format natively.
+ *
+ * @param options NamingService-specific, can be null
+ * @since 0.9.20
+ */
+ public void export(Writer out, Properties options) throws IOException {
+ Map entries = getBase64Entries(options);
+ out.write("# Address book: ");
+ out.write(getName());
+ out.write('\n');
+ int sz = entries.size();
+ if (sz <= 0) {
+ out.write("# No entries\n");
+ return;
+ }
+ out.write("# Exported: ");
+ out.write((new Date()).toString());
+ out.write('\n');
+ if (sz > 1) {
+ out.write("# " + sz + " entries\n");
+ }
+ for (Map.Entry e : entries.entrySet()) {
+ out.write(e.getKey());
+ out.write('=');
+ out.write(e.getValue());
+ out.write('\n');
+ }
}
/**
diff --git a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
index 58a2d1a95c..f4e9225167 100644
--- a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
@@ -15,7 +15,9 @@ import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -364,6 +366,102 @@ public class SingleFileNamingService extends NamingService {
}
}
+ /**
+ * Overridden since we store base64 natively.
+ *
+ * @param options As follows:
+ * Key "search": return only those matching substring
+ * Key "startsWith": return only those starting with
+ * ("[0-9]" allowed)
+ * @return all mappings (matching the options if non-null)
+ * or empty Map if none.
+ * Returned Map is not sorted.
+ * @since 0.9.20
+ */
+ public Map getBase64Entries(Properties options) {
+ if (!_file.exists())
+ return Collections.emptyMap();
+ String searchOpt = null;
+ String startsWith = null;
+ if (options != null) {
+ searchOpt = options.getProperty("search");
+ startsWith = options.getProperty("startsWith");
+ }
+ BufferedReader in = null;
+ getReadLock();
+ try {
+ in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
+ String line = null;
+ Map 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() ??????????????
+ if (b64.length() < 387)
+ continue;
+ rv.put(key, b64);
+ }
+ if (searchOpt == null && startsWith == null) {
+ _lastWrite = _file.lastModified();
+ _size = rv.size();
+ }
+ return rv;
+ } catch (IOException ioe) {
+ _log.error("getEntries error", ioe);
+ return Collections.emptyMap();
+ } finally {
+ if (in != null) try { in.close(); } catch (IOException ioe) {}
+ releaseReadLock();
+ }
+ }
+
+ /**
+ * Overridden for efficiency.
+ * Output is not sorted.
+ *
+ * @param options ignored
+ * @since 0.9.20
+ */
+ public void export(Writer out, Properties options) throws IOException {
+ out.write("# Address book: ");
+ out.write(getName());
+ out.write('\n');
+ out.write("# Exported: ");
+ out.write((new Date()).toString());
+ out.write('\n');
+ 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) {
+ out.write(line);
+ out.write('\n');
+ }
+ } finally {
+ if (in != null) try { in.close(); } catch (IOException ioe) {}
+ releaseReadLock();
+ }
+ }
+
/**
* @param options ignored
* @return all known host names, unsorted