1 // /Copyright 2003-2005 Arthur van Hoff, Rick Blair
2 // Licensed under Apache License version 2.0
3 // Original license LGPL
4 
5 package javax.jmdns.impl;
6 
7 import java.io.IOException;
8 import java.net.DatagramPacket;
9 import java.net.Inet4Address;
10 import java.net.Inet6Address;
11 import java.net.InetAddress;
12 import java.net.MulticastSocket;
13 import java.net.SocketException;
14 import java.util.AbstractMap;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.Iterator;
21 import java.util.LinkedList;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Properties;
25 import java.util.Random;
26 import java.util.Set;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ConcurrentMap;
29 import java.util.concurrent.ExecutorService;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34 
35 import javax.jmdns.JmDNS;
36 import javax.jmdns.ServiceEvent;
37 import javax.jmdns.ServiceInfo;
38 import javax.jmdns.ServiceInfo.Fields;
39 import javax.jmdns.ServiceListener;
40 import javax.jmdns.ServiceTypeListener;
41 import javax.jmdns.impl.ListenerStatus.ServiceListenerStatus;
42 import javax.jmdns.impl.ListenerStatus.ServiceTypeListenerStatus;
43 import javax.jmdns.impl.constants.DNSConstants;
44 import javax.jmdns.impl.constants.DNSRecordClass;
45 import javax.jmdns.impl.constants.DNSRecordType;
46 import javax.jmdns.impl.constants.DNSState;
47 import javax.jmdns.impl.tasks.DNSTask;
48 
49 // REMIND: multiple IP addresses
50 
51 /**
52  * mDNS implementation in Java.
53  *
54  * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Scott Lewis
55  */
56 public class JmDNSImpl extends JmDNS implements DNSStatefulObject, DNSTaskStarter {
57     private static Logger logger = Logger.getLogger(JmDNSImpl.class.getName());
58 
59     public enum Operation {
60         Remove, Update, Add, RegisterServiceType, Noop
61     }
62 
63     /**
64      * This is the multicast group, we are listening to for multicast DNS messages.
65      */
66     private volatile InetAddress                                     _group;
67     /**
68      * This is our multicast socket.
69      */
70     private volatile MulticastSocket                                 _socket;
71 
72     /**
73      * Holds instances of JmDNS.DNSListener. Must by a synchronized collection, because it is updated from concurrent threads.
74      */
75     private final List<DNSListener>                                  _listeners;
76 
77     /**
78      * Holds instances of ServiceListener's. Keys are Strings holding a fully qualified service type. Values are LinkedList's of ServiceListener's.
79      */
80     private final ConcurrentMap<String, List<ServiceListenerStatus>> _serviceListeners;
81 
82     /**
83      * Holds instances of ServiceTypeListener's.
84      */
85     private final Set<ServiceTypeListenerStatus>                     _typeListeners;
86 
87     /**
88      * Cache for DNSEntry's.
89      */
90     private final DNSCache                                           _cache;
91 
92     /**
93      * 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.
94      */
95     private final ConcurrentMap<String, ServiceInfo>                 _services;
96 
97     /**
98      * This hashtable holds the service types that have been registered or that have been received in an incoming datagram.<br/>
99      * Keys are instances of String which hold an all lower-case version of the fully qualified service type.<br/>
100      * Values hold the fully qualified service type.
101      */
102     private final ConcurrentMap<String, ServiceTypeEntry>            _serviceTypes;
103 
104     private volatile Delegate                                        _delegate;
105 
106     /**
107      * This is used to store type entries. The type is stored as a call variable and the map support the subtypes.
108      * <p>
109      * The key is the lowercase version as the value is the case preserved version.
110      * </p>
111      */
112     public static class ServiceTypeEntry extends AbstractMap<String, String> implements Cloneable {
113 
114         private final Set<Map.Entry<String, String>> _entrySet;
115 
116         private final String                         _type;
117 
118         private static class SubTypeEntry implements Entry<String, String>, java.io.Serializable, Cloneable {
119 
120             private static final long serialVersionUID = 9188503522395855322L;
121 
122             private final String      _key;
123             private final String      _value;
124 
SubTypeEntry(String subtype)125             public SubTypeEntry(String subtype) {
126                 super();
127                 _value = (subtype != null ? subtype : "");
128                 _key = _value.toLowerCase();
129             }
130 
131             /**
132              * {@inheritDoc}
133              */
134             @Override
getKey()135             public String getKey() {
136                 return _key;
137             }
138 
139             /**
140              * {@inheritDoc}
141              */
142             @Override
getValue()143             public String getValue() {
144                 return _value;
145             }
146 
147             /**
148              * Replaces the value corresponding to this entry with the specified value (optional operation). This implementation simply throws <tt>UnsupportedOperationException</tt>, as this class implements an <i>immutable</i> map entry.
149              *
150              * @param value
151              *            new value to be stored in this entry
152              * @return (Does not return)
153              * @exception UnsupportedOperationException
154              *                always
155              */
156             @Override
setValue(String value)157             public String setValue(String value) {
158                 throw new UnsupportedOperationException();
159             }
160 
161             /**
162              * {@inheritDoc}
163              */
164             @Override
equals(Object entry)165             public boolean equals(Object entry) {
166                 if (!(entry instanceof Map.Entry)) {
167                     return false;
168                 }
169                 return this.getKey().equals(((Map.Entry<?, ?>) entry).getKey()) && this.getValue().equals(((Map.Entry<?, ?>) entry).getValue());
170             }
171 
172             /**
173              * {@inheritDoc}
174              */
175             @Override
hashCode()176             public int hashCode() {
177                 return (_key == null ? 0 : _key.hashCode()) ^ (_value == null ? 0 : _value.hashCode());
178             }
179 
180             /*
181              * (non-Javadoc)
182              * @see java.lang.Object#clone()
183              */
184             @Override
clone()185             public SubTypeEntry clone() {
186                 // Immutable object
187                 return this;
188             }
189 
190             /**
191              * {@inheritDoc}
192              */
193             @Override
toString()194             public String toString() {
195                 return _key + "=" + _value;
196             }
197 
198         }
199 
ServiceTypeEntry(String type)200         public ServiceTypeEntry(String type) {
201             super();
202             this._type = type;
203             this._entrySet = new HashSet<Map.Entry<String, String>>();
204         }
205 
206         /**
207          * The type associated with this entry.
208          *
209          * @return the type
210          */
getType()211         public String getType() {
212             return _type;
213         }
214 
215         /*
216          * (non-Javadoc)
217          * @see java.util.AbstractMap#entrySet()
218          */
219         @Override
entrySet()220         public Set<Map.Entry<String, String>> entrySet() {
221             return _entrySet;
222         }
223 
224         /**
225          * Returns <code>true</code> if this set contains the specified element. More formally, returns <code>true</code> if and only if this set contains an element <code>e</code> such that
226          * <code>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</code>.
227          *
228          * @param subtype
229          *            element whose presence in this set is to be tested
230          * @return <code>true</code> if this set contains the specified element
231          */
contains(String subtype)232         public boolean contains(String subtype) {
233             return subtype != null && this.containsKey(subtype.toLowerCase());
234         }
235 
236         /**
237          * Adds the specified element to this set if it is not already present. More formally, adds the specified element <code>e</code> to this set if this set contains no element <code>e2</code> such that
238          * <code>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</code>. If this set already contains the element, the call leaves the set unchanged and returns <code>false</code>.
239          *
240          * @param subtype
241          *            element to be added to this set
242          * @return <code>true</code> if this set did not already contain the specified element
243          */
add(String subtype)244         public boolean add(String subtype) {
245             if (subtype == null || this.contains(subtype)) {
246                 return false;
247             }
248             _entrySet.add(new SubTypeEntry(subtype));
249             return true;
250         }
251 
252         /**
253          * Returns an iterator over the elements in this set. The elements are returned in no particular order (unless this set is an instance of some class that provides a guarantee).
254          *
255          * @return an iterator over the elements in this set
256          */
iterator()257         public Iterator<String> iterator() {
258             return this.keySet().iterator();
259         }
260 
261         /*
262          * (non-Javadoc)
263          * @see java.util.AbstractMap#clone()
264          */
265         @Override
clone()266         public ServiceTypeEntry clone() {
267             ServiceTypeEntry entry = new ServiceTypeEntry(this.getType());
268             for (Map.Entry<String, String> subTypeEntry : this.entrySet()) {
269                 entry.add(subTypeEntry.getValue());
270             }
271             return entry;
272         }
273 
274         /*
275          * (non-Javadoc)
276          * @see java.util.AbstractMap#toString()
277          */
278         @Override
toString()279         public String toString() {
280             final StringBuilder aLog = new StringBuilder(200);
281             if (this.isEmpty()) {
282                 aLog.append("empty");
283             } else {
284                 for (String value : this.values()) {
285                     aLog.append(value);
286                     aLog.append(", ");
287                 }
288                 aLog.setLength(aLog.length() - 2);
289             }
290             return aLog.toString();
291         }
292 
293     }
294 
295     /**
296      * This is the shutdown hook, we registered with the java runtime.
297      */
298     protected Thread                                      _shutdown;
299 
300     /**
301      * Handle on the local host
302      */
303     private HostInfo                                      _localHost;
304 
305     private Thread                                        _incomingListener;
306 
307     /**
308      * Throttle count. This is used to count the overall number of probes sent by JmDNS. When the last throttle increment happened .
309      */
310     private int                                           _throttle;
311 
312     /**
313      * Last throttle increment.
314      */
315     private long                                          _lastThrottleIncrement;
316 
317     private final ExecutorService                         _executor = Executors.newSingleThreadExecutor();
318 
319     //
320     // 2009-09-16 ldeck: adding docbug patch with slight ammendments
321     // 'Fixes two deadlock conditions involving JmDNS.close() - ID: 1473279'
322     //
323     // ---------------------------------------------------
324     /**
325      * The timer that triggers our announcements. We can't use the main timer object, because that could cause a deadlock where Prober waits on JmDNS.this lock held by close(), close() waits for us to finish, and we wait for Prober to give us back
326      * the timer thread so we can announce. (Patch from docbug in 2006-04-19 still wasn't patched .. so I'm doing it!)
327      */
328     // private final Timer _cancelerTimer;
329     // ---------------------------------------------------
330 
331     /**
332      * The source for random values. This is used to introduce random delays in responses. This reduces the potential for collisions on the network.
333      */
334     private final static Random                           _random   = new Random();
335 
336     /**
337      * 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.
338      */
339     private final ReentrantLock                           _ioLock   = new ReentrantLock();
340 
341     /**
342      * 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.<br/>
343      * FIXME [PJYF June 8 2010]: This does not work well with multiple planned answers for packages that came in from different clients.
344      */
345     private DNSIncoming                                   _plannedAnswer;
346 
347     // State machine
348 
349     /**
350      * 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.
351      *
352      * @see #list
353      */
354     private final ConcurrentMap<String, ServiceCollector> _serviceCollectors;
355 
356     private final String                                  _name;
357 
358     /**
359      * Main method to display API information if run from java -jar
360      *
361      * @param argv
362      *            the command line arguments
363      */
main(String[] argv)364     public static void main(String[] argv) {
365         String version = null;
366         try {
367             final Properties pomProperties = new Properties();
368             pomProperties.load(JmDNSImpl.class.getResourceAsStream("/META-INF/maven/javax.jmdns/jmdns/pom.properties"));
369             version = pomProperties.getProperty("version");
370         } catch (Exception e) {
371             version = "RUNNING.IN.IDE.FULL";
372         }
373         System.out.println("JmDNS version \"" + version + "\"");
374         System.out.println(" ");
375 
376         System.out.println("Running on java version \"" + System.getProperty("java.version") + "\"" + " (build " + System.getProperty("java.runtime.version") + ")" + " from " + System.getProperty("java.vendor"));
377 
378         System.out.println("Operating environment \"" + System.getProperty("os.name") + "\"" + " version " + System.getProperty("os.version") + " on " + System.getProperty("os.arch"));
379 
380         System.out.println("For more information on JmDNS please visit https://sourceforge.net/projects/jmdns/");
381     }
382 
383     /**
384      * Create an instance of JmDNS and bind it to a specific network interface given its IP-address.
385      *
386      * @param address
387      *            IP address to bind to.
388      * @param name
389      *            name of the newly created JmDNS
390      * @exception IOException
391      */
JmDNSImpl(InetAddress address, String name)392     public JmDNSImpl(InetAddress address, String name) throws IOException {
393         super();
394         if (logger.isLoggable(Level.FINER)) {
395             logger.finer("JmDNS instance created");
396         }
397         _cache = new DNSCache(100);
398 
399         _listeners = Collections.synchronizedList(new ArrayList<DNSListener>());
400         _serviceListeners = new ConcurrentHashMap<String, List<ServiceListenerStatus>>();
401         _typeListeners = Collections.synchronizedSet(new HashSet<ServiceTypeListenerStatus>());
402         _serviceCollectors = new ConcurrentHashMap<String, ServiceCollector>();
403 
404         _services = new ConcurrentHashMap<String, ServiceInfo>(20);
405         _serviceTypes = new ConcurrentHashMap<String, ServiceTypeEntry>(20);
406 
407         _localHost = HostInfo.newHostInfo(address, this, name);
408         _name = (name != null ? name : _localHost.getName());
409 
410         // _cancelerTimer = new Timer("JmDNS.cancelerTimer");
411 
412         // (ldeck 2.1.1) preventing shutdown blocking thread
413         // -------------------------------------------------
414         // _shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
415         // Runtime.getRuntime().addShutdownHook(_shutdown);
416 
417         // -------------------------------------------------
418 
419         // Bind to multicast socket
420         this.openMulticastSocket(this.getLocalHost());
421         this.start(this.getServices().values());
422 
423         this.startReaper();
424     }
425 
start(Collection<? extends ServiceInfo> serviceInfos)426     private void start(Collection<? extends ServiceInfo> serviceInfos) {
427         if (_incomingListener == null) {
428             _incomingListener = new SocketListener(this);
429             _incomingListener.start();
430         }
431         this.startProber();
432         for (ServiceInfo info : serviceInfos) {
433             try {
434                 this.registerService(new ServiceInfoImpl(info));
435             } catch (final Exception exception) {
436                 logger.log(Level.WARNING, "start() Registration exception ", exception);
437             }
438         }
439     }
440 
openMulticastSocket(HostInfo hostInfo)441     private void openMulticastSocket(HostInfo hostInfo) throws IOException {
442         if (_group == null) {
443             if (hostInfo.getInetAddress() instanceof Inet6Address) {
444                 _group = InetAddress.getByName(DNSConstants.MDNS_GROUP_IPV6);
445             } else {
446                 _group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
447             }
448         }
449         if (_socket != null) {
450             this.closeMulticastSocket();
451         }
452         _socket = new MulticastSocket(DNSConstants.MDNS_PORT);
453         if ((hostInfo != null) && (hostInfo.getInterface() != null)) {
454             try {
455                 _socket.setNetworkInterface(hostInfo.getInterface());
456             } catch (SocketException e) {
457                 if (logger.isLoggable(Level.FINE)) {
458                     logger.fine("openMulticastSocket() Set network interface exception: " + e.getMessage());
459                 }
460             }
461         }
462         _socket.setTimeToLive(255);
463         _socket.joinGroup(_group);
464     }
465 
closeMulticastSocket()466     private void closeMulticastSocket() {
467         // jP: 20010-01-18. See below. We'll need this monitor...
468         // assert (Thread.holdsLock(this));
469         if (logger.isLoggable(Level.FINER)) {
470             logger.finer("closeMulticastSocket()");
471         }
472         if (_socket != null) {
473             // close socket
474             try {
475                 try {
476                     _socket.leaveGroup(_group);
477                 } catch (SocketException exception) {
478                     //
479                 }
480                 _socket.close();
481                 // jP: 20010-01-18. It isn't safe to join() on the listener
482                 // thread - it attempts to lock the IoLock object, and deadlock
483                 // ensues. Per issue #2933183, changed this to wait on the JmDNS
484                 // monitor, checking on each notify (or timeout) that the
485                 // listener thread has stopped.
486                 //
487                 while (_incomingListener != null && _incomingListener.isAlive()) {
488                     synchronized (this) {
489                         try {
490                             if (_incomingListener != null && _incomingListener.isAlive()) {
491                                 // wait time is arbitrary, we're really expecting notification.
492                                 if (logger.isLoggable(Level.FINER)) {
493                                     logger.finer("closeMulticastSocket(): waiting for jmDNS monitor");
494                                 }
495                                 this.wait(1000);
496                             }
497                         } catch (InterruptedException ignored) {
498                             // Ignored
499                         }
500                     }
501                 }
502                 _incomingListener = null;
503             } catch (final Exception exception) {
504                 logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", exception);
505             }
506             _socket = null;
507         }
508     }
509 
510     // State machine
511     /**
512      * {@inheritDoc}
513      */
514     @Override
advanceState(DNSTask task)515     public boolean advanceState(DNSTask task) {
516         return this._localHost.advanceState(task);
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
revertState()523     public boolean revertState() {
524         return this._localHost.revertState();
525     }
526 
527     /**
528      * {@inheritDoc}
529      */
530     @Override
cancelState()531     public boolean cancelState() {
532         return this._localHost.cancelState();
533     }
534 
535     /**
536      * {@inheritDoc}
537      */
538     @Override
closeState()539     public boolean closeState() {
540         return this._localHost.closeState();
541     }
542 
543     /**
544      * {@inheritDoc}
545      */
546     @Override
recoverState()547     public boolean recoverState() {
548         return this._localHost.recoverState();
549     }
550 
551     /**
552      * {@inheritDoc}
553      */
554     @Override
getDns()555     public JmDNSImpl getDns() {
556         return this;
557     }
558 
559     /**
560      * {@inheritDoc}
561      */
562     @Override
associateWithTask(DNSTask task, DNSState state)563     public void associateWithTask(DNSTask task, DNSState state) {
564         this._localHost.associateWithTask(task, state);
565     }
566 
567     /**
568      * {@inheritDoc}
569      */
570     @Override
removeAssociationWithTask(DNSTask task)571     public void removeAssociationWithTask(DNSTask task) {
572         this._localHost.removeAssociationWithTask(task);
573     }
574 
575     /**
576      * {@inheritDoc}
577      */
578     @Override
isAssociatedWithTask(DNSTask task, DNSState state)579     public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
580         return this._localHost.isAssociatedWithTask(task, state);
581     }
582 
583     /**
584      * {@inheritDoc}
585      */
586     @Override
isProbing()587     public boolean isProbing() {
588         return this._localHost.isProbing();
589     }
590 
591     /**
592      * {@inheritDoc}
593      */
594     @Override
isAnnouncing()595     public boolean isAnnouncing() {
596         return this._localHost.isAnnouncing();
597     }
598 
599     /**
600      * {@inheritDoc}
601      */
602     @Override
isAnnounced()603     public boolean isAnnounced() {
604         return this._localHost.isAnnounced();
605     }
606 
607     /**
608      * {@inheritDoc}
609      */
610     @Override
isCanceling()611     public boolean isCanceling() {
612         return this._localHost.isCanceling();
613     }
614 
615     /**
616      * {@inheritDoc}
617      */
618     @Override
isCanceled()619     public boolean isCanceled() {
620         return this._localHost.isCanceled();
621     }
622 
623     /**
624      * {@inheritDoc}
625      */
626     @Override
isClosing()627     public boolean isClosing() {
628         return this._localHost.isClosing();
629     }
630 
631     /**
632      * {@inheritDoc}
633      */
634     @Override
isClosed()635     public boolean isClosed() {
636         return this._localHost.isClosed();
637     }
638 
639     /**
640      * {@inheritDoc}
641      */
642     @Override
waitForAnnounced(long timeout)643     public boolean waitForAnnounced(long timeout) {
644         return this._localHost.waitForAnnounced(timeout);
645     }
646 
647     /**
648      * {@inheritDoc}
649      */
650     @Override
waitForCanceled(long timeout)651     public boolean waitForCanceled(long timeout) {
652         return this._localHost.waitForCanceled(timeout);
653     }
654 
655     /**
656      * Return the DNSCache associated with the cache variable
657      *
658      * @return DNS cache
659      */
getCache()660     public DNSCache getCache() {
661         return _cache;
662     }
663 
664     /**
665      * {@inheritDoc}
666      */
667     @Override
getName()668     public String getName() {
669         return _name;
670     }
671 
672     /**
673      * {@inheritDoc}
674      */
675     @Override
getHostName()676     public String getHostName() {
677         return _localHost.getName();
678     }
679 
680     /**
681      * Returns the local host info
682      *
683      * @return local host info
684      */
getLocalHost()685     public HostInfo getLocalHost() {
686         return _localHost;
687     }
688 
689     /**
690      * {@inheritDoc}
691      */
692     @Override
getInetAddress()693     public InetAddress getInetAddress() throws IOException {
694         return _localHost.getInetAddress();
695     }
696 
697     /**
698      * {@inheritDoc}
699      */
700     @Override
701     @Deprecated
getInterface()702     public InetAddress getInterface() throws IOException {
703         return _socket.getInterface();
704     }
705 
706     /**
707      * {@inheritDoc}
708      */
709     @Override
getServiceInfo(String type, String name)710     public ServiceInfo getServiceInfo(String type, String name) {
711         return this.getServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
712     }
713 
714     /**
715      * {@inheritDoc}
716      */
717     @Override
getServiceInfo(String type, String name, long timeout)718     public ServiceInfo getServiceInfo(String type, String name, long timeout) {
719         return this.getServiceInfo(type, name, false, timeout);
720     }
721 
722     /**
723      * {@inheritDoc}
724      */
725     @Override
getServiceInfo(String type, String name, boolean persistent)726     public ServiceInfo getServiceInfo(String type, String name, boolean persistent) {
727         return this.getServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
728     }
729 
730     /**
731      * {@inheritDoc}
732      */
733     @Override
getServiceInfo(String type, String name, boolean persistent, long timeout)734     public ServiceInfo getServiceInfo(String type, String name, boolean persistent, long timeout) {
735         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
736         this.waitForInfoData(info, timeout);
737         return (info.hasData() ? info : null);
738     }
739 
resolveServiceInfo(String type, String name, String subtype, boolean persistent)740     ServiceInfoImpl resolveServiceInfo(String type, String name, String subtype, boolean persistent) {
741         this.cleanCache();
742         String loType = type.toLowerCase();
743         this.registerServiceType(type);
744         if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
745             this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
746         }
747 
748         // Check if the answer is in the cache.
749         final ServiceInfoImpl info = this.getServiceInfoFromCache(type, name, subtype, persistent);
750         // We still run the resolver to do the dispatch but if the info is already there it will quit immediately
751         this.startServiceInfoResolver(info);
752 
753         return info;
754     }
755 
getServiceInfoFromCache(String type, String name, String subtype, boolean persistent)756     ServiceInfoImpl getServiceInfoFromCache(String type, String name, String subtype, boolean persistent) {
757         // Check if the answer is in the cache.
758         ServiceInfoImpl info = new ServiceInfoImpl(type, name, subtype, 0, 0, 0, persistent, (byte[]) null);
759         DNSEntry pointerEntry = this.getCache().getDNSEntry(new DNSRecord.Pointer(type, DNSRecordClass.CLASS_ANY, false, 0, info.getQualifiedName()));
760         if (pointerEntry instanceof DNSRecord) {
761             ServiceInfoImpl cachedInfo = (ServiceInfoImpl) ((DNSRecord) pointerEntry).getServiceInfo(persistent);
762             if (cachedInfo != null) {
763                 // To get a complete info record we need to retrieve the service, address and the text bytes.
764 
765                 Map<Fields, String> map = cachedInfo.getQualifiedNameMap();
766                 byte[] srvBytes = null;
767                 String server = "";
768                 DNSEntry serviceEntry = this.getCache().getDNSEntry(info.getQualifiedName(), DNSRecordType.TYPE_SRV, DNSRecordClass.CLASS_ANY);
769                 if (serviceEntry instanceof DNSRecord) {
770                     ServiceInfo cachedServiceEntryInfo = ((DNSRecord) serviceEntry).getServiceInfo(persistent);
771                     if (cachedServiceEntryInfo != null) {
772                         cachedInfo = new ServiceInfoImpl(map, cachedServiceEntryInfo.getPort(), cachedServiceEntryInfo.getWeight(), cachedServiceEntryInfo.getPriority(), persistent, (byte[]) null);
773                         srvBytes = cachedServiceEntryInfo.getTextBytes();
774                         server = cachedServiceEntryInfo.getServer();
775                     }
776                 }
777                 DNSEntry addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_ANY);
778                 if (addressEntry instanceof DNSRecord) {
779                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
780                     if (cachedAddressInfo != null) {
781                         for (Inet4Address address : cachedAddressInfo.getInet4Addresses()) {
782                             cachedInfo.addAddress(address);
783                         }
784                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
785                     }
786                 }
787                 addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_ANY);
788                 if (addressEntry instanceof DNSRecord) {
789                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
790                     if (cachedAddressInfo != null) {
791                         for (Inet6Address address : cachedAddressInfo.getInet6Addresses()) {
792                             cachedInfo.addAddress(address);
793                         }
794                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
795                     }
796                 }
797                 DNSEntry textEntry = this.getCache().getDNSEntry(cachedInfo.getQualifiedName(), DNSRecordType.TYPE_TXT, DNSRecordClass.CLASS_ANY);
798                 if (textEntry instanceof DNSRecord) {
799                     ServiceInfo cachedTextInfo = ((DNSRecord) textEntry).getServiceInfo(persistent);
800                     if (cachedTextInfo != null) {
801                         cachedInfo._setText(cachedTextInfo.getTextBytes());
802                     }
803                 }
804                 if (cachedInfo.getTextBytes().length == 0) {
805                     cachedInfo._setText(srvBytes);
806                 }
807                 if (cachedInfo.hasData()) {
808                     info = cachedInfo;
809                 }
810             }
811         }
812         return info;
813     }
814 
waitForInfoData(ServiceInfo info, long timeout)815     private void waitForInfoData(ServiceInfo info, long timeout) {
816         synchronized (info) {
817             long loops = (timeout / 200L);
818             if (loops < 1) {
819                 loops = 1;
820             }
821             for (int i = 0; i < loops; i++) {
822                 if (info.hasData()) {
823                     break;
824                 }
825                 try {
826                     info.wait(200);
827                 } catch (final InterruptedException e) {
828                     /* Stub */
829                 }
830             }
831         }
832     }
833 
834     /**
835      * {@inheritDoc}
836      */
837     @Override
requestServiceInfo(String type, String name)838     public void requestServiceInfo(String type, String name) {
839         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
840     }
841 
842     /**
843      * {@inheritDoc}
844      */
845     @Override
requestServiceInfo(String type, String name, boolean persistent)846     public void requestServiceInfo(String type, String name, boolean persistent) {
847         this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
848     }
849 
850     /**
851      * {@inheritDoc}
852      */
853     @Override
requestServiceInfo(String type, String name, long timeout)854     public void requestServiceInfo(String type, String name, long timeout) {
855         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
856     }
857 
858     /**
859      * {@inheritDoc}
860      */
861     @Override
requestServiceInfo(String type, String name, boolean persistent, long timeout)862     public void requestServiceInfo(String type, String name, boolean persistent, long timeout) {
863         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
864         this.waitForInfoData(info, timeout);
865     }
866 
handleServiceResolved(ServiceEvent event)867     void handleServiceResolved(ServiceEvent event) {
868         List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
869         final List<ServiceListenerStatus> listCopy;
870         if ((list != null) && (!list.isEmpty())) {
871             if ((event.getInfo() != null) && event.getInfo().hasData()) {
872                 final ServiceEvent localEvent = event;
873                 synchronized (list) {
874                     listCopy = new ArrayList<ServiceListenerStatus>(list);
875                 }
876                 for (final ServiceListenerStatus listener : listCopy) {
877                     _executor.submit(new Runnable() {
878                         /** {@inheritDoc} */
879                         @Override
880                         public void run() {
881                             listener.serviceResolved(localEvent);
882                         }
883                     });
884                 }
885             }
886         }
887     }
888 
889     /**
890      * {@inheritDoc}
891      */
892     @Override
addServiceTypeListener(ServiceTypeListener listener)893     public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
894         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
895         _typeListeners.add(status);
896 
897         // report cached service types
898         for (String type : _serviceTypes.keySet()) {
899             status.serviceTypeAdded(new ServiceEventImpl(this, type, "", null));
900         }
901 
902         this.startTypeResolver();
903     }
904 
905     /**
906      * {@inheritDoc}
907      */
908     @Override
removeServiceTypeListener(ServiceTypeListener listener)909     public void removeServiceTypeListener(ServiceTypeListener listener) {
910         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
911         _typeListeners.remove(status);
912     }
913 
914     /**
915      * {@inheritDoc}
916      */
917     @Override
addServiceListener(String type, ServiceListener listener)918     public void addServiceListener(String type, ServiceListener listener) {
919         this.addServiceListener(type, listener, ListenerStatus.ASYNCHONEOUS);
920     }
921 
addServiceListener(String type, ServiceListener listener, boolean synch)922     private void addServiceListener(String type, ServiceListener listener, boolean synch) {
923         ServiceListenerStatus status = new ServiceListenerStatus(listener, synch);
924         final String loType = type.toLowerCase();
925         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
926         if (list == null) {
927             if (_serviceListeners.putIfAbsent(loType, new LinkedList<ServiceListenerStatus>()) == null) {
928                 if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
929                     // We have a problem here. The service collectors must be called synchronously so that their cache get cleaned up immediately or we will report .
930                     this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
931                 }
932             }
933             list = _serviceListeners.get(loType);
934         }
935         if (list != null) {
936             synchronized (list) {
937                 if (!list.contains(listener)) {
938                     list.add(status);
939                 }
940             }
941         }
942         // report cached service types
943         final List<ServiceEvent> serviceEvents = new ArrayList<ServiceEvent>();
944         Collection<DNSEntry> dnsEntryLits = this.getCache().allValues();
945         for (DNSEntry entry : dnsEntryLits) {
946             final DNSRecord record = (DNSRecord) entry;
947             if (record.getRecordType() == DNSRecordType.TYPE_SRV) {
948                 if (record.getKey().endsWith(loType)) {
949                     // Do not used the record embedded method for generating event this will not work.
950                     // serviceEvents.add(record.getServiceEvent(this));
951                     serviceEvents.add(new ServiceEventImpl(this, record.getType(), toUnqualifiedName(record.getType(), record.getName()), record.getServiceInfo()));
952                 }
953             }
954         }
955         // Actually call listener with all service events added above
956         for (ServiceEvent serviceEvent : serviceEvents) {
957             status.serviceAdded(serviceEvent);
958         }
959         // Create/start ServiceResolver
960         this.startServiceResolver(type);
961     }
962 
963     /**
964      * {@inheritDoc}
965      */
966     @Override
removeServiceListener(String type, ServiceListener listener)967     public void removeServiceListener(String type, ServiceListener listener) {
968         String loType = type.toLowerCase();
969         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
970         if (list != null) {
971             synchronized (list) {
972                 ServiceListenerStatus status = new ServiceListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
973                 list.remove(status);
974                 if (list.isEmpty()) {
975                     _serviceListeners.remove(loType, list);
976                 }
977             }
978         }
979     }
980 
981     /**
982      * {@inheritDoc}
983      */
984     @Override
registerService(ServiceInfo infoAbstract)985     public void registerService(ServiceInfo infoAbstract) throws IOException {
986         if (this.isClosing() || this.isClosed()) {
987             throw new IllegalStateException("This DNS is closed.");
988         }
989         final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;
990 
991         if (info.getDns() != null) {
992             if (info.getDns() != this) {
993                 throw new IllegalStateException("A service information can only be registered with a single instamce of JmDNS.");
994             } else if (_services.get(info.getKey()) != null) {
995                 throw new IllegalStateException("A service information can only be registered once.");
996             }
997         }
998         info.setDns(this);
999 
1000         this.registerServiceType(info.getTypeWithSubtype());
1001 
1002         // bind the service to this address
1003         info.recoverState();
1004         info.setServer(_localHost.getName());
1005         info.addAddress(_localHost.getInet4Address());
1006         info.addAddress(_localHost.getInet6Address());
1007 
1008         this.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
1009 
1010         this.makeServiceNameUnique(info);
1011         while (_services.putIfAbsent(info.getKey(), info) != null) {
1012             this.makeServiceNameUnique(info);
1013         }
1014 
1015         this.startProber();
1016         info.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
1017 
1018         if (logger.isLoggable(Level.FINE)) {
1019             logger.fine("registerService() JmDNS registered service as " + info);
1020         }
1021     }
1022 
1023     /**
1024      * {@inheritDoc}
1025      */
1026     @Override
unregisterService(ServiceInfo infoAbstract)1027     public void unregisterService(ServiceInfo infoAbstract) {
1028         final ServiceInfoImpl info = (ServiceInfoImpl) _services.get(infoAbstract.getKey());
1029 
1030         if (info != null) {
1031             info.cancelState();
1032             this.startCanceler();
1033             info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1034 
1035             _services.remove(info.getKey(), info);
1036             if (logger.isLoggable(Level.FINE)) {
1037                 logger.fine("unregisterService() JmDNS unregistered service as " + info);
1038             }
1039         } else {
1040             logger.warning("Removing unregistered service info: " + infoAbstract.getKey());
1041         }
1042     }
1043 
1044     /**
1045      * {@inheritDoc}
1046      */
1047     @Override
unregisterAllServices()1048     public void unregisterAllServices() {
1049         if (logger.isLoggable(Level.FINER)) {
1050             logger.finer("unregisterAllServices()");
1051         }
1052 
1053         for (String name : _services.keySet()) {
1054             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
1055             if (info != null) {
1056                 if (logger.isLoggable(Level.FINER)) {
1057                     logger.finer("Cancelling service info: " + info);
1058                 }
1059                 info.cancelState();
1060             }
1061         }
1062         this.startCanceler();
1063 
1064         for (String name : _services.keySet()) {
1065             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
1066             if (info != null) {
1067                 if (logger.isLoggable(Level.FINER)) {
1068                     logger.finer("Wait for service info cancel: " + info);
1069                 }
1070                 info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1071                 _services.remove(name, info);
1072             }
1073         }
1074 
1075     }
1076 
1077     /**
1078      * {@inheritDoc}
1079      */
1080     @Override
registerServiceType(String type)1081     public boolean registerServiceType(String type) {
1082         boolean typeAdded = false;
1083         Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(type);
1084         String domain = map.get(Fields.Domain);
1085         String protocol = map.get(Fields.Protocol);
1086         String application = map.get(Fields.Application);
1087         String subtype = map.get(Fields.Subtype);
1088 
1089         final String name = (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
1090         final String loname = name.toLowerCase();
1091         if (logger.isLoggable(Level.FINE)) {
1092             logger.fine(this.getName() + ".registering service type: " + type + " as: " + name + (subtype.length() > 0 ? " subtype: " + subtype : ""));
1093         }
1094         if (!_serviceTypes.containsKey(loname) && !application.toLowerCase().equals("dns-sd") && !domain.toLowerCase().endsWith("in-addr.arpa") && !domain.toLowerCase().endsWith("ip6.arpa")) {
1095             typeAdded = _serviceTypes.putIfAbsent(loname, new ServiceTypeEntry(name)) == null;
1096             if (typeAdded) {
1097                 final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
1098                 final ServiceEvent event = new ServiceEventImpl(this, name, "", null);
1099                 for (final ServiceTypeListenerStatus status : list) {
1100                     _executor.submit(new Runnable() {
1101                         /** {@inheritDoc} */
1102                         @Override
1103                         public void run() {
1104                             status.serviceTypeAdded(event);
1105                         }
1106                     });
1107                 }
1108             }
1109         }
1110         if (subtype.length() > 0) {
1111             ServiceTypeEntry subtypes = _serviceTypes.get(loname);
1112             if ((subtypes != null) && (!subtypes.contains(subtype))) {
1113                 synchronized (subtypes) {
1114                     if (!subtypes.contains(subtype)) {
1115                         typeAdded = true;
1116                         subtypes.add(subtype);
1117                         final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
1118                         final ServiceEvent event = new ServiceEventImpl(this, "_" + subtype + "._sub." + name, "", null);
1119                         for (final ServiceTypeListenerStatus status : list) {
1120                             _executor.submit(new Runnable() {
1121                                 /** {@inheritDoc} */
1122                                 @Override
1123                                 public void run() {
1124                                     status.subTypeForServiceTypeAdded(event);
1125                                 }
1126                             });
1127                         }
1128                     }
1129                 }
1130             }
1131         }
1132         return typeAdded;
1133     }
1134 
1135     /**
1136      * Generate a possibly unique name for a service using the information we have in the cache.
1137      *
1138      * @return returns true, if the name of the service info had to be changed.
1139      */
makeServiceNameUnique(ServiceInfoImpl info)1140     private boolean makeServiceNameUnique(ServiceInfoImpl info) {
1141         final String originalQualifiedName = info.getKey();
1142         final long now = System.currentTimeMillis();
1143 
1144         boolean collision;
1145         do {
1146             collision = false;
1147 
1148             // Check for collision in cache
1149             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(info.getKey())) {
1150                 if (DNSRecordType.TYPE_SRV.equals(dnsEntry.getRecordType()) && !dnsEntry.isExpired(now)) {
1151                     final DNSRecord.Service s = (DNSRecord.Service) dnsEntry;
1152                     if (s.getPort() != info.getPort() || !s.getServer().equals(_localHost.getName())) {
1153                         if (logger.isLoggable(Level.FINER)) {
1154                             logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + dnsEntry + " s.server=" + s.getServer() + " " + _localHost.getName() + " equals:" + (s.getServer().equals(_localHost.getName())));
1155                         }
1156                         info.setName(incrementName(info.getName()));
1157                         collision = true;
1158                         break;
1159                     }
1160                 }
1161             }
1162 
1163             // Check for collision with other service infos published by JmDNS
1164             final ServiceInfo selfService = _services.get(info.getKey());
1165             if (selfService != null && selfService != info) {
1166                 info.setName(incrementName(info.getName()));
1167                 collision = true;
1168             }
1169         }
1170         while (collision);
1171 
1172         return !(originalQualifiedName.equals(info.getKey()));
1173     }
1174 
incrementName(String name)1175     String incrementName(String name) {
1176         String aName = name;
1177         try {
1178             final int l = aName.lastIndexOf('(');
1179             final int r = aName.lastIndexOf(')');
1180             if ((l >= 0) && (l < r)) {
1181                 aName = aName.substring(0, l) + "(" + (Integer.parseInt(aName.substring(l + 1, r)) + 1) + ")";
1182             } else {
1183                 aName += " (2)";
1184             }
1185         } catch (final NumberFormatException e) {
1186             aName += " (2)";
1187         }
1188         return aName;
1189     }
1190 
1191     /**
1192      * 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.
1193      *
1194      * @param listener
1195      *            DSN listener
1196      * @param question
1197      *            DNS query
1198      */
addListener(DNSListener listener, DNSQuestion question)1199     public void addListener(DNSListener listener, DNSQuestion question) {
1200         final long now = System.currentTimeMillis();
1201 
1202         // add the new listener
1203         _listeners.add(listener);
1204 
1205         // report existing matched records
1206 
1207         if (question != null) {
1208             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(question.getName().toLowerCase())) {
1209                 if (question.answeredBy(dnsEntry) && !dnsEntry.isExpired(now)) {
1210                     listener.updateRecord(this.getCache(), now, dnsEntry);
1211                 }
1212             }
1213         }
1214     }
1215 
1216     /**
1217      * Remove a listener from all outstanding questions. The listener will no longer receive any updates.
1218      *
1219      * @param listener
1220      *            DSN listener
1221      */
removeListener(DNSListener listener)1222     public void removeListener(DNSListener listener) {
1223         _listeners.remove(listener);
1224     }
1225 
1226     /**
1227      * Renew a service when the record become stale. If there is no service collector for the type this method does nothing.
1228      *
1229      * @param record
1230      *            DNS record
1231      */
renewServiceCollector(DNSRecord record)1232     public void renewServiceCollector(DNSRecord record) {
1233         ServiceInfo info = record.getServiceInfo();
1234         if (_serviceCollectors.containsKey(info.getType().toLowerCase())) {
1235             // Create/start ServiceResolver
1236             this.startServiceResolver(info.getType());
1237         }
1238     }
1239 
1240     // Remind: Method updateRecord should receive a better name.
1241     /**
1242      * Notify all listeners that a record was updated.
1243      *
1244      * @param now
1245      *            update date
1246      * @param rec
1247      *            DNS record
1248      * @param operation
1249      *            DNS cache operation
1250      */
updateRecord(long now, DNSRecord rec, Operation operation)1251     public void updateRecord(long now, DNSRecord rec, Operation operation) {
1252         // We do not want to block the entire DNS while we are updating the record for each listener (service info)
1253         {
1254             List<DNSListener> listenerList = null;
1255             synchronized (_listeners) {
1256                 listenerList = new ArrayList<DNSListener>(_listeners);
1257             }
1258             for (DNSListener listener : listenerList) {
1259                 listener.updateRecord(this.getCache(), now, rec);
1260             }
1261         }
1262         if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()))
1263         // if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()) || DNSRecordType.TYPE_SRV.equals(rec.getRecordType()))
1264         {
1265             ServiceEvent event = rec.getServiceEvent(this);
1266             if ((event.getInfo() == null) || !event.getInfo().hasData()) {
1267                 // We do not care about the subtype because the info is only used if complete and the subtype will then be included.
1268                 ServiceInfo info = this.getServiceInfoFromCache(event.getType(), event.getName(), "", false);
1269                 if (info.hasData()) {
1270                     event = new ServiceEventImpl(this, event.getType(), event.getName(), info);
1271                 }
1272             }
1273 
1274             List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
1275             final List<ServiceListenerStatus> serviceListenerList;
1276             if (list != null) {
1277                 synchronized (list) {
1278                     serviceListenerList = new ArrayList<ServiceListenerStatus>(list);
1279                 }
1280             } else {
1281                 serviceListenerList = Collections.emptyList();
1282             }
1283             if (logger.isLoggable(Level.FINEST)) {
1284                 logger.finest(this.getName() + ".updating record for event: " + event + " list " + serviceListenerList + " operation: " + operation);
1285             }
1286             if (!serviceListenerList.isEmpty()) {
1287                 final ServiceEvent localEvent = event;
1288 
1289                 switch (operation) {
1290                     case Add:
1291                         for (final ServiceListenerStatus listener : serviceListenerList) {
1292                             if (listener.isSynchronous()) {
1293                                 listener.serviceAdded(localEvent);
1294                             } else {
1295                                 _executor.submit(new Runnable() {
1296                                     /** {@inheritDoc} */
1297                                     @Override
1298                                     public void run() {
1299                                         listener.serviceAdded(localEvent);
1300                                     }
1301                                 });
1302                             }
1303                         }
1304                         break;
1305                     case Remove:
1306                         for (final ServiceListenerStatus listener : serviceListenerList) {
1307                             if (listener.isSynchronous()) {
1308                                 listener.serviceRemoved(localEvent);
1309                             } else {
1310                                 _executor.submit(new Runnable() {
1311                                     /** {@inheritDoc} */
1312                                     @Override
1313                                     public void run() {
1314                                         listener.serviceRemoved(localEvent);
1315                                     }
1316                                 });
1317                             }
1318                         }
1319                         break;
1320                     default:
1321                         break;
1322                 }
1323             }
1324         }
1325     }
1326 
handleRecord(DNSRecord record, long now)1327     void handleRecord(DNSRecord record, long now) {
1328         DNSRecord newRecord = record;
1329 
1330         Operation cacheOperation = Operation.Noop;
1331         final boolean expired = newRecord.isExpired(now);
1332         if (logger.isLoggable(Level.FINE)) {
1333             logger.fine(this.getName() + " handle response: " + newRecord);
1334         }
1335 
1336         // update the cache
1337         if (!newRecord.isServicesDiscoveryMetaQuery() && !newRecord.isDomainDiscoveryQuery()) {
1338             final boolean unique = newRecord.isUnique();
1339             final DNSRecord cachedRecord = (DNSRecord) this.getCache().getDNSEntry(newRecord);
1340             if (logger.isLoggable(Level.FINE)) {
1341                 logger.fine(this.getName() + " handle response cached record: " + cachedRecord);
1342             }
1343             if (unique) {
1344                 for (DNSEntry entry : this.getCache().getDNSEntryList(newRecord.getKey())) {
1345                     if (newRecord.getRecordType().equals(entry.getRecordType()) && newRecord.getRecordClass().equals(entry.getRecordClass()) && (entry != cachedRecord)) {
1346                         ((DNSRecord) entry).setWillExpireSoon(now);
1347                     }
1348                 }
1349             }
1350             if (cachedRecord != null) {
1351                 if (expired) {
1352                     // if the record has a 0 ttl that means we have a cancel record we need to delay the removal by 1s
1353                     if (newRecord.getTTL() == 0) {
1354                         cacheOperation = Operation.Noop;
1355                         cachedRecord.setWillExpireSoon(now);
1356                         // the actual record will be disposed of by the record reaper.
1357                     } else {
1358                         cacheOperation = Operation.Remove;
1359                         this.getCache().removeDNSEntry(cachedRecord);
1360                     }
1361                 } else {
1362                     // If the record content has changed we need to inform our listeners.
1363                     if (!newRecord.sameValue(cachedRecord) || (!newRecord.sameSubtype(cachedRecord) && (newRecord.getSubtype().length() > 0))) {
1364                         if (newRecord.isSingleValued()) {
1365                             cacheOperation = Operation.Update;
1366                             this.getCache().replaceDNSEntry(newRecord, cachedRecord);
1367                         } else {
1368                             // Address record can have more than one value on multi-homed machines
1369                             cacheOperation = Operation.Add;
1370                             this.getCache().addDNSEntry(newRecord);
1371                         }
1372                     } else {
1373                         cachedRecord.resetTTL(newRecord);
1374                         newRecord = cachedRecord;
1375                     }
1376                 }
1377             } else {
1378                 if (!expired) {
1379                     cacheOperation = Operation.Add;
1380                     this.getCache().addDNSEntry(newRecord);
1381                 }
1382             }
1383         }
1384 
1385         // Register new service types
1386         if (newRecord.getRecordType() == DNSRecordType.TYPE_PTR) {
1387             // handle DNSConstants.DNS_META_QUERY records
1388             boolean typeAdded = false;
1389             if (newRecord.isServicesDiscoveryMetaQuery()) {
1390                 // The service names are in the alias.
1391                 if (!expired) {
1392                     typeAdded = this.registerServiceType(((DNSRecord.Pointer) newRecord).getAlias());
1393                 }
1394                 return;
1395             }
1396             typeAdded |= this.registerServiceType(newRecord.getName());
1397             if (typeAdded && (cacheOperation == Operation.Noop)) {
1398                 cacheOperation = Operation.RegisterServiceType;
1399             }
1400         }
1401 
1402         // notify the listeners
1403         if (cacheOperation != Operation.Noop) {
1404             this.updateRecord(now, newRecord, cacheOperation);
1405         }
1406 
1407     }
1408 
1409     /**
1410      * Handle an incoming response. Cache answers, and pass them on to the appropriate questions.
1411      *
1412      * @exception IOException
1413      */
handleResponse(DNSIncoming msg)1414     void handleResponse(DNSIncoming msg) throws IOException {
1415         final long now = System.currentTimeMillis();
1416 
1417         boolean hostConflictDetected = false;
1418         boolean serviceConflictDetected = false;
1419 
1420         for (DNSRecord newRecord : msg.getAllAnswers()) {
1421             this.handleRecord(newRecord, now);
1422 
1423             if (DNSRecordType.TYPE_A.equals(newRecord.getRecordType()) || DNSRecordType.TYPE_AAAA.equals(newRecord.getRecordType())) {
1424                 hostConflictDetected |= newRecord.handleResponse(this);
1425             } else {
1426                 serviceConflictDetected |= newRecord.handleResponse(this);
1427             }
1428 
1429         }
1430 
1431         if (hostConflictDetected || serviceConflictDetected) {
1432             this.startProber();
1433         }
1434     }
1435 
1436     /**
1437      * Handle an incoming query. See if we can answer any part of it given our service infos.
1438      *
1439      * @param in
1440      * @param addr
1441      * @param port
1442      * @exception IOException
1443      */
handleQuery(DNSIncoming in, InetAddress addr, int port)1444     void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException {
1445         if (logger.isLoggable(Level.FINE)) {
1446             logger.fine(this.getName() + ".handle query: " + in);
1447         }
1448         // Track known answers
1449         boolean conflictDetected = false;
1450         final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
1451         for (DNSRecord answer : in.getAllAnswers()) {
1452             conflictDetected |= answer.handleQuery(this, expirationTime);
1453         }
1454 
1455         this.ioLock();
1456         try {
1457 
1458             if (_plannedAnswer != null) {
1459                 _plannedAnswer.append(in);
1460             } else {
1461                 DNSIncoming plannedAnswer = in.clone();
1462                 if (in.isTruncated()) {
1463                     _plannedAnswer = plannedAnswer;
1464                 }
1465                 this.startResponder(plannedAnswer, port);
1466             }
1467 
1468         } finally {
1469             this.ioUnlock();
1470         }
1471 
1472         final long now = System.currentTimeMillis();
1473         for (DNSRecord answer : in.getAnswers()) {
1474             this.handleRecord(answer, now);
1475         }
1476 
1477         if (conflictDetected) {
1478             this.startProber();
1479         }
1480     }
1481 
respondToQuery(DNSIncoming in)1482     public void respondToQuery(DNSIncoming in) {
1483         this.ioLock();
1484         try {
1485             if (_plannedAnswer == in) {
1486                 _plannedAnswer = null;
1487             }
1488         } finally {
1489             this.ioUnlock();
1490         }
1491     }
1492 
1493     /**
1494      * Add an answer to a question. Deal with the case when the outgoing packet overflows
1495      *
1496      * @param in
1497      * @param addr
1498      * @param port
1499      * @param out
1500      * @param rec
1501      * @return outgoing answer
1502      * @exception IOException
1503      */
addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec)1504     public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException {
1505         DNSOutgoing newOut = out;
1506         if (newOut == null) {
1507             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
1508         }
1509         try {
1510             newOut.addAnswer(in, rec);
1511         } catch (final IOException e) {
1512             newOut.setFlags(newOut.getFlags() | DNSConstants.FLAGS_TC);
1513             newOut.setId(in.getId());
1514             send(newOut);
1515 
1516             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
1517             newOut.addAnswer(in, rec);
1518         }
1519         return newOut;
1520     }
1521 
1522     /**
1523      * Send an outgoing multicast DNS message.
1524      *
1525      * @param out
1526      * @exception IOException
1527      */
send(DNSOutgoing out)1528     public void send(DNSOutgoing out) throws IOException {
1529         if (!out.isEmpty()) {
1530             byte[] message = out.data();
1531             final DatagramPacket packet = new DatagramPacket(message, message.length, _group, DNSConstants.MDNS_PORT);
1532 
1533             if (logger.isLoggable(Level.FINEST)) {
1534                 try {
1535                     final DNSIncoming msg = new DNSIncoming(packet);
1536                     if (logger.isLoggable(Level.FINEST)) {
1537                         logger.finest("send(" + this.getName() + ") JmDNS out:" + msg.print(true));
1538                     }
1539                 } catch (final IOException e) {
1540                     logger.throwing(getClass().toString(), "send(" + this.getName() + ") - JmDNS can not parse what it sends!!!", e);
1541                 }
1542             }
1543             final MulticastSocket ms = _socket;
1544             if (ms != null && !ms.isClosed()) {
1545                 ms.send(packet);
1546             }
1547         }
1548     }
1549 
1550     /*
1551      * (non-Javadoc)
1552      * @see javax.jmdns.impl.DNSTaskStarter#purgeTimer()
1553      */
1554     @Override
purgeTimer()1555     public void purgeTimer() {
1556         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeTimer();
1557     }
1558 
1559     /*
1560      * (non-Javadoc)
1561      * @see javax.jmdns.impl.DNSTaskStarter#purgeStateTimer()
1562      */
1563     @Override
purgeStateTimer()1564     public void purgeStateTimer() {
1565         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeStateTimer();
1566     }
1567 
1568     /*
1569      * (non-Javadoc)
1570      * @see javax.jmdns.impl.DNSTaskStarter#cancelTimer()
1571      */
1572     @Override
cancelTimer()1573     public void cancelTimer() {
1574         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelTimer();
1575     }
1576 
1577     /*
1578      * (non-Javadoc)
1579      * @see javax.jmdns.impl.DNSTaskStarter#cancelStateTimer()
1580      */
1581     @Override
cancelStateTimer()1582     public void cancelStateTimer() {
1583         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelStateTimer();
1584     }
1585 
1586     /*
1587      * (non-Javadoc)
1588      * @see javax.jmdns.impl.DNSTaskStarter#startProber()
1589      */
1590     @Override
startProber()1591     public void startProber() {
1592         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startProber();
1593     }
1594 
1595     /*
1596      * (non-Javadoc)
1597      * @see javax.jmdns.impl.DNSTaskStarter#startAnnouncer()
1598      */
1599     @Override
startAnnouncer()1600     public void startAnnouncer() {
1601         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startAnnouncer();
1602     }
1603 
1604     /*
1605      * (non-Javadoc)
1606      * @see javax.jmdns.impl.DNSTaskStarter#startRenewer()
1607      */
1608     @Override
startRenewer()1609     public void startRenewer() {
1610         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startRenewer();
1611     }
1612 
1613     /*
1614      * (non-Javadoc)
1615      * @see javax.jmdns.impl.DNSTaskStarter#startCanceler()
1616      */
1617     @Override
startCanceler()1618     public void startCanceler() {
1619         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startCanceler();
1620     }
1621 
1622     /*
1623      * (non-Javadoc)
1624      * @see javax.jmdns.impl.DNSTaskStarter#startReaper()
1625      */
1626     @Override
startReaper()1627     public void startReaper() {
1628         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startReaper();
1629     }
1630 
1631     /*
1632      * (non-Javadoc)
1633      * @see javax.jmdns.impl.DNSTaskStarter#startServiceInfoResolver(javax.jmdns.impl.ServiceInfoImpl)
1634      */
1635     @Override
startServiceInfoResolver(ServiceInfoImpl info)1636     public void startServiceInfoResolver(ServiceInfoImpl info) {
1637         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceInfoResolver(info);
1638     }
1639 
1640     /*
1641      * (non-Javadoc)
1642      * @see javax.jmdns.impl.DNSTaskStarter#startTypeResolver()
1643      */
1644     @Override
startTypeResolver()1645     public void startTypeResolver() {
1646         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startTypeResolver();
1647     }
1648 
1649     /*
1650      * (non-Javadoc)
1651      * @see javax.jmdns.impl.DNSTaskStarter#startServiceResolver(java.lang.String)
1652      */
1653     @Override
startServiceResolver(String type)1654     public void startServiceResolver(String type) {
1655         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceResolver(type);
1656     }
1657 
1658     /*
1659      * (non-Javadoc)
1660      * @see javax.jmdns.impl.DNSTaskStarter#startResponder(javax.jmdns.impl.DNSIncoming, int)
1661      */
1662     @Override
startResponder(DNSIncoming in, int port)1663     public void startResponder(DNSIncoming in, int port) {
1664         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startResponder(in, port);
1665     }
1666 
1667     // REMIND: Why is this not an anonymous inner class?
1668     /**
1669      * Shutdown operations.
1670      */
1671     protected class Shutdown implements Runnable {
1672         /** {@inheritDoc} */
1673         @Override
run()1674         public void run() {
1675             try {
1676                 _shutdown = null;
1677                 close();
1678             } catch (Throwable exception) {
1679                 System.err.println("Error while shuting down. " + exception);
1680             }
1681         }
1682     }
1683 
1684     private final Object _recoverLock = new Object();
1685 
1686     /**
1687      * Recover jmdns when there is an error.
1688      */
recover()1689     public void recover() {
1690         logger.finer(this.getName() + "recover()");
1691         // We have an IO error so lets try to recover if anything happens lets close it.
1692         // This should cover the case of the IP address changing under our feet
1693         if (this.isClosing() || this.isClosed() || this.isCanceling() || this.isCanceled()) {
1694             return;
1695         }
1696 
1697         // We need some definite lock here as we may have multiple timer running in the same thread that will not be stopped by the reentrant lock
1698         // in the state object. This is only a problem in this case as we are going to execute in seperate thread so that the timer can clear.
1699         synchronized (_recoverLock) {
1700             // Stop JmDNS
1701             // This protects against recursive calls
1702             if (this.cancelState()) {
1703                 logger.finer(this.getName() + "recover() thread " + Thread.currentThread().getName());
1704                 Thread recover = new Thread(this.getName() + ".recover()") {
1705                     /**
1706                      * {@inheritDoc}
1707                      */
1708                     @Override
1709                     public void run() {
1710                         __recover();
1711                     }
1712                 };
1713                 recover.start();
1714             }
1715         }
1716     }
1717 
__recover()1718     void __recover() {
1719         // Synchronize only if we are not already in process to prevent dead locks
1720         //
1721         if (logger.isLoggable(Level.FINER)) {
1722             logger.finer(this.getName() + "recover() Cleanning up");
1723         }
1724 
1725         logger.warning("RECOVERING");
1726         // Purge the timer
1727         this.purgeTimer();
1728 
1729         // We need to keep a copy for reregistration
1730         final Collection<ServiceInfo> oldServiceInfos = new ArrayList<ServiceInfo>(getServices().values());
1731 
1732         // Cancel all services
1733         this.unregisterAllServices();
1734         this.disposeServiceCollectors();
1735 
1736         this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1737 
1738         // Purge the canceler timer
1739         this.purgeStateTimer();
1740 
1741         //
1742         // close multicast socket
1743         this.closeMulticastSocket();
1744 
1745         //
1746         this.getCache().clear();
1747         if (logger.isLoggable(Level.FINER)) {
1748             logger.finer(this.getName() + "recover() All is clean");
1749         }
1750 
1751         if (this.isCanceled()) {
1752             //
1753             // All is clear now start the services
1754             //
1755             for (ServiceInfo info : oldServiceInfos) {
1756                 ((ServiceInfoImpl) info).recoverState();
1757             }
1758             this.recoverState();
1759 
1760             try {
1761                 this.openMulticastSocket(this.getLocalHost());
1762                 this.start(oldServiceInfos);
1763             } catch (final Exception exception) {
1764                 logger.log(Level.WARNING, this.getName() + "recover() Start services exception ", exception);
1765             }
1766             logger.log(Level.WARNING, this.getName() + "recover() We are back!");
1767         } else {
1768             // We have a problem. We could not clear the state.
1769             logger.log(Level.WARNING, this.getName() + "recover() Could not recover we are Down!");
1770             if (this.getDelegate() != null) {
1771                 this.getDelegate().cannotRecoverFromIOError(this.getDns(), oldServiceInfos);
1772             }
1773         }
1774 
1775     }
1776 
cleanCache()1777     public void cleanCache() {
1778         long now = System.currentTimeMillis();
1779         for (DNSEntry entry : this.getCache().allValues()) {
1780             try {
1781                 DNSRecord record = (DNSRecord) entry;
1782                 if (record.isExpired(now)) {
1783                     this.updateRecord(now, record, Operation.Remove);
1784                     this.getCache().removeDNSEntry(record);
1785                 } else if (record.isStale(now)) {
1786                     // we should query for the record we care about i.e. those in the service collectors
1787                     this.renewServiceCollector(record);
1788                 }
1789             } catch (Exception exception) {
1790                 logger.log(Level.SEVERE, this.getName() + ".Error while reaping records: " + entry, exception);
1791                 logger.severe(this.toString());
1792             }
1793         }
1794     }
1795 
1796     /**
1797      * {@inheritDoc}
1798      */
1799     @Override
close()1800     public void close() {
1801         if (this.isClosing()) {
1802             return;
1803         }
1804 
1805         if (logger.isLoggable(Level.FINER)) {
1806             logger.finer("Cancelling JmDNS: " + this);
1807         }
1808         // Stop JmDNS
1809         // This protects against recursive calls
1810         if (this.closeState()) {
1811             // We got the tie break now clean up
1812 
1813             // Stop the timer
1814             logger.finer("Canceling the timer");
1815             this.cancelTimer();
1816 
1817             // Cancel all services
1818             this.unregisterAllServices();
1819             this.disposeServiceCollectors();
1820 
1821             if (logger.isLoggable(Level.FINER)) {
1822                 logger.finer("Wait for JmDNS cancel: " + this);
1823             }
1824             this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1825 
1826             // Stop the canceler timer
1827             logger.finer("Canceling the state timer");
1828             this.cancelStateTimer();
1829 
1830             // Stop the executor
1831             _executor.shutdown();
1832 
1833             // close socket
1834             this.closeMulticastSocket();
1835 
1836             // remove the shutdown hook
1837             if (_shutdown != null) {
1838                 Runtime.getRuntime().removeShutdownHook(_shutdown);
1839             }
1840 
1841             if (logger.isLoggable(Level.FINER)) {
1842                 logger.finer("JmDNS closed.");
1843             }
1844         }
1845         advanceState(null);
1846     }
1847 
1848     /**
1849      * {@inheritDoc}
1850      */
1851     @Override
1852     @Deprecated
printServices()1853     public void printServices() {
1854         System.err.println(toString());
1855     }
1856 
1857     /**
1858      * {@inheritDoc}
1859      */
1860     @Override
toString()1861     public String toString() {
1862         final StringBuilder aLog = new StringBuilder(2048);
1863         aLog.append("\t---- Local Host -----");
1864         aLog.append("\n\t");
1865         aLog.append(_localHost);
1866         aLog.append("\n\t---- Services -----");
1867         for (String key : _services.keySet()) {
1868             aLog.append("\n\t\tService: ");
1869             aLog.append(key);
1870             aLog.append(": ");
1871             aLog.append(_services.get(key));
1872         }
1873         aLog.append("\n");
1874         aLog.append("\t---- Types ----");
1875         for (String key : _serviceTypes.keySet()) {
1876             ServiceTypeEntry subtypes = _serviceTypes.get(key);
1877             aLog.append("\n\t\tType: ");
1878             aLog.append(subtypes.getType());
1879             aLog.append(": ");
1880             aLog.append(subtypes.isEmpty() ? "no subtypes" : subtypes);
1881         }
1882         aLog.append("\n");
1883         aLog.append(_cache.toString());
1884         aLog.append("\n");
1885         aLog.append("\t---- Service Collectors ----");
1886         for (String key : _serviceCollectors.keySet()) {
1887             aLog.append("\n\t\tService Collector: ");
1888             aLog.append(key);
1889             aLog.append(": ");
1890             aLog.append(_serviceCollectors.get(key));
1891         }
1892         aLog.append("\n");
1893         aLog.append("\t---- Service Listeners ----");
1894         for (String key : _serviceListeners.keySet()) {
1895             aLog.append("\n\t\tService Listener: ");
1896             aLog.append(key);
1897             aLog.append(": ");
1898             aLog.append(_serviceListeners.get(key));
1899         }
1900         return aLog.toString();
1901     }
1902 
1903     /**
1904      * {@inheritDoc}
1905      */
1906     @Override
list(String type)1907     public ServiceInfo[] list(String type) {
1908         return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
1909     }
1910 
1911     /**
1912      * {@inheritDoc}
1913      */
1914     @Override
list(String type, long timeout)1915     public ServiceInfo[] list(String type, long timeout) {
1916         this.cleanCache();
1917         // Implementation note: The first time a list for a given type is
1918         // requested, a ServiceCollector is created which collects service
1919         // infos. This greatly speeds up the performance of subsequent calls
1920         // to this method. The caveats are, that 1) the first call to this
1921         // method for a given type is slow, and 2) we spawn a ServiceCollector
1922         // instance for each service type which increases network traffic a
1923         // little.
1924 
1925         String loType = type.toLowerCase();
1926 
1927         boolean newCollectorCreated = false;
1928         if (this.isCanceling() || this.isCanceled()) {
1929             return new ServiceInfo[0];
1930         }
1931 
1932         ServiceCollector collector = _serviceCollectors.get(loType);
1933         if (collector == null) {
1934             newCollectorCreated = _serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null;
1935             collector = _serviceCollectors.get(loType);
1936             if (newCollectorCreated) {
1937                 this.addServiceListener(type, collector, ListenerStatus.SYNCHONEOUS);
1938             }
1939         }
1940         if (logger.isLoggable(Level.FINER)) {
1941             logger.finer(this.getName() + ".collector: " + collector);
1942         }
1943         // At this stage the collector should never be null but it keeps findbugs happy.
1944         return (collector != null ? collector.list(timeout) : new ServiceInfo[0]);
1945     }
1946 
1947     /**
1948      * {@inheritDoc}
1949      */
1950     @Override
listBySubtype(String type)1951     public Map<String, ServiceInfo[]> listBySubtype(String type) {
1952         return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT);
1953     }
1954 
1955     /**
1956      * {@inheritDoc}
1957      */
1958     @Override
listBySubtype(String type, long timeout)1959     public Map<String, ServiceInfo[]> listBySubtype(String type, long timeout) {
1960         Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5);
1961         for (ServiceInfo info : this.list(type, timeout)) {
1962             String subtype = info.getSubtype().toLowerCase();
1963             if (!map.containsKey(subtype)) {
1964                 map.put(subtype, new ArrayList<ServiceInfo>(10));
1965             }
1966             map.get(subtype).add(info);
1967         }
1968 
1969         Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size());
1970         for (String subtype : map.keySet()) {
1971             List<ServiceInfo> infoForSubType = map.get(subtype);
1972             result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()]));
1973         }
1974 
1975         return result;
1976     }
1977 
1978     /**
1979      * This method disposes all ServiceCollector instances which have been created by calls to method <code>list(type)</code>.
1980      *
1981      * @see #list
1982      */
disposeServiceCollectors()1983     private void disposeServiceCollectors() {
1984         if (logger.isLoggable(Level.FINER)) {
1985             logger.finer("disposeServiceCollectors()");
1986         }
1987         for (String type : _serviceCollectors.keySet()) {
1988             ServiceCollector collector = _serviceCollectors.get(type);
1989             if (collector != null) {
1990                 this.removeServiceListener(type, collector);
1991                 _serviceCollectors.remove(type, collector);
1992             }
1993         }
1994     }
1995 
1996     /**
1997      * Instances of ServiceCollector are used internally to speed up the performance of method <code>list(type)</code>.
1998      *
1999      * @see #list
2000      */
2001     private static class ServiceCollector implements ServiceListener {
2002         // private static Logger logger = Logger.getLogger(ServiceCollector.class.getName());
2003 
2004         /**
2005          * A set of collected service instance names.
2006          */
2007         private final ConcurrentMap<String, ServiceInfo>  _infos;
2008 
2009         /**
2010          * A set of collected service event waiting to be resolved.
2011          */
2012         private final ConcurrentMap<String, ServiceEvent> _events;
2013 
2014         /**
2015          * This is the type we are listening for (only used for debugging).
2016          */
2017         private final String                              _type;
2018 
2019         /**
2020          * This is used to force a wait on the first invocation of list.
2021          */
2022         private volatile boolean                          _needToWaitForInfos;
2023 
ServiceCollector(String type)2024         public ServiceCollector(String type) {
2025             super();
2026             _infos = new ConcurrentHashMap<String, ServiceInfo>();
2027             _events = new ConcurrentHashMap<String, ServiceEvent>();
2028             _type = type;
2029             _needToWaitForInfos = true;
2030         }
2031 
2032         /**
2033          * A service has been added.
2034          *
2035          * @param event
2036          *            service event
2037          */
2038         @Override
serviceAdded(ServiceEvent event)2039         public void serviceAdded(ServiceEvent event) {
2040             synchronized (this) {
2041                 ServiceInfo info = event.getInfo();
2042                 if ((info != null) && (info.hasData())) {
2043                     _infos.put(event.getName(), info);
2044                 } else {
2045                     String subtype = (info != null ? info.getSubtype() : "");
2046                     info = ((JmDNSImpl) event.getDNS()).resolveServiceInfo(event.getType(), event.getName(), subtype, true);
2047                     if (info != null) {
2048                         _infos.put(event.getName(), info);
2049                     } else {
2050                         _events.put(event.getName(), event);
2051                     }
2052                 }
2053             }
2054         }
2055 
2056         /**
2057          * A service has been removed.
2058          *
2059          * @param event
2060          *            service event
2061          */
2062         @Override
serviceRemoved(ServiceEvent event)2063         public void serviceRemoved(ServiceEvent event) {
2064             synchronized (this) {
2065                 _infos.remove(event.getName());
2066                 _events.remove(event.getName());
2067             }
2068         }
2069 
2070         /**
2071          * A service has been resolved. Its details are now available in the ServiceInfo record.
2072          *
2073          * @param event
2074          *            service event
2075          */
2076         @Override
serviceResolved(ServiceEvent event)2077         public void serviceResolved(ServiceEvent event) {
2078             synchronized (this) {
2079                 _infos.put(event.getName(), event.getInfo());
2080                 _events.remove(event.getName());
2081             }
2082         }
2083 
2084         /**
2085          * Returns an array of all service infos which have been collected by this ServiceCollector.
2086          *
2087          * @param timeout
2088          *            timeout if the info list is empty.
2089          * @return Service Info array
2090          */
list(long timeout)2091         public ServiceInfo[] list(long timeout) {
2092             if (_infos.isEmpty() || !_events.isEmpty() || _needToWaitForInfos) {
2093                 long loops = (timeout / 200L);
2094                 if (loops < 1) {
2095                     loops = 1;
2096                 }
2097                 for (int i = 0; i < loops; i++) {
2098                     try {
2099                         Thread.sleep(200);
2100                     } catch (final InterruptedException e) {
2101                         /* Stub */
2102                     }
2103                     if (_events.isEmpty() && !_infos.isEmpty() && !_needToWaitForInfos) {
2104                         break;
2105                     }
2106                 }
2107             }
2108             _needToWaitForInfos = false;
2109             return _infos.values().toArray(new ServiceInfo[_infos.size()]);
2110         }
2111 
2112         /**
2113          * {@inheritDoc}
2114          */
2115         @Override
toString()2116         public String toString() {
2117             final StringBuffer aLog = new StringBuffer();
2118             aLog.append("\n\tType: ");
2119             aLog.append(_type);
2120             if (_infos.isEmpty()) {
2121                 aLog.append("\n\tNo services collected.");
2122             } else {
2123                 aLog.append("\n\tServices");
2124                 for (String key : _infos.keySet()) {
2125                     aLog.append("\n\t\tService: ");
2126                     aLog.append(key);
2127                     aLog.append(": ");
2128                     aLog.append(_infos.get(key));
2129                 }
2130             }
2131             if (_events.isEmpty()) {
2132                 aLog.append("\n\tNo event queued.");
2133             } else {
2134                 aLog.append("\n\tEvents");
2135                 for (String key : _events.keySet()) {
2136                     aLog.append("\n\t\tEvent: ");
2137                     aLog.append(key);
2138                     aLog.append(": ");
2139                     aLog.append(_events.get(key));
2140                 }
2141             }
2142             return aLog.toString();
2143         }
2144     }
2145 
toUnqualifiedName(String type, String qualifiedName)2146     static String toUnqualifiedName(String type, String qualifiedName) {
2147         String loType = type.toLowerCase();
2148         String loQualifiedName = qualifiedName.toLowerCase();
2149         if (loQualifiedName.endsWith(loType) && !(loQualifiedName.equals(loType))) {
2150             return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
2151         }
2152         return qualifiedName;
2153     }
2154 
getServices()2155     public Map<String, ServiceInfo> getServices() {
2156         return _services;
2157     }
2158 
setLastThrottleIncrement(long lastThrottleIncrement)2159     public void setLastThrottleIncrement(long lastThrottleIncrement) {
2160         this._lastThrottleIncrement = lastThrottleIncrement;
2161     }
2162 
getLastThrottleIncrement()2163     public long getLastThrottleIncrement() {
2164         return _lastThrottleIncrement;
2165     }
2166 
setThrottle(int throttle)2167     public void setThrottle(int throttle) {
2168         this._throttle = throttle;
2169     }
2170 
getThrottle()2171     public int getThrottle() {
2172         return _throttle;
2173     }
2174 
getRandom()2175     public static Random getRandom() {
2176         return _random;
2177     }
2178 
ioLock()2179     public void ioLock() {
2180         _ioLock.lock();
2181     }
2182 
ioUnlock()2183     public void ioUnlock() {
2184         _ioLock.unlock();
2185     }
2186 
setPlannedAnswer(DNSIncoming plannedAnswer)2187     public void setPlannedAnswer(DNSIncoming plannedAnswer) {
2188         this._plannedAnswer = plannedAnswer;
2189     }
2190 
getPlannedAnswer()2191     public DNSIncoming getPlannedAnswer() {
2192         return _plannedAnswer;
2193     }
2194 
setLocalHost(HostInfo localHost)2195     void setLocalHost(HostInfo localHost) {
2196         this._localHost = localHost;
2197     }
2198 
getServiceTypes()2199     public Map<String, ServiceTypeEntry> getServiceTypes() {
2200         return _serviceTypes;
2201     }
2202 
getSocket()2203     public MulticastSocket getSocket() {
2204         return _socket;
2205     }
2206 
getGroup()2207     public InetAddress getGroup() {
2208         return _group;
2209     }
2210 
2211     @Override
getDelegate()2212     public Delegate getDelegate() {
2213         return this._delegate;
2214     }
2215 
2216     @Override
setDelegate(Delegate delegate)2217     public Delegate setDelegate(Delegate delegate) {
2218         Delegate previous = this._delegate;
2219         this._delegate = delegate;
2220         return previous;
2221     }
2222 
2223 }
2224