blob: cbe073a6cdc9d18e71ce8ff9a17bc355c2d33179 [file] [log] [blame]
///Copyright 2003-2005 Arthur van Hoff, Rick Blair
//Licensed under Apache License version 2.0
//Original license LGPL
package javax.jmdns.impl;
import java.io.IOException;
import java.net.*;
import java.util.*;
import javax.jmdns.*;
import javax.jmdns.impl.tasks.*;
// REMIND: multiple IP addresses
/**
* mDNS implementation in Java.
*
* @version %I%, %G%
* @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer,
* Pierre Frisch, Scott Lewis
*/
public class JmDNSImpl extends JmDNS {
// private static Logger logger = Logger.getLogger(JmDNSImpl.class.getName());
// hack for the iPhone (arm darwin) to not set the NetworkInterface. This fails on GNU classpath 0.96.
private static final boolean isIphone = Boolean.getBoolean("net.mdns.isArmDarwin");
/**
* This is the multicast group, we are listening to for multicast DNS
* messages.
*/
private InetAddress group;
/**
* This is our multicast socket.
*/
private MulticastSocket socket;
/**
* Used to fix live lock problem on unregester.
*/
private boolean closed = false;
/**
* Holds instances of JmDNS.DNSListener. Must by a synchronized collection,
* because it is updated from concurrent threads.
*/
private List listeners;
/**
* Holds instances of ServiceListener's. Keys are Strings holding a fully
* qualified service type. Values are LinkedList's of ServiceListener's.
*/
private Map serviceListeners;
/**
* Holds instances of ServiceTypeListener's.
*/
private List typeListeners;
/**
* Cache for DNSEntry's.
*/
private DNSCache cache;
/**
* This hashtable holds the services that have been registered. Keys are
* instances of String which hold an all lower-case version of the fully
* qualified service name. Values are instances of ServiceInfo.
*/
Map services;
/**
* This hashtable holds the service types that have been registered or that
* have been received in an incoming datagram. Keys are instances of String
* which hold an all lower-case version of the fully qualified service type.
* Values hold the fully qualified service type.
*/
Map serviceTypes;
/**
* This is the shutdown hook, we registered with the java runtime.
*/
private Thread shutdown;
/**
* Handle on the local host
*/
private HostInfo localHost;
private Thread incomingListener = null;
/**
* Throttle count. This is used to count the overall number of probes sent
* by JmDNS. When the last throttle increment happened .
*/
private int throttle;
/**
* Last throttle increment.
*/
private long lastThrottleIncrement;
/**
* The timer is used to dispatch all outgoing messages of JmDNS. It is also
* used to dispatch maintenance tasks for the DNS cache.
*/
Timer timer;
/**
* The source for random values. This is used to introduce random delays in
* responses. This reduces the potential for collisions on the network.
*/
private final static Random random = new Random();
/**
* This lock is used to coordinate processing of incoming and outgoing
* messages. This is needed, because the Rendezvous Conformance Test does
* not forgive race conditions.
*/
private Object ioLock = new Object();
/**
* If an incoming package which needs an answer is truncated, we store it
* here. We add more incoming DNSRecords to it, until the JmDNS.Responder
* timer picks it up. Remind: This does not work well with multiple planned
* answers for packages that came in from different clients.
*/
private DNSIncoming plannedAnswer;
// State machine
/**
* The state of JmDNS. <p/> For proper handling of concurrency, this
* variable must be changed only using methods advanceState(), revertState()
* and cancel().
*/
private DNSState state = DNSState.PROBING_1;
/**
* Timer task associated to the host name. This is used to prevent from
* having multiple tasks associated to the host name at the same time.
*/
private TimerTask task;
/**
* This hashtable is used to maintain a list of service types being
* collected by this JmDNS instance. The key of the hashtable is a service
* type name, the value is an instance of JmDNS.ServiceCollector.
*
* @see #list
*/
private final HashMap serviceCollectors = new HashMap();
/**
* Create an instance of JmDNS.
*/
public JmDNSImpl() throws IOException {
// logger.finer("JmDNS instance created");
try {
// determine the interface on which to run on
InetAddress addr = null;
String ip = System.getProperty("net.mdns.interface");
if (ip != null) {
addr = InetAddress.getByName(ip);
init(addr, addr.getHostName());
} else {
addr = InetAddress.getLocalHost();
init(addr.isLoopbackAddress() ? null : addr, addr.getHostName());
}
// [
// PJYF
// Oct
// 14
// 2004
// ]
// Why
// do
// we
// disallow
// the
// loopback
// address
// ?
} catch (final IOException e) {
init(null, "computer");
}
}
/**
* Create an instance of JmDNS and bind it to a specific network interface
* given its IP-address.
*/
public JmDNSImpl(InetAddress addr) throws IOException {
try {
init(addr, addr.getHostName());
} catch (final IOException e) {
init(null, "computer");
}
}
/**
* Initialize everything.
*
* @param address
* The interface to which JmDNS binds to.
* @param name
* The host name of the interface.
*/
private void init(InetAddress address, String name) throws IOException {
// A host name with "." is illegal. so strip off everything and append .
// local.
final int idx = name.indexOf(".");
if (idx > 0) {
name = name.substring(0, idx);
}
name += ".local.";
// localHost to IP address binding
localHost = new HostInfo(address, name);
cache = new DNSCache(100);
listeners = Collections.synchronizedList(new ArrayList());
serviceListeners = new HashMap();
typeListeners = new ArrayList();
services = new Hashtable(20);
serviceTypes = new Hashtable(20);
// REMIND: If I could pass in a name for the Timer thread,
// I would pass' JmDNS.Timer'.
timer = new Timer();
new RecordReaper(this).start(timer);
shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
Runtime.getRuntime().addShutdownHook(shutdown);
incomingListener = new Thread(new SocketListener(this), "JmDNS.SocketListener");
// Bind to multicast socket
openMulticastSocket(getLocalHost());
start(getServices().values());
}
private void start(Collection serviceInfos) {
setState(DNSState.PROBING_1);
incomingListener.start();
new Prober(this).start(timer);
for (final Iterator iterator = serviceInfos.iterator(); iterator.hasNext();) {
try {
registerService(new ServiceInfoImpl((ServiceInfoImpl) iterator.next()));
} catch (final Exception exception) {
// logger.log(Level.WARNING, "start() Registration exception ", exception);
}
}
}
private void openMulticastSocket(HostInfo hostInfo) throws IOException {
if (group == null) {
group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
}
if (socket != null) {
this.closeMulticastSocket();
}
socket = new MulticastSocket(DNSConstants.MDNS_PORT);
if (!isIphone && (hostInfo != null) && (localHost.getInterface() != null)) {
socket.setNetworkInterface(hostInfo.getInterface());
}
socket.setTimeToLive(255);
socket.joinGroup(group);
}
private void closeMulticastSocket() {
// logger.finer("closeMulticastSocket()");
if (socket != null) {
// close socket
try {
socket.leaveGroup(group);
socket.close();
if (incomingListener != null) {
incomingListener.join();
}
} catch (final Exception exception) {
// logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ",
// exception);
}
socket = null;
}
}
// State machine
/**
* Sets the state and notifies all objects that wait on JmDNS.
*/
public synchronized void advanceState() {
setState(getState().advance());
notifyAll();
}
/**
* Sets the state and notifies all objects that wait on JmDNS.
*/
synchronized void revertState() {
setState(getState().revert());
notifyAll();
}
/**
* Sets the state and notifies all objects that wait on JmDNS.
*/
synchronized void cancel() {
setState(DNSState.CANCELED);
notifyAll();
}
/**
* Returns the current state of this info.
*/
public DNSState getState() {
return state;
}
/**
* Return the DNSCache associated with the cache variable
*/
public DNSCache getCache() {
return cache;
}
/**
* @see javax.jmdns.JmDNS#getHostName()
*/
public String getHostName() {
return localHost.getName();
}
public HostInfo getLocalHost() {
return localHost;
}
/**
* @see javax.jmdns.JmDNS#getInterface()
*/
public InetAddress getInterface() throws IOException {
return socket.getInterface();
}
/**
* @see javax.jmdns.JmDNS#getServiceInfo(java.lang.String, java.lang.String)
*/
public ServiceInfo getServiceInfo(String type, String name) {
return getServiceInfo(type, name, 3 * 1000);
}
/**
* @see javax.jmdns.JmDNS#getServiceInfo(java.lang.String, java.lang.String,
* int)
*/
public ServiceInfo getServiceInfo(String type, String name, int timeout) {
final ServiceInfoImpl info = new ServiceInfoImpl(type, name);
new ServiceInfoResolver(this, info).start(timer);
try {
final long end = System.currentTimeMillis() + timeout;
long delay;
synchronized (info) {
while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) {
info.wait(delay);
}
}
} catch (final InterruptedException e) {
// empty
}
return (info.hasData()) ? info : null;
}
/**
* @see javax.jmdns.JmDNS#requestServiceInfo(java.lang.String,
* java.lang.String)
*/
public void requestServiceInfo(String type, String name) {
requestServiceInfo(type, name, 3 * 1000);
}
/**
* @see javax.jmdns.JmDNS#requestServiceInfo(java.lang.String,
* java.lang.String, int)
*/
public void requestServiceInfo(String type, String name, int timeout) {
registerServiceType(type);
final ServiceInfoImpl info = new ServiceInfoImpl(type, name);
new ServiceInfoResolver(this, info).start(timer);
try {
final long end = System.currentTimeMillis() + timeout;
long delay;
synchronized (info) {
while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) {
info.wait(delay);
}
}
} catch (final InterruptedException e) {
// empty
}
}
void handleServiceResolved(ServiceInfoImpl info) {
List list = null;
ArrayList listCopy = null;
synchronized (serviceListeners) {
list = (List) serviceListeners.get(info.type.toLowerCase());
if (list != null) {
listCopy = new ArrayList(list);
}
}
if (listCopy != null) {
final ServiceEvent event = new ServiceEventImpl(this, info.type, info.getName(), info);
for (final Iterator iterator = listCopy.iterator(); iterator.hasNext();) {
((ServiceListener) iterator.next()).serviceResolved(event);
}
}
}
/**
* @see
* javax.jmdns.JmDNS#addServiceTypeListener(javax.jmdns.ServiceTypeListener
* )
*/
public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
synchronized (this) {
typeListeners.remove(listener);
typeListeners.add(listener);
}
// report cached service types
for (final Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();) {
listener.serviceTypeAdded(new ServiceEventImpl(this, (String) iterator.next(), null, null));
}
new TypeResolver(this).start(timer);
}
/**
* @see javax.jmdns.JmDNS#removeServiceTypeListener(javax.jmdns.
* ServiceTypeListener)
*/
public void removeServiceTypeListener(ServiceTypeListener listener) {
synchronized (this) {
typeListeners.remove(listener);
}
}
/**
* @see javax.jmdns.JmDNS#addServiceListener(java.lang.String,
* javax.jmdns.ServiceListener)
*/
public void addServiceListener(String type, ServiceListener listener) {
final String lotype = type.toLowerCase();
removeServiceListener(lotype, listener);
List list = null;
synchronized (serviceListeners) {
list = (List) serviceListeners.get(lotype);
if (list == null) {
list = Collections.synchronizedList(new LinkedList());
serviceListeners.put(lotype, list);
}
list.add(listener);
}
// report cached service types
final List serviceEvents = new ArrayList();
synchronized (cache) {
for (final Iterator i = cache.iterator(); i.hasNext();) {
for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next()) {
final DNSRecord rec = (DNSRecord) n.getValue();
if (rec.type == DNSConstants.TYPE_SRV) {
if (rec.name.endsWith(type)) {
serviceEvents.add(new ServiceEventImpl(this, type, toUnqualifiedName(type, rec.name), null));
}
}
}
}
}
// Actually call listener with all service events added above
for (final Iterator i = serviceEvents.iterator(); i.hasNext();) {
listener.serviceAdded((ServiceEventImpl) i.next());
}
// Create/start ServiceResolver
new ServiceResolver(this, type).start(timer);
}
/**
* @see javax.jmdns.JmDNS#removeServiceListener(java.lang.String,
* javax.jmdns.ServiceListener)
*/
public void removeServiceListener(String type, ServiceListener listener) {
type = type.toLowerCase();
List list = null;
synchronized (serviceListeners) {
list = (List) serviceListeners.get(type);
if (list != null) {
list.remove(listener);
if (list.size() == 0) {
serviceListeners.remove(type);
}
}
}
}
/**
* @see javax.jmdns.JmDNS#registerService(javax.jmdns.ServiceInfo)
*/
public void registerService(ServiceInfo infoAbstract) throws IOException {
final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;
registerServiceType(info.type);
// bind the service to this address
info.server = localHost.getName();
info.addr = localHost.getAddress();
synchronized (this) {
makeServiceNameUnique(info);
services.put(info.getQualifiedName().toLowerCase(), info);
}
new /* Service */Prober(this).start(timer);
try {
synchronized (info) {
while (info.getState().compareTo(DNSState.ANNOUNCED) < 0) {
info.wait();
}
}
} catch (final InterruptedException e) {
// empty
}
// logger.fine("registerService() JmDNS registered service as " + info);
}
/**
* @see javax.jmdns.JmDNS#unregisterService(javax.jmdns.ServiceInfo)
*/
public void unregisterService(ServiceInfo infoAbstract) {
final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;
synchronized (this) {
services.remove(info.getQualifiedName().toLowerCase());
}
info.cancel();
// Note: We use this lock object to synchronize on it.
// Synchronizing on another object (e.g. the ServiceInfo) does
// not make sense, because the sole purpose of the lock is to
// wait until the canceler has finished. If we synchronized on
// the ServiceInfo or on the Canceler, we would block all
// accesses to synchronized methods on that object. This is not
// what we want!
final Object lock = new Object();
// Remind: We get a deadlock here, if the Canceler does not run!
try {
synchronized (lock) {
new Canceler(this, info, lock).start(timer);
lock.wait();
}
} catch (final InterruptedException e) {
// empty
}
}
/**
* @see javax.jmdns.JmDNS#unregisterAllServices()
*/
public void unregisterAllServices() {
// logger.finer("unregisterAllServices()");
if (services.size() == 0) {
return;
}
Collection list;
synchronized (this) {
list = new LinkedList(services.values());
services.clear();
}
for (final Iterator iterator = list.iterator(); iterator.hasNext();) {
((ServiceInfoImpl) iterator.next()).cancel();
}
final Object lock = new Object();
new Canceler(this, list, lock).start(timer);
// Remind: We get a livelock here, if the Canceler does not run!
try {
synchronized (lock) {
if (!closed) {
lock.wait();
}
}
} catch (final InterruptedException e) {
// empty
}
}
/**
* @see javax.jmdns.JmDNS#registerServiceType(java.lang.String)
*/
public void registerServiceType(String type) {
final String name = type.toLowerCase();
if (serviceTypes.get(name) == null) {
if ((type.indexOf(DNSConstants.DNS_META_QUERY) < 0) && !type.endsWith(".in-addr.arpa.")) {
Collection list;
synchronized (this) {
serviceTypes.put(name, type);
list = new LinkedList(typeListeners);
}
for (final Iterator iterator = list.iterator(); iterator.hasNext();) {
((ServiceTypeListener) iterator.next()).serviceTypeAdded(new ServiceEventImpl(this, type, null, null));
}
}
}
}
/**
* Generate a possibly unique name for a host using the information we have
* in the cache.
*
* @return returns true, if the name of the host had to be changed.
*/
// private boolean makeHostNameUnique(DNSRecord.Address host)
// {
// final String originalName = host.getName();
// System.currentTimeMillis();
//
// boolean collision;
// do
// {
// collision = false;
//
// // Check for collision in cache
// for (DNSCache.CacheNode j = cache.find(host.getName().toLowerCase()); j != null; j = j
// .next())
// {
// if (false)
// {
// host.name = incrementName(host.getName());
// collision = true;
// break;
// }
// }
// }
// while (collision);
//
// if (originalName.equals(host.getName()))
// {
// return false;
// }
// else
// {
// return true;
// }
// }
/**
* Generate a possibly unique name for a service using the information we
* have in the cache.
*
* @return returns true, if the name of the service info had to be changed.
*/
private boolean makeServiceNameUnique(ServiceInfoImpl info) {
final String originalQualifiedName = info.getQualifiedName();
final long now = System.currentTimeMillis();
boolean collision;
do {
collision = false;
// Check for collision in cache
for (DNSCache.CacheNode j = cache.find(info.getQualifiedName().toLowerCase()); j != null; j = j.next()) {
final DNSRecord a = (DNSRecord) j.getValue();
if ((a.type == DNSConstants.TYPE_SRV) && !a.isExpired(now)) {
final DNSRecord.Service s = (DNSRecord.Service) a;
if (s.port != info.port || !s.server.equals(localHost.getName())) {
// logger
// .finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:"
// + a
// + " s.server="
// + s.server
// + " "
// + localHost.getName()
// + " equals:" + (s.server.equals(localHost.getName())));
info.setName(incrementName(info.getName()));
collision = true;
break;
}
}
}
// Check for collision with other service infos published by JmDNS
final Object selfService = services.get(info.getQualifiedName().toLowerCase());
if (selfService != null && selfService != info) {
info.setName(incrementName(info.getName()));
collision = true;
}
} while (collision);
return !(originalQualifiedName.equals(info.getQualifiedName()));
}
String incrementName(String name) {
try {
final int l = name.lastIndexOf('(');
final int r = name.lastIndexOf(')');
if ((l >= 0) && (l < r)) {
name = name.substring(0, l) + "(" + (Integer.parseInt(name.substring(l + 1, r)) + 1) + ")";
} else {
name += " (2)";
}
} catch (final NumberFormatException e) {
name += " (2)";
}
return name;
}
/**
* Add a listener for a question. The listener will receive updates of
* answers to the question as they arrive, or from the cache if they are
* already available.
*/
public void addListener(DNSListener listener, DNSQuestion question) {
final long now = System.currentTimeMillis();
// add the new listener
synchronized (this) {
listeners.add(listener);
}
// report existing matched records
if (question != null) {
for (DNSCache.CacheNode i = cache.find(question.name); i != null; i = i.next()) {
final DNSRecord c = (DNSRecord) i.getValue();
if (question.answeredBy(c) && !c.isExpired(now)) {
listener.updateRecord(this, now, c);
}
}
}
}
/**
* Remove a listener from all outstanding questions. The listener will no
* longer receive any updates.
*/
public void removeListener(DNSListener listener) {
synchronized (this) {
listeners.remove(listener);
}
}
// Remind: Method updateRecord should receive a better name.
/**
* Notify all listeners that a record was updated.
*/
public void updateRecord(long now, DNSRecord rec) {
// We do not want to block the entire DNS while we are updating the
// record for each listener (service info)
List listenerList = null;
synchronized (this) {
listenerList = new ArrayList(listeners);
}
for (final Iterator iterator = listenerList.iterator(); iterator.hasNext();) {
final DNSListener listener = (DNSListener) iterator.next();
listener.updateRecord(this, now, rec);
}
if (rec.type == DNSConstants.TYPE_PTR || rec.type == DNSConstants.TYPE_SRV) {
List serviceListenerList = null;
synchronized (serviceListeners) {
serviceListenerList = (List) serviceListeners.get(rec.name.toLowerCase());
// Iterate on a copy in case listeners will modify it
if (serviceListenerList != null) {
serviceListenerList = new ArrayList(serviceListenerList);
}
}
if (serviceListenerList != null) {
final boolean expired = rec.isExpired(now);
final String type = rec.getName();
final String name = ((DNSRecord.Pointer) rec).getAlias();
// DNSRecord old = (DNSRecord)services.get(name.toLowerCase());
if (!expired) {
// new record
final ServiceEvent event = new ServiceEventImpl(this, type, toUnqualifiedName(type, name), null);
for (final Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) {
((ServiceListener) iterator.next()).serviceAdded(event);
}
} else {
// expire record
final ServiceEvent event = new ServiceEventImpl(this, type, toUnqualifiedName(type, name), null);
for (final Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) {
((ServiceListener) iterator.next()).serviceRemoved(event);
}
}
}
}
}
/**
* Handle an incoming response. Cache answers, and pass them on to the
* appropriate questions.
*/
void handleResponse(DNSIncoming msg) throws IOException {
final long now = System.currentTimeMillis();
boolean hostConflictDetected = false;
boolean serviceConflictDetected = false;
for (final Iterator i = msg.answers.iterator(); i.hasNext();) {
boolean isInformative = false;
DNSRecord rec = (DNSRecord) i.next();
final boolean expired = rec.isExpired(now);
// update the cache
final DNSRecord c = (DNSRecord) cache.get(rec);
if (c != null) {
if (expired) {
isInformative = true;
cache.remove(c);
} else {
c.resetTTL(rec);
rec = c;
}
} else {
if (!expired) {
isInformative = true;
cache.add(rec);
}
}
switch (rec.type) {
case DNSConstants.TYPE_PTR :
// handle _mdns._udp records
if (rec.getName().indexOf(DNSConstants.DNS_META_QUERY) >= 0) {
if (!expired && rec.name.startsWith("_services" + DNSConstants.DNS_META_QUERY)) {
isInformative = true;
registerServiceType(((DNSRecord.Pointer) rec).alias);
}
continue;
}
registerServiceType(rec.name);
break;
}
if ((rec.getType() == DNSConstants.TYPE_A) || (rec.getType() == DNSConstants.TYPE_AAAA)) {
hostConflictDetected |= rec.handleResponse(this);
} else {
serviceConflictDetected |= rec.handleResponse(this);
}
// notify the listeners
if (isInformative) {
updateRecord(now, rec);
}
}
if (hostConflictDetected || serviceConflictDetected) {
new Prober(this).start(timer);
}
}
/**
* Handle an incoming query. See if we can answer any part of it given our
* service infos.
*/
void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException {
// Track known answers
boolean hostConflictDetected = false;
boolean serviceConflictDetected = false;
final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
for (final Iterator i = in.answers.iterator(); i.hasNext();) {
final DNSRecord answer = (DNSRecord) i.next();
if ((answer.getType() == DNSConstants.TYPE_A) || (answer.getType() == DNSConstants.TYPE_AAAA)) {
hostConflictDetected |= answer.handleQuery(this, expirationTime);
} else {
serviceConflictDetected |= answer.handleQuery(this, expirationTime);
}
}
if (plannedAnswer != null) {
plannedAnswer.append(in);
} else {
if (in.isTruncated()) {
plannedAnswer = in;
}
new Responder(this, in, addr, port).start();
}
if (hostConflictDetected || serviceConflictDetected) {
new Prober(this).start(timer);
}
}
/**
* Add an answer to a question. Deal with the case when the outgoing packet
* overflows
*/
public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException {
if (out == null) {
out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
}
try {
out.addAnswer(in, rec);
} catch (final IOException e) {
out.flags |= DNSConstants.FLAGS_TC;
out.id = in.id;
out.finish();
send(out);
out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
out.addAnswer(in, rec);
}
return out;
}
/**
* Send an outgoing multicast DNS message.
*/
public void send(DNSOutgoing out) throws IOException {
out.finish();
if (!out.isEmpty()) {
final DatagramPacket packet = new DatagramPacket(out.data, out.off, group, DNSConstants.MDNS_PORT);
try {
final DNSIncoming msg = new DNSIncoming(packet);
// logger.finest("send() JmDNS out:" + msg.print(true));
} catch (final IOException e) {
// logger.throwing(getClass().toString(),
// "send(DNSOutgoing) - JmDNS can not parse what it sends!!!", e);
}
final MulticastSocket ms = socket;
if (ms != null && !ms.isClosed())
ms.send(packet);
}
}
public void startAnnouncer() {
if (getState() != DNSState.CANCELED)
new Announcer(this).start(timer);
}
public void startRenewer() {
if (getState() != DNSState.CANCELED)
new Renewer(this).start(timer);
}
public void schedule(TimerTask task, int delay) {
if (getState() != DNSState.CANCELED)
timer.schedule(task, delay);
}
// REMIND: Why is this not an anonymous inner class?
/**
* Shutdown operations.
*/
private class Shutdown implements Runnable {
public void run() {
shutdown = null;
close();
}
}
/**
* Recover jmdns when there is an error.
*/
public void recover() {
// logger.finer("recover()");
// We have an IO error so lets try to recover if anything happens lets
// close it.
// This should cover the case of the IP address changing under our feet
if (DNSState.CANCELED != getState()) {
synchronized (this) { // Synchronize only if we are not already in process to prevent
// dead locks
//
// logger.finer("recover() Cleanning up");
// Stop JmDNS
setState(DNSState.CANCELED); // This protects against recursive
// calls
// We need to keep a copy for reregistration
final Collection oldServiceInfos = new ArrayList(getServices().values());
// Cancel all services
unregisterAllServices();
disposeServiceCollectors();
//
// close multicast socket
closeMulticastSocket();
//
cache.clear();
// logger.finer("recover() All is clean");
//
// All is clear now start the services
//
try {
openMulticastSocket(getLocalHost());
start(oldServiceInfos);
} catch (final Exception exception) {
// logger.log(Level.WARNING, "recover() Start services exception ", exception);
}
// logger.log(Level.WARNING, "recover() We are back!");
}
}
}
/**
* @see javax.jmdns.JmDNS#close()
*/
public void close() {
if (getState() != DNSState.CANCELED) {
synchronized (this) { // Synchronize only if we are not already in process to prevent
// dead locks
// Stop JmDNS
setState(DNSState.CANCELED); // This protects against recursive
// calls
unregisterAllServices();
// Stop the timer
timer.cancel();
disposeServiceCollectors();
// close socket
closeMulticastSocket();
// remove the shutdown hook
if (shutdown != null) {
Runtime.getRuntime().removeShutdownHook(shutdown);
}
}
}
}
/**
* List cache entries, for debugging only.
*/
void print() {
System.out.println("---- cache ----");
cache.print();
System.out.println();
}
/**
* @see javax.jmdns.JmDNS#printServices()
*/
public void printServices() {
System.err.println(toString());
}
public String toString() {
final StringBuffer aLog = new StringBuffer();
aLog.append("\t---- Services -----");
if (services != null) {
for (final Iterator k = services.keySet().iterator(); k.hasNext();) {
final Object key = k.next();
aLog.append("\n\t\tService: " + key + ": " + services.get(key));
}
}
aLog.append("\n");
aLog.append("\t---- Types ----");
if (serviceTypes != null) {
for (final Iterator k = serviceTypes.keySet().iterator(); k.hasNext();) {
final Object key = k.next();
aLog.append("\n\t\tType: " + key + ": " + serviceTypes.get(key));
}
}
aLog.append("\n");
aLog.append(cache.toString());
aLog.append("\n");
aLog.append("\t---- Service Collectors ----");
if (serviceCollectors != null) {
synchronized (serviceCollectors) {
for (final Iterator k = serviceCollectors.keySet().iterator(); k.hasNext();) {
final Object key = k.next();
aLog.append("\n\t\tService Collector: " + key + ": " + serviceCollectors.get(key));
}
serviceCollectors.clear();
}
}
return aLog.toString();
}
/**
* @see javax.jmdns.JmDNS#list(java.lang.String)
*/
public ServiceInfo[] list(String type) {
// Implementation note: The first time a list for a given type is
// requested, a ServiceCollector is created which collects service
// infos. This greatly speeds up the performance of subsequent calls
// to this method. The caveats are, that 1) the first call to this
// method
// for a given type is slow, and 2) we spawn a ServiceCollector
// instance for each service type which increases network traffic a
// little.
ServiceCollector collector;
boolean newCollectorCreated;
synchronized (serviceCollectors) {
collector = (ServiceCollector) serviceCollectors.get(type);
if (collector == null) {
collector = new ServiceCollector(type);
serviceCollectors.put(type, collector);
addServiceListener(type, collector);
newCollectorCreated = true;
} else {
newCollectorCreated = false;
}
}
// After creating a new ServiceCollector, we collect service infos for
// 200 milliseconds. This should be enough time, to get some service
// infos from the network.
if (newCollectorCreated) {
try {
Thread.sleep(200);
} catch (final InterruptedException e) {
}
}
return collector.list();
}
/**
* This method disposes all ServiceCollector instances which have been
* created by calls to method <code>list(type)</code>.
*
* @see #list
*/
private void disposeServiceCollectors() {
// logger.finer("disposeServiceCollectors()");
synchronized (serviceCollectors) {
for (final Iterator i = serviceCollectors.values().iterator(); i.hasNext();) {
final ServiceCollector collector = (ServiceCollector) i.next();
removeServiceListener(collector.type, collector);
}
serviceCollectors.clear();
}
}
/**
* Instances of ServiceCollector are used internally to speed up the
* performance of method <code>list(type)</code>.
*
* @see #list
*/
private static class ServiceCollector implements ServiceListener {
// private static Logger logger = Logger.getLogger(ServiceCollector.class.getName());
/**
* A set of collected service instance names.
*/
private final Map infos = Collections.synchronizedMap(new HashMap());
public String type;
public ServiceCollector(String type) {
this.type = type;
}
/**
* A service has been added.
*/
public void serviceAdded(ServiceEvent event) {
synchronized (infos) {
event.getDNS().requestServiceInfo(event.getType(), event.getName(), 0);
}
}
/**
* A service has been removed.
*/
public void serviceRemoved(ServiceEvent event) {
synchronized (infos) {
infos.remove(event.getName());
}
}
/**
* A service hase been resolved. Its details are now available in the
* ServiceInfo record.
*/
public void serviceResolved(ServiceEvent event) {
synchronized (infos) {
infos.put(event.getName(), event.getInfo());
}
}
/**
* Returns an array of all service infos which have been collected by
* this ServiceCollector.
*/
public ServiceInfoImpl[] list() {
synchronized (infos) {
return (ServiceInfoImpl[]) infos.values().toArray(new ServiceInfoImpl[infos.size()]);
}
}
public String toString() {
final StringBuffer aLog = new StringBuffer();
synchronized (infos) {
for (final Iterator k = infos.keySet().iterator(); k.hasNext();) {
final Object key = k.next();
aLog.append("\n\t\tService: " + key + ": " + infos.get(key));
}
}
return aLog.toString();
}
};
private static String toUnqualifiedName(String type, String qualifiedName) {
if (qualifiedName.endsWith(type)) {
return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
} else {
return qualifiedName;
}
}
public void setState(DNSState state) {
this.state = state;
}
public void setTask(TimerTask task) {
this.task = task;
}
public TimerTask getTask() {
return task;
}
public Map getServices() {
return services;
}
public void setLastThrottleIncrement(long lastThrottleIncrement) {
this.lastThrottleIncrement = lastThrottleIncrement;
}
public long getLastThrottleIncrement() {
return lastThrottleIncrement;
}
public void setThrottle(int throttle) {
this.throttle = throttle;
}
public int getThrottle() {
return throttle;
}
public static Random getRandom() {
return random;
}
public void setIoLock(Object ioLock) {
this.ioLock = ioLock;
}
public Object getIoLock() {
return ioLock;
}
public void setPlannedAnswer(DNSIncoming plannedAnswer) {
this.plannedAnswer = plannedAnswer;
}
public DNSIncoming getPlannedAnswer() {
return plannedAnswer;
}
void setLocalHost(HostInfo localHost) {
this.localHost = localHost;
}
public Map getServiceTypes() {
return serviceTypes;
}
public void setClosed(boolean closed) {
this.closed = closed;
}
public boolean isClosed() {
return closed;
}
public MulticastSocket getSocket() {
return socket;
}
public InetAddress getGroup() {
return group;
}
}