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