diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java index b9be1257a..0d37e86a5 100644 --- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java +++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java @@ -19,7 +19,9 @@ 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; @@ -335,6 +337,18 @@ public class BlockfileNamingService extends DummyNamingService { 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); if (d != null) @@ -349,6 +363,8 @@ public class BlockfileNamingService extends DummyNamingService { DestEntry de = getEntry(list, key); if (de != null) { d = de.dest; + if (storedOptions != null) + storedOptions.putAll(de.props); break; } } catch (IOException ioe) { @@ -361,6 +377,178 @@ public class BlockfileNamingService extends DummyNamingService { 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) { + 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); + 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) { + try { + SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer); + if (sl == null) + return false; + boolean rv = removeEntry(sl, key) != null; + if (rv) { + for (NamingServiceListener nsl : _listeners) { + nsl.entryRemoved(this, key); + } + } + return rv; + } catch (IOException re) { + return false; + } catch (RuntimeException re) { + return false; + } + } + } + + /** + * @param options If non-null and contains the key "list", get + * from that list (default "hosts.txt", NOT all lists) + * Key "limit": max number to return + * Key "startsWith": return only those starting with + * Key "beginWith": start here in the iteration + * Don't use both + */ + @Override + public Map getEntries(Properties options) { + String listname = FALLBACK_LIST; + String startsWith = null; + String beginWith = null; + int limit = Integer.MAX_VALUE; + if (options != null) { + listname = options.getProperty("list"); + startsWith = options.getProperty("startsWith"); + beginWith = options.getProperty("beginWith"); + if (beginWith == null) + beginWith = startsWith; + String lim = options.getProperty("limit"); + try { + limit = Integer.parseInt(lim); + } catch (NumberFormatException nfe) {} + } + synchronized(_bf) { + try { + SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer); + if (sl == null) + return Collections.EMPTY_MAP; + SkipIterator iter; + if (startsWith != null) + iter = sl.find(beginWith); + else + iter = sl.iterator(); + Map rv = new HashMap(); + for (int i = 0; i < limit && iter.hasNext(); i++) { + String key = (String) iter.nextKey(); + if (startsWith != null && !key.startsWith(startsWith)) + break; + DestEntry de = (DestEntry) iter.next(); + rv.put(key, de.dest); + } + return rv; + } catch (IOException re) { + return Collections.EMPTY_MAP; + } catch (RuntimeException 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) { + try { + SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer); + if (sl == null) + return 0; + return sl.size(); + } catch (IOException re) { + return 0; + } catch (RuntimeException re) { + return 0; + } + } + } + + ////////// End NamingService API + private void dumpDB() { synchronized(_bf) { if (_isClosed) diff --git a/core/java/src/net/i2p/client/naming/MetaNamingService.java b/core/java/src/net/i2p/client/naming/MetaNamingService.java index 9fef0f735..f5c680549 100644 --- a/core/java/src/net/i2p/client/naming/MetaNamingService.java +++ b/core/java/src/net/i2p/client/naming/MetaNamingService.java @@ -65,7 +65,7 @@ public class MetaNamingService extends DummyNamingService { @Override public List getNamingServices() { - return new Collections.unmodifiableList(_services); + return Collections.unmodifiableList(_services); } @Override 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 000000000..2966d48dd --- /dev/null +++ b/core/java/src/net/i2p/client/naming/NamingServiceListener.java @@ -0,0 +1,25 @@ +package net.i2p.client.naming; + +import java.util.Properties; + +import net.i2p.data.Destination; + +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/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java new file mode 100644 index 000000000..ba3b2bc2a --- /dev/null +++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java @@ -0,0 +1,346 @@ +/* + * 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.util.Iterator; +import java.util.List; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +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 static Log _log = new Log(SingleFileNamingService.class); + private final File _file; + private final ReentrantReadWriteLock _fileLock; + + 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(); + Properties hosts = new Properties(); + getReadLock(); + try { + DataHelper.loadProps(hosts, _file, true); + } 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(); + } + 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; + } + return null; + } + + /** + * 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 { + 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) { + if (!getWriteLock()) + return false; + OutputStream out = null; + try { + // 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) { + if (!getWriteLock()) + return false; + if (!_file.exists()) + return false; + BufferedReader in = null; + BufferedWriter out = null; + try { + 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(); + } + } + + 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); + } +}