/*
* 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.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;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* Naming services create a subclass of this class.
*/
public abstract class NamingService {
protected final Log _log;
protected final I2PAppContext _context;
protected final Set<NamingServiceListener> _listeners;
protected final Set<NamingServiceUpdater> _updaters;
/** what classname should be used as the naming service impl? */
public static final String PROP_IMPL = "i2p.naming.impl";
private static final String DEFAULT_IMPL = "net.i2p.router.naming.BlockfileNamingService";
private static final String OLD_DEFAULT_IMPL = "net.i2p.client.naming.BlockfileNamingService";
private static final String BACKUP_IMPL = "net.i2p.client.naming.HostsTxtNamingService";
/**
* The naming service should only be constructed and accessed through the
* application context. This constructor should only be used by the
* appropriate application context itself.
*
*/
protected NamingService(I2PAppContext context) {
_context = context;
_log = context.logManager().getLog(getClass());
_listeners = new CopyOnWriteArraySet<NamingServiceListener>();
_updaters = new CopyOnWriteArraySet<NamingServiceUpdater>();
}
/**
* Look up a host name.
* @return the Destination for this host name, or
* <code>null</code> if name is unknown.
*/
public Destination lookup(String hostname) {
return lookup(hostname, null, null);
}
/**
* Reverse lookup a destination
* This implementation returns reverseLookup(dest, null).
*
* @param dest non-null
* @return a host name for this Destination, or <code>null</code>
* if none is known. It is safe for subclasses to always return
* <code>null</code> if no reverse lookup is possible.
*/
public String reverseLookup(Destination dest) {
return reverseLookup(dest, null);
}
/**
* Reverse lookup a hash.
* This implementation returns null.
* Subclasses implementing reverse lookups should override.
*
* @param h non-null
* @return a host name for this hash, or <code>null</code>
* if none is known. It is safe for subclasses to always return
* <code>null</code> if no reverse lookup is possible.
*/
public String reverseLookup(Hash h) { return null; }
/**
* If the host name is a valid Base64 encoded destination, return the
* decoded Destination. Useful as a "fallback" in custom naming
* implementations.
* This is misnamed as it isn't a "lookup" at all, but
* a simple conversion from a Base64 string to a Destination.
*
* @param hostname 516+ character Base 64
* @return Destination or null on error
*/
protected Destination lookupBase64(String hostname) {
try {
Destination result = new Destination();
result.fromBase64(hostname);
return result;
} catch (DataFormatException dfe) {
if (_log.shouldLog(Log.WARN)) _log.warn("Bad B64 dest [" + hostname + "]", dfe);
return null;
}
}
@Override
public String toString() {
return getClass().getSimpleName();
}
///// New API Starts Here
/**
* @return Class simple name by default
* @since 0.8.7
*/
public String getName() {
return getClass().getSimpleName();
}
/**
* Warning - unimplemented in any subclass.
* Returns null always.
*
* @return NamingService-specific options or null
* @since 0.8.7
*/
public Properties getConfiguration() {
return null;
}
/**
* Warning - unimplemented in any subclass.
* Returns true always.
*
* @return success
* @since 0.8.7
*/
public boolean setConfiguration(Properties p) {
return true;
}
// These are for daisy chaining (MetaNamingService)
/**
* This implementation returns null.
* Subclasses implementing chaining should override.
*
* @return chained naming services or null
* @since 0.8.7
*/
public List<NamingService> getNamingServices() {
return null;
}
/**
* This implementation returns null.
* Subclasses implementing chaining should override.
*
* @return parent naming service or null if this is the root
* @since 0.8.7
*/
public NamingService getParent() {
return null;
}
/**
* Only for chaining-capable NamingServices. Add to end of the list.
* @return success
* @since 0.8.7
*/
public boolean addNamingService(NamingService ns) {
return addNamingService(ns, false);
}
/**
* Only for chaining-capable NamingServices.
* This implementation returns false.
* Subclasses implementing chaining should override.
*
* @param head or tail
* @return success
* @since 0.8.7
*/
public boolean addNamingService(NamingService ns, boolean head) {
return false;
}
/**
* Only for chaining-capable NamingServices.
* This implementation returns false.
* Subclasses implementing chaining should override.
*
* @return success
* @since 0.8.7
*/
public boolean removeNamingService(NamingService ns) {
return false;
}
// options would be used to specify public / private / master ...
// or should we just daisy chain 3 HostsTxtNamingServices ?
// that might be better... then addressbook only talks to the 'router' HostsTxtNamingService
/**
* @return number of entries or -1 if unknown
* @since 0.8.7
*/
public int size() {
return size(null);
}
/**
* This implementation returns -1.
* Most subclasses should override.
*
* @param options NamingService-specific, can be null
* @return number of entries (matching the options if non-null) or -1 if unknown
* @since 0.8.7
*/
public int size(Properties options) {
return -1;
}
/**
* Warning - This obviously brings the whole database into memory,
* so use is discouraged.
*
* @return all mappings
* or empty Map if none;
* Returned Map is not necessarily sorted, implementation dependent
* @since 0.8.7
*/
public Map<String, Destination> getEntries() {
return getEntries(null);
}
/**
* Warning - This will bring the whole database into memory
* if options is null, empty, or unsupported, use with caution.
*
* @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
*/
public Map<String, Destination> getEntries(Properties options) {
return Collections.emptyMap();
}
/**
* This may be more or less efficient than getEntries(),
* depending on the implementation.
* 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, implemented in 0.9.20
*/
public Map<String, String> getBase64Entries(Properties options) {
Map<String, Destination> entries = getEntries(options);
if (entries.size() <= 0)
return Collections.emptyMap();
Map<String, String> rv = new TreeMap<String, String>();
for (Map.Entry<String, Destination> 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<String, String> entries = getBase64Entries(options);
out.write("# Address book: ");
out.write(getName());
if (options != null) {
String list = options.getProperty("list");
if (list != null)
out.write(" (" + list + ')');
}
final String nl = System.getProperty("line.separator", "\n");
out.write(nl);
int sz = entries.size();
if (sz <= 0) {
out.write("# No entries");
out.write(nl);
return;
}
out.write("# Exported: ");
out.write((new Date()).toString());
out.write(nl);
if (sz > 1) {
out.write("# " + sz + " entries");
out.write(nl);
}
for (Map.Entry<String, String> e : entries.entrySet()) {
out.write(e.getKey());
out.write('=');
out.write(e.getValue());
out.write(nl);
}
}
/**
* @return all known host names
* or empty Set if none;
* Returned Set is not necessarily sorted, implementation dependent
* @since 0.8.7
*/
public Set<String> getNames() {
return getNames(null);
}
/**
* @param options NamingService-specific, can be null
* @return all known host names (matching the options if non-null)
* or empty Set if none;
* Returned Set is not necessarily sorted, implementation dependent
* @since 0.8.7
*/
public Set<String> getNames(Properties options) {
return Collections.emptySet();
}
/**
* Add a hostname and Destination to the addressbook.
* Overwrites old entry if it exists.
* See also putIfAbsent() and update().
*
* @return success
* @since 0.8.7
*/
public boolean put(String hostname, Destination d) {
return put(hostname, d, null);
}
/**
* Add a hostname and Destination to the addressbook.
* Overwrites old entry if it exists.
* See also putIfAbsent() and update().
*
* @param options NamingService-specific, can be null
* @return success
* @since 0.8.7
*/
public boolean put(String hostname, Destination d, Properties options) {
return false;
}
/**
* Add a hostname and Destination to the addressbook.
* Fails if entry previously exists.
* See also put() and update().
*
* @return success
* @since 0.8.7
*/
public boolean putIfAbsent(String hostname, Destination d) {
return putIfAbsent(hostname, d, null);
}
/**
* Add a hostname and Destination to the addressbook.
* Fails if entry previously exists.
* See also put() and update().
*
* @param options NamingService-specific, can be null
* @return success
* @since 0.8.7
*/
public boolean putIfAbsent(String hostname, Destination d, Properties options) {
return false;
}
/**
* Put all the entries, each with the given options.
* This implementation calls put() for each entry.
* Subclasses may override if a more efficient implementation is available.
*
* @param options NamingService-specific, can be null
* @return total success, or false if any put failed
* @since 0.8.7
*/
public boolean putAll(Map<String, Destination> entries, Properties options) {
boolean rv = true;
for (Map.Entry<String, Destination> entry : entries.entrySet()) {
if (!put(entry.getKey(), entry.getValue(), options))
rv = false;
}
return rv;
}
/**
* Fails if entry did not previously exist.
* Warning - unimplemented in any subclass.
* This implementation returns false.
*
* @param d may be null if only options are changing
* @param options NamingService-specific, can be null
* @return success
* @since 0.8.7
*/
public boolean update(String hostname, Destination d, Properties options) {
return false;
}
/**
* Delete the entry.
* @return true if removed successfully, false on error or if it did not exist
* @since 0.8.7
*/
public boolean remove(String hostname) {
return remove(hostname, (Properties) null);
}
/**
* Delete the entry.
* @param options NamingService-specific, can be null
* @return true if removed successfully, false on error or if it did not exist
* @since 0.8.7
*/
public boolean remove(String hostname, Properties options) {
return false;
}
/**
* Ask any registered updaters to update now
* @param options NamingService- or updater-specific, may be null
* @since 0.8.7
*/
public void requestUpdate(Properties options) {
for (NamingServiceUpdater nsu : _updaters) {
nsu.update(options);
}
}
/**
* @since 0.8.7
*/
public void registerListener(NamingServiceListener nsl) {
_listeners.add(nsl);
}
/**
* @since 0.8.7
*/
public void unregisterListener(NamingServiceListener nsl) {
_listeners.remove(nsl);
}
/**
* @since 0.8.7
*/
public void registerUpdater(NamingServiceUpdater nsu) {
_updaters.add(nsu);
}
/**
* @since 0.8.7
*/
public void unregisterUpdater(NamingServiceUpdater nsu) {
_updaters.remove(nsu);
}
/**
* Same as lookup(hostname) but with in and out options
* Note that whether this (and lookup(hostname)) resolve Base 32 addresses
* in the form {52 chars}.b32.i2p is NamingService-specific.
*
* @param lookupOptions input parameter, NamingService-specific, can be null
* @param storedOptions output parameter, NamingService-specific, any stored properties will be added if non-null
* @return dest or null
* @since 0.8.7
*/
public abstract Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions);
/**
* Same as reverseLookup(dest) but with options
* This implementation returns null.
* Subclasses implementing reverse lookups should override.
*
* @param d non-null
* @param options NamingService-specific, can be null
* @return host name or null
* @since 0.8.7
*/
public String reverseLookup(Destination d, Properties options) {
return null;
}
/**
* Lookup a Base 32 address. This may require the router to fetch the LeaseSet,
* which may take quite a while.
* This implementation returns null.
* See also lookup(Hash, int).
*
* @param hostname must be {52 chars}.b32.i2p
* @param timeout in seconds; <= 0 means use router default
* @return dest or null
* @since 0.8.7
*/
public Destination lookupBase32(String hostname, int timeout) {
return null;
}
/**
* Same as lookupBase32() but with the SHA256 Hash precalculated
* This implementation returns null.
*
* @param timeout in seconds; <= 0 means use router default
* @return dest or null
* @since 0.8.7
*/
public Destination lookup(Hash hash, int timeout) {
return null;
}
/**
* Parent will call when added.
* If this is the root naming service, the core will start it.
* Should not be called by others.
* @since 0.8.7
*/
public void start() {}
/**
* Parent will call when removed.
* If this is the root naming service, the core will stop it.
* Should not be called by others.
* @since 0.8.7
*/
public void shutdown() {}
//// End New API
//// Begin new API for multiple Destinations
/**
* For NamingServices that support multiple Destinations for a single host name,
* return all of them.
*
* It is recommended that the returned list is in order of priority, highest-first,
* but this is NamingService-specific.
*
* Not recommended for resolving Base 32 addresses;
* whether this does resolve Base 32 addresses
* in the form {52 chars}.b32.i2p is NamingService-specific.
*
* @return non-empty List of Destinations, or null if nothing found
* @since 0.9.26
*/
public List<Destination> lookupAll(String hostname) {
return lookupAll(hostname, null, null);
}
/**
* For NamingServices that support multiple Destinations and Properties for a single host name,
* return all of them.
*
* It is recommended that the returned list is in order of priority, highest-first,
* but this is NamingService-specific.
*
* If storedOptions is non-null, it must be a List that supports null entries.
* If the returned value (the List of Destinations) is non-null,
* the same number of Properties objects will be added to storedOptions.
* If no properties were found for a given Destination, the corresponding
* entry in the storedOptions list will be null.
*
* Not recommended for resolving Base 32 addresses;
* whether this does resolve Base 32 addresses
* in the form {52 chars}.b32.i2p is NamingService-specific.
*
* This implementation simply calls lookup().
* Subclasses implementing multiple destinations per hostname should override.
*
* @param lookupOptions input parameter, NamingService-specific, may be null
* @param storedOptions output parameter, NamingService-specific, any stored properties will be added if non-null
* @return non-empty List of Destinations, or null if nothing found
* @since 0.9.26
*/
public List<Destination> lookupAll(String hostname, Properties lookupOptions, List<Properties> storedOptions) {
Properties props = storedOptions != null ? new Properties() : null;
Destination d = lookup(hostname, lookupOptions, props);
List<Destination> rv;
if (d != null) {
rv = Collections.singletonList(d);
if (storedOptions != null)
storedOptions.add(props.isEmpty() ? null : props);
} else {
rv = null;
}
return rv;
}
/**
* Add a Destination to an existing hostname's entry in the addressbook.
*
* @return success
* @since 0.9.26
*/
public boolean addDestination(String hostname, Destination d) {
return addDestination(hostname, d, null);
}
/**
* Add a Destination to an existing hostname's entry in the addressbook.
* This implementation simply calls putIfAbsent().
* Subclasses implementing multiple destinations per hostname should override.
*
* @param options NamingService-specific, may be null
* @return success
* @since 0.9.26
*/
public boolean addDestination(String hostname, Destination d, Properties options) {
return putIfAbsent(hostname, d, options);
}
/**
* Remove a hostname's entry only if it contains the Destination d.
* If the NamingService supports multiple Destinations per hostname,
* and this is the only Destination, removes the entire entry.
* If aditional Destinations remain, it only removes the
* specified Destination from the entry.
*
* @return true if entry containing d was successfully removed.
* @since 0.9.26
*/
public boolean remove(String hostname, Destination d) {
return remove(hostname, d, null);
}
/**
* Remove a hostname's entry only if it contains the Destination d.
* If the NamingService supports multiple Destinations per hostname,
* and this is the only Destination, removes the entire entry.
* If aditional Destinations remain, it only removes the
* specified Destination from the entry.
*
* This implementation simply calls lookup() and remove().
* Subclasses implementing multiple destinations per hostname,
* or with more efficient implementations, should override.
* Fails if entry previously exists.
*
* @param options NamingService-specific, may be null
* @return true if entry containing d was successfully removed.
* @since 0.9.26
*/
public boolean remove(String hostname, Destination d, Properties options) {
Destination old = lookup(hostname, options, null);
if (!d.equals(old))
return false;
return remove(hostname, options);
}
/**
* Reverse lookup a hash.
* This implementation returns the result from reverseLookup, or null.
* Subclasses implementing reverse lookups should override.
*
* @param h non-null
* @return a non-empty list of host names for this hash, or <code>null</code>
* if none is known. It is safe for subclasses to always return
* <code>null</code> if no reverse lookup is possible.
* @since 0.9.26
*/
public List<String> reverseLookupAll(Hash h) {
String s = reverseLookup(h);
return (s != null) ? Collections.singletonList(s) : null;
}
/**
* Reverse lookup a destination
* This implementation returns reverseLookupAll(dest, null).
*
* @param dest non-null
* @return a non-empty list of host names for this Destination, or <code>null</code>
* if none is known. It is safe for subclasses to always return
* <code>null</code> if no reverse lookup is possible.
* @since 0.9.26
*/
public List<String> reverseLookupAll(Destination dest) {
return reverseLookupAll(dest, null);
}
/**
* Same as reverseLookupAll(dest) but with options
* This implementation returns the result from reverseLookup, or null.
* Subclasses implementing reverse lookups should override.
*
* @param d non-null
* @param options NamingService-specific, can be null
* @return a non-empty list of host names for this Destination, or <code>null</code>
* @since 0.9.26
*/
public List<String> reverseLookupAll(Destination d, Properties options) {
String s = reverseLookup(d, options);
return (s != null) ? Collections.singletonList(s) : null;
}
//// End new API for multiple Destinations
/**
* WARNING - for use by I2PAppContext only - others must use
* I2PAppContext.namingService()
*
* Get a naming service instance. This method ensures that there
* will be only one naming service instance (singleton) as well as
* choose the implementation from the "i2p.naming.impl" system
* property.
*
* FIXME Actually, it doesn't ensure that. Only call this once!!!
*/
public static final synchronized NamingService createInstance(I2PAppContext context) {
NamingService instance = null;
String dflt = context.isRouterContext() ? DEFAULT_IMPL : BACKUP_IMPL;
String impl = context.getProperty(PROP_IMPL, DEFAULT_IMPL);
if (impl.equals(OLD_DEFAULT_IMPL))
impl = dflt;
try {
Class<?> cls = Class.forName(impl);
Constructor<?> con = cls.getConstructor(I2PAppContext.class);
instance = (NamingService)con.newInstance(context);
} catch (Exception ex) {
Log log = context.logManager().getLog(NamingService.class);
// Blockfile may throw RuntimeException but HostsTxt won't
if (!impl.equals(BACKUP_IMPL)) {
log.error("Cannot load naming service " + impl + ", using HostsTxtNamingService", ex);
instance = new HostsTxtNamingService(context);
} else {
log.error("Cannot load naming service " + impl + ", only .b32.i2p lookups will succeed", ex);
instance = new DummyNamingService(context);
}
}
return instance;
}
}