add missing files; new API in blockfile NS

This commit is contained in:
zzz
2011-02-27 14:49:04 +00:00
parent c269546c08
commit a3fb49adcb
4 changed files with 560 additions and 1 deletions

View File

@ -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<String, Destination> 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<String, Destination> 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)

View File

@ -65,7 +65,7 @@ public class MetaNamingService extends DummyNamingService {
@Override
public List<NamingService> getNamingServices() {
return new Collections.unmodifiableList(_services);
return Collections.unmodifiableList(_services);
}
@Override

View File

@ -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);
}

View File

@ -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);
}
}