forked from I2P_Developers/i2p.i2p
add missing files; new API in blockfile NS
This commit is contained in:
@ -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)
|
||||
|
@ -65,7 +65,7 @@ public class MetaNamingService extends DummyNamingService {
|
||||
|
||||
@Override
|
||||
public List<NamingService> getNamingServices() {
|
||||
return new Collections.unmodifiableList(_services);
|
||||
return Collections.unmodifiableList(_services);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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);
|
||||
}
|
||||
|
346
core/java/src/net/i2p/client/naming/SingleFileNamingService.java
Normal file
346
core/java/src/net/i2p/client/naming/SingleFileNamingService.java
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user