View Javadoc

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 
125             public SubTypeEntry(String subtype) {
126                 super();
127                 _value = (subtype != null ? subtype : "");
128                 _key = _value.toLowerCase();
129             }
130 
131             /**
132              * {@inheritDoc}
133              */
134             @Override
135             public String getKey() {
136                 return _key;
137             }
138 
139             /**
140              * {@inheritDoc}
141              */
142             @Override
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
157             public String setValue(String value) {
158                 throw new UnsupportedOperationException();
159             }
160 
161             /**
162              * {@inheritDoc}
163              */
164             @Override
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
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
185             public SubTypeEntry clone() {
186                 // Immutable object
187                 return this;
188             }
189 
190             /**
191              * {@inheritDoc}
192              */
193             @Override
194             public String toString() {
195                 return _key + "=" + _value;
196             }
197 
198         }
199 
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          */
211         public String getType() {
212             return _type;
213         }
214 
215         /*
216          * (non-Javadoc)
217          * @see java.util.AbstractMap#entrySet()
218          */
219         @Override
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          */
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          */
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          */
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
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
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      */
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      */
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 
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 
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 
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
515     public boolean advanceState(DNSTask task) {
516         return this._localHost.advanceState(task);
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
523     public boolean revertState() {
524         return this._localHost.revertState();
525     }
526 
527     /**
528      * {@inheritDoc}
529      */
530     @Override
531     public boolean cancelState() {
532         return this._localHost.cancelState();
533     }
534 
535     /**
536      * {@inheritDoc}
537      */
538     @Override
539     public boolean closeState() {
540         return this._localHost.closeState();
541     }
542 
543     /**
544      * {@inheritDoc}
545      */
546     @Override
547     public boolean recoverState() {
548         return this._localHost.recoverState();
549     }
550 
551     /**
552      * {@inheritDoc}
553      */
554     @Override
555     public JmDNSImpl getDns() {
556         return this;
557     }
558 
559     /**
560      * {@inheritDoc}
561      */
562     @Override
563     public void associateWithTask(DNSTask task, DNSState state) {
564         this._localHost.associateWithTask(task, state);
565     }
566 
567     /**
568      * {@inheritDoc}
569      */
570     @Override
571     public void removeAssociationWithTask(DNSTask task) {
572         this._localHost.removeAssociationWithTask(task);
573     }
574 
575     /**
576      * {@inheritDoc}
577      */
578     @Override
579     public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
580         return this._localHost.isAssociatedWithTask(task, state);
581     }
582 
583     /**
584      * {@inheritDoc}
585      */
586     @Override
587     public boolean isProbing() {
588         return this._localHost.isProbing();
589     }
590 
591     /**
592      * {@inheritDoc}
593      */
594     @Override
595     public boolean isAnnouncing() {
596         return this._localHost.isAnnouncing();
597     }
598 
599     /**
600      * {@inheritDoc}
601      */
602     @Override
603     public boolean isAnnounced() {
604         return this._localHost.isAnnounced();
605     }
606 
607     /**
608      * {@inheritDoc}
609      */
610     @Override
611     public boolean isCanceling() {
612         return this._localHost.isCanceling();
613     }
614 
615     /**
616      * {@inheritDoc}
617      */
618     @Override
619     public boolean isCanceled() {
620         return this._localHost.isCanceled();
621     }
622 
623     /**
624      * {@inheritDoc}
625      */
626     @Override
627     public boolean isClosing() {
628         return this._localHost.isClosing();
629     }
630 
631     /**
632      * {@inheritDoc}
633      */
634     @Override
635     public boolean isClosed() {
636         return this._localHost.isClosed();
637     }
638 
639     /**
640      * {@inheritDoc}
641      */
642     @Override
643     public boolean waitForAnnounced(long timeout) {
644         return this._localHost.waitForAnnounced(timeout);
645     }
646 
647     /**
648      * {@inheritDoc}
649      */
650     @Override
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      */
660     public DNSCache getCache() {
661         return _cache;
662     }
663 
664     /**
665      * {@inheritDoc}
666      */
667     @Override
668     public String getName() {
669         return _name;
670     }
671 
672     /**
673      * {@inheritDoc}
674      */
675     @Override
676     public String getHostName() {
677         return _localHost.getName();
678     }
679 
680     /**
681      * Returns the local host info
682      *
683      * @return local host info
684      */
685     public HostInfo getLocalHost() {
686         return _localHost;
687     }
688 
689     /**
690      * {@inheritDoc}
691      */
692     @Override
693     public InetAddress getInterface() throws IOException {
694         return _socket.getInterface();
695     }
696 
697     /**
698      * {@inheritDoc}
699      */
700     @Override
701     public ServiceInfo getServiceInfo(String type, String name) {
702         return this.getServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
703     }
704 
705     /**
706      * {@inheritDoc}
707      */
708     @Override
709     public ServiceInfo getServiceInfo(String type, String name, long timeout) {
710         return this.getServiceInfo(type, name, false, timeout);
711     }
712 
713     /**
714      * {@inheritDoc}
715      */
716     @Override
717     public ServiceInfo getServiceInfo(String type, String name, boolean persistent) {
718         return this.getServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
719     }
720 
721     /**
722      * {@inheritDoc}
723      */
724     @Override
725     public ServiceInfo getServiceInfo(String type, String name, boolean persistent, long timeout) {
726         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
727         this.waitForInfoData(info, timeout);
728         return (info.hasData() ? info : null);
729     }
730 
731     ServiceInfoImpl resolveServiceInfo(String type, String name, String subtype, boolean persistent) {
732         this.cleanCache();
733         String loType = type.toLowerCase();
734         this.registerServiceType(type);
735         if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
736             this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
737         }
738 
739         // Check if the answer is in the cache.
740         final ServiceInfoImpl info = this.getServiceInfoFromCache(type, name, subtype, persistent);
741         // We still run the resolver to do the dispatch but if the info is already there it will quit immediately
742         this.startServiceInfoResolver(info);
743 
744         return info;
745     }
746 
747     ServiceInfoImpl getServiceInfoFromCache(String type, String name, String subtype, boolean persistent) {
748         // Check if the answer is in the cache.
749         ServiceInfoImpl info = new ServiceInfoImpl(type, name, subtype, 0, 0, 0, persistent, (byte[]) null);
750         DNSEntry pointerEntry = this.getCache().getDNSEntry(new DNSRecord.Pointer(type, DNSRecordClass.CLASS_ANY, false, 0, info.getQualifiedName()));
751         if (pointerEntry instanceof DNSRecord) {
752             ServiceInfoImpl cachedInfo = (ServiceInfoImpl) ((DNSRecord) pointerEntry).getServiceInfo(persistent);
753             if (cachedInfo != null) {
754                 // To get a complete info record we need to retrieve the service, address and the text bytes.
755 
756                 Map<Fields, String> map = cachedInfo.getQualifiedNameMap();
757                 byte[] srvBytes = null;
758                 String server = "";
759                 DNSEntry serviceEntry = this.getCache().getDNSEntry(info.getQualifiedName(), DNSRecordType.TYPE_SRV, DNSRecordClass.CLASS_ANY);
760                 if (serviceEntry instanceof DNSRecord) {
761                     ServiceInfo cachedServiceEntryInfo = ((DNSRecord) serviceEntry).getServiceInfo(persistent);
762                     if (cachedServiceEntryInfo != null) {
763                         cachedInfo = new ServiceInfoImpl(map, cachedServiceEntryInfo.getPort(), cachedServiceEntryInfo.getWeight(), cachedServiceEntryInfo.getPriority(), persistent, (byte[]) null);
764                         srvBytes = cachedServiceEntryInfo.getTextBytes();
765                         server = cachedServiceEntryInfo.getServer();
766                     }
767                 }
768                 DNSEntry addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_ANY);
769                 if (addressEntry instanceof DNSRecord) {
770                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
771                     if (cachedAddressInfo != null) {
772                         for (Inet4Address address : cachedAddressInfo.getInet4Addresses()) {
773                             cachedInfo.addAddress(address);
774                         }
775                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
776                     }
777                 }
778                 addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_ANY);
779                 if (addressEntry instanceof DNSRecord) {
780                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
781                     if (cachedAddressInfo != null) {
782                         for (Inet6Address address : cachedAddressInfo.getInet6Addresses()) {
783                             cachedInfo.addAddress(address);
784                         }
785                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
786                     }
787                 }
788                 DNSEntry textEntry = this.getCache().getDNSEntry(cachedInfo.getQualifiedName(), DNSRecordType.TYPE_TXT, DNSRecordClass.CLASS_ANY);
789                 if (textEntry instanceof DNSRecord) {
790                     ServiceInfo cachedTextInfo = ((DNSRecord) textEntry).getServiceInfo(persistent);
791                     if (cachedTextInfo != null) {
792                         cachedInfo._setText(cachedTextInfo.getTextBytes());
793                     }
794                 }
795                 if (cachedInfo.getTextBytes().length == 0) {
796                     cachedInfo._setText(srvBytes);
797                 }
798                 if (cachedInfo.hasData()) {
799                     info = cachedInfo;
800                 }
801             }
802         }
803         return info;
804     }
805 
806     private void waitForInfoData(ServiceInfo info, long timeout) {
807         synchronized (info) {
808             long loops = (timeout / 200L);
809             if (loops < 1) {
810                 loops = 1;
811             }
812             for (int i = 0; i < loops; i++) {
813                 if (info.hasData()) {
814                     break;
815                 }
816                 try {
817                     info.wait(200);
818                 } catch (final InterruptedException e) {
819                     /* Stub */
820                 }
821             }
822         }
823     }
824 
825     /**
826      * {@inheritDoc}
827      */
828     @Override
829     public void requestServiceInfo(String type, String name) {
830         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
831     }
832 
833     /**
834      * {@inheritDoc}
835      */
836     @Override
837     public void requestServiceInfo(String type, String name, boolean persistent) {
838         this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
839     }
840 
841     /**
842      * {@inheritDoc}
843      */
844     @Override
845     public void requestServiceInfo(String type, String name, long timeout) {
846         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
847     }
848 
849     /**
850      * {@inheritDoc}
851      */
852     @Override
853     public void requestServiceInfo(String type, String name, boolean persistent, long timeout) {
854         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
855         this.waitForInfoData(info, timeout);
856     }
857 
858     void handleServiceResolved(ServiceEvent event) {
859         List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
860         final List<ServiceListenerStatus> listCopy;
861         if ((list != null) && (!list.isEmpty())) {
862             if ((event.getInfo() != null) && event.getInfo().hasData()) {
863                 final ServiceEvent localEvent = event;
864                 synchronized (list) {
865                     listCopy = new ArrayList<ServiceListenerStatus>(list);
866                 }
867                 for (final ServiceListenerStatus listener : listCopy) {
868                     _executor.submit(new Runnable() {
869                         /** {@inheritDoc} */
870                         @Override
871                         public void run() {
872                             listener.serviceResolved(localEvent);
873                         }
874                     });
875                 }
876             }
877         }
878     }
879 
880     /**
881      * {@inheritDoc}
882      */
883     @Override
884     public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
885         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
886         _typeListeners.add(status);
887 
888         // report cached service types
889         for (String type : _serviceTypes.keySet()) {
890             status.serviceTypeAdded(new ServiceEventImpl(this, type, "", null));
891         }
892 
893         this.startTypeResolver();
894     }
895 
896     /**
897      * {@inheritDoc}
898      */
899     @Override
900     public void removeServiceTypeListener(ServiceTypeListener listener) {
901         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
902         _typeListeners.remove(status);
903     }
904 
905     /**
906      * {@inheritDoc}
907      */
908     @Override
909     public void addServiceListener(String type, ServiceListener listener) {
910         this.addServiceListener(type, listener, ListenerStatus.ASYNCHONEOUS);
911     }
912 
913     private void addServiceListener(String type, ServiceListener listener, boolean synch) {
914         ServiceListenerStatus status = new ServiceListenerStatus(listener, synch);
915         final String loType = type.toLowerCase();
916         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
917         if (list == null) {
918             if (_serviceListeners.putIfAbsent(loType, new LinkedList<ServiceListenerStatus>()) == null) {
919                 if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
920                     // We have a problem here. The service collectors must be called synchronously so that their cache get cleaned up immediately or we will report .
921                     this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
922                 }
923             }
924             list = _serviceListeners.get(loType);
925         }
926         if (list != null) {
927             synchronized (list) {
928                 if (!list.contains(listener)) {
929                     list.add(status);
930                 }
931             }
932         }
933         // report cached service types
934         final List<ServiceEvent> serviceEvents = new ArrayList<ServiceEvent>();
935         Collection<DNSEntry> dnsEntryLits = this.getCache().allValues();
936         for (DNSEntry entry : dnsEntryLits) {
937             final DNSRecord record = (DNSRecord) entry;
938             if (record.getRecordType() == DNSRecordType.TYPE_SRV) {
939                 if (record.getKey().endsWith(loType)) {
940                     // Do not used the record embedded method for generating event this will not work.
941                     // serviceEvents.add(record.getServiceEvent(this));
942                     serviceEvents.add(new ServiceEventImpl(this, record.getType(), toUnqualifiedName(record.getType(), record.getName()), record.getServiceInfo()));
943                 }
944             }
945         }
946         // Actually call listener with all service events added above
947         for (ServiceEvent serviceEvent : serviceEvents) {
948             status.serviceAdded(serviceEvent);
949         }
950         // Create/start ServiceResolver
951         this.startServiceResolver(type);
952     }
953 
954     /**
955      * {@inheritDoc}
956      */
957     @Override
958     public void removeServiceListener(String type, ServiceListener listener) {
959         String loType = type.toLowerCase();
960         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
961         if (list != null) {
962             synchronized (list) {
963                 ServiceListenerStatus status = new ServiceListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
964                 list.remove(status);
965                 if (list.isEmpty()) {
966                     _serviceListeners.remove(loType, list);
967                 }
968             }
969         }
970     }
971 
972     /**
973      * {@inheritDoc}
974      */
975     @Override
976     public void registerService(ServiceInfo infoAbstract) throws IOException {
977         if (this.isClosing() || this.isClosed()) {
978             throw new IllegalStateException("This DNS is closed.");
979         }
980         final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;
981 
982         if (info.getDns() != null) {
983             if (info.getDns() != this) {
984                 throw new IllegalStateException("A service information can only be registered with a single instamce of JmDNS.");
985             } else if (_services.get(info.getKey()) != null) {
986                 throw new IllegalStateException("A service information can only be registered once.");
987             }
988         }
989         info.setDns(this);
990 
991         this.registerServiceType(info.getTypeWithSubtype());
992 
993         // bind the service to this address
994         info.recoverState();
995         info.setServer(_localHost.getName());
996         info.addAddress(_localHost.getInet4Address());
997         info.addAddress(_localHost.getInet6Address());
998 
999         this.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
1000 
1001         this.makeServiceNameUnique(info);
1002         while (_services.putIfAbsent(info.getKey(), info) != null) {
1003             this.makeServiceNameUnique(info);
1004         }
1005 
1006         this.startProber();
1007         info.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
1008 
1009         if (logger.isLoggable(Level.FINE)) {
1010             logger.fine("registerService() JmDNS registered service as " + info);
1011         }
1012     }
1013 
1014     /**
1015      * {@inheritDoc}
1016      */
1017     @Override
1018     public void unregisterService(ServiceInfo infoAbstract) {
1019         final ServiceInfoImpl info = (ServiceInfoImpl) _services.get(infoAbstract.getKey());
1020 
1021         if (info != null) {
1022             info.cancelState();
1023             this.startCanceler();
1024             info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1025 
1026             _services.remove(info.getKey(), info);
1027             if (logger.isLoggable(Level.FINE)) {
1028                 logger.fine("unregisterService() JmDNS unregistered service as " + info);
1029             }
1030         } else {
1031             logger.warning("Removing unregistered service info: " + infoAbstract.getKey());
1032         }
1033     }
1034 
1035     /**
1036      * {@inheritDoc}
1037      */
1038     @Override
1039     public void unregisterAllServices() {
1040         if (logger.isLoggable(Level.FINER)) {
1041             logger.finer("unregisterAllServices()");
1042         }
1043 
1044         for (String name : _services.keySet()) {
1045             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
1046             if (info != null) {
1047                 if (logger.isLoggable(Level.FINER)) {
1048                     logger.finer("Cancelling service info: " + info);
1049                 }
1050                 info.cancelState();
1051             }
1052         }
1053         this.startCanceler();
1054 
1055         for (String name : _services.keySet()) {
1056             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
1057             if (info != null) {
1058                 if (logger.isLoggable(Level.FINER)) {
1059                     logger.finer("Wait for service info cancel: " + info);
1060                 }
1061                 info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1062                 _services.remove(name, info);
1063             }
1064         }
1065 
1066     }
1067 
1068     /**
1069      * {@inheritDoc}
1070      */
1071     @Override
1072     public boolean registerServiceType(String type) {
1073         boolean typeAdded = false;
1074         Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(type);
1075         String domain = map.get(Fields.Domain);
1076         String protocol = map.get(Fields.Protocol);
1077         String application = map.get(Fields.Application);
1078         String subtype = map.get(Fields.Subtype);
1079 
1080         final String name = (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
1081         final String loname = name.toLowerCase();
1082         if (logger.isLoggable(Level.FINE)) {
1083             logger.fine(this.getName() + ".registering service type: " + type + " as: " + name + (subtype.length() > 0 ? " subtype: " + subtype : ""));
1084         }
1085         if (!_serviceTypes.containsKey(loname) && !application.toLowerCase().equals("dns-sd") && !domain.toLowerCase().endsWith("in-addr.arpa") && !domain.toLowerCase().endsWith("ip6.arpa")) {
1086             typeAdded = _serviceTypes.putIfAbsent(loname, new ServiceTypeEntry(name)) == null;
1087             if (typeAdded) {
1088                 final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
1089                 final ServiceEvent event = new ServiceEventImpl(this, name, "", null);
1090                 for (final ServiceTypeListenerStatus status : list) {
1091                     _executor.submit(new Runnable() {
1092                         /** {@inheritDoc} */
1093                         @Override
1094                         public void run() {
1095                             status.serviceTypeAdded(event);
1096                         }
1097                     });
1098                 }
1099             }
1100         }
1101         if (subtype.length() > 0) {
1102             ServiceTypeEntry subtypes = _serviceTypes.get(loname);
1103             if ((subtypes != null) && (!subtypes.contains(subtype))) {
1104                 synchronized (subtypes) {
1105                     if (!subtypes.contains(subtype)) {
1106                         typeAdded = true;
1107                         subtypes.add(subtype);
1108                         final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
1109                         final ServiceEvent event = new ServiceEventImpl(this, "_" + subtype + "._sub." + name, "", null);
1110                         for (final ServiceTypeListenerStatus status : list) {
1111                             _executor.submit(new Runnable() {
1112                                 /** {@inheritDoc} */
1113                                 @Override
1114                                 public void run() {
1115                                     status.subTypeForServiceTypeAdded(event);
1116                                 }
1117                             });
1118                         }
1119                     }
1120                 }
1121             }
1122         }
1123         return typeAdded;
1124     }
1125 
1126     /**
1127      * Generate a possibly unique name for a service using the information we have in the cache.
1128      *
1129      * @return returns true, if the name of the service info had to be changed.
1130      */
1131     private boolean makeServiceNameUnique(ServiceInfoImpl info) {
1132         final String originalQualifiedName = info.getKey();
1133         final long now = System.currentTimeMillis();
1134 
1135         boolean collision;
1136         do {
1137             collision = false;
1138 
1139             // Check for collision in cache
1140             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(info.getKey())) {
1141                 if (DNSRecordType.TYPE_SRV.equals(dnsEntry.getRecordType()) && !dnsEntry.isExpired(now)) {
1142                     final DNSRecord.Service s = (DNSRecord.Service) dnsEntry;
1143                     if (s.getPort() != info.getPort() || !s.getServer().equals(_localHost.getName())) {
1144                         if (logger.isLoggable(Level.FINER)) {
1145                             logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + dnsEntry + " s.server=" + s.getServer() + " " + _localHost.getName() + " equals:" + (s.getServer().equals(_localHost.getName())));
1146                         }
1147                         info.setName(incrementName(info.getName()));
1148                         collision = true;
1149                         break;
1150                     }
1151                 }
1152             }
1153 
1154             // Check for collision with other service infos published by JmDNS
1155             final ServiceInfo selfService = _services.get(info.getKey());
1156             if (selfService != null && selfService != info) {
1157                 info.setName(incrementName(info.getName()));
1158                 collision = true;
1159             }
1160         }
1161         while (collision);
1162 
1163         return !(originalQualifiedName.equals(info.getKey()));
1164     }
1165 
1166     String incrementName(String name) {
1167         String aName = name;
1168         try {
1169             final int l = aName.lastIndexOf('(');
1170             final int r = aName.lastIndexOf(')');
1171             if ((l >= 0) && (l < r)) {
1172                 aName = aName.substring(0, l) + "(" + (Integer.parseInt(aName.substring(l + 1, r)) + 1) + ")";
1173             } else {
1174                 aName += " (2)";
1175             }
1176         } catch (final NumberFormatException e) {
1177             aName += " (2)";
1178         }
1179         return aName;
1180     }
1181 
1182     /**
1183      * 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.
1184      *
1185      * @param listener
1186      *            DSN listener
1187      * @param question
1188      *            DNS query
1189      */
1190     public void addListener(DNSListener listener, DNSQuestion question) {
1191         final long now = System.currentTimeMillis();
1192 
1193         // add the new listener
1194         _listeners.add(listener);
1195 
1196         // report existing matched records
1197 
1198         if (question != null) {
1199             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(question.getName().toLowerCase())) {
1200                 if (question.answeredBy(dnsEntry) && !dnsEntry.isExpired(now)) {
1201                     listener.updateRecord(this.getCache(), now, dnsEntry);
1202                 }
1203             }
1204         }
1205     }
1206 
1207     /**
1208      * Remove a listener from all outstanding questions. The listener will no longer receive any updates.
1209      *
1210      * @param listener
1211      *            DSN listener
1212      */
1213     public void removeListener(DNSListener listener) {
1214         _listeners.remove(listener);
1215     }
1216 
1217     /**
1218      * Renew a service when the record become stale. If there is no service collector for the type this method does nothing.
1219      *
1220      * @param record
1221      *            DNS record
1222      */
1223     public void renewServiceCollector(DNSRecord record) {
1224         ServiceInfo info = record.getServiceInfo();
1225         if (_serviceCollectors.containsKey(info.getType().toLowerCase())) {
1226             // Create/start ServiceResolver
1227             this.startServiceResolver(info.getType());
1228         }
1229     }
1230 
1231     // Remind: Method updateRecord should receive a better name.
1232     /**
1233      * Notify all listeners that a record was updated.
1234      *
1235      * @param now
1236      *            update date
1237      * @param rec
1238      *            DNS record
1239      * @param operation
1240      *            DNS cache operation
1241      */
1242     public void updateRecord(long now, DNSRecord rec, Operation operation) {
1243         // We do not want to block the entire DNS while we are updating the record for each listener (service info)
1244         {
1245             List<DNSListener> listenerList = null;
1246             synchronized (_listeners) {
1247                 listenerList = new ArrayList<DNSListener>(_listeners);
1248             }
1249             for (DNSListener listener : listenerList) {
1250                 listener.updateRecord(this.getCache(), now, rec);
1251             }
1252         }
1253         if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()))
1254         // if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()) || DNSRecordType.TYPE_SRV.equals(rec.getRecordType()))
1255         {
1256             ServiceEvent event = rec.getServiceEvent(this);
1257             if ((event.getInfo() == null) || !event.getInfo().hasData()) {
1258                 // We do not care about the subtype because the info is only used if complete and the subtype will then be included.
1259                 ServiceInfo info = this.getServiceInfoFromCache(event.getType(), event.getName(), "", false);
1260                 if (info.hasData()) {
1261                     event = new ServiceEventImpl(this, event.getType(), event.getName(), info);
1262                 }
1263             }
1264 
1265             List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
1266             final List<ServiceListenerStatus> serviceListenerList;
1267             if (list != null) {
1268                 synchronized (list) {
1269                     serviceListenerList = new ArrayList<ServiceListenerStatus>(list);
1270                 }
1271             } else {
1272                 serviceListenerList = Collections.emptyList();
1273             }
1274             if (logger.isLoggable(Level.FINEST)) {
1275                 logger.finest(this.getName() + ".updating record for event: " + event + " list " + serviceListenerList + " operation: " + operation);
1276             }
1277             if (!serviceListenerList.isEmpty()) {
1278                 final ServiceEvent localEvent = event;
1279 
1280                 switch (operation) {
1281                     case Add:
1282                         for (final ServiceListenerStatus listener : serviceListenerList) {
1283                             if (listener.isSynchronous()) {
1284                                 listener.serviceAdded(localEvent);
1285                             } else {
1286                                 _executor.submit(new Runnable() {
1287                                     /** {@inheritDoc} */
1288                                     @Override
1289                                     public void run() {
1290                                         listener.serviceAdded(localEvent);
1291                                     }
1292                                 });
1293                             }
1294                         }
1295                         break;
1296                     case Remove:
1297                         for (final ServiceListenerStatus listener : serviceListenerList) {
1298                             if (listener.isSynchronous()) {
1299                                 listener.serviceRemoved(localEvent);
1300                             } else {
1301                                 _executor.submit(new Runnable() {
1302                                     /** {@inheritDoc} */
1303                                     @Override
1304                                     public void run() {
1305                                         listener.serviceRemoved(localEvent);
1306                                     }
1307                                 });
1308                             }
1309                         }
1310                         break;
1311                     default:
1312                         break;
1313                 }
1314             }
1315         }
1316     }
1317 
1318     void handleRecord(DNSRecord record, long now) {
1319         DNSRecord newRecord = record;
1320 
1321         Operation cacheOperation = Operation.Noop;
1322         final boolean expired = newRecord.isExpired(now);
1323         if (logger.isLoggable(Level.FINE)) {
1324             logger.fine(this.getName() + " handle response: " + newRecord);
1325         }
1326 
1327         // update the cache
1328         if (!newRecord.isServicesDiscoveryMetaQuery() && !newRecord.isDomainDiscoveryQuery()) {
1329             final boolean unique = newRecord.isUnique();
1330             final DNSRecord cachedRecord = (DNSRecord) this.getCache().getDNSEntry(newRecord);
1331             if (logger.isLoggable(Level.FINE)) {
1332                 logger.fine(this.getName() + " handle response cached record: " + cachedRecord);
1333             }
1334             if (unique) {
1335                 for (DNSEntry entry : this.getCache().getDNSEntryList(newRecord.getKey())) {
1336                     if (newRecord.getRecordType().equals(entry.getRecordType()) && newRecord.getRecordClass().equals(entry.getRecordClass()) && (entry != cachedRecord)) {
1337                         ((DNSRecord) entry).setWillExpireSoon(now);
1338                     }
1339                 }
1340             }
1341             if (cachedRecord != null) {
1342                 if (expired) {
1343                     // if the record has a 0 ttl that means we have a cancel record we need to delay the removal by 1s
1344                     if (newRecord.getTTL() == 0) {
1345                         cacheOperation = Operation.Noop;
1346                         cachedRecord.setWillExpireSoon(now);
1347                         // the actual record will be disposed of by the record reaper.
1348                     } else {
1349                         cacheOperation = Operation.Remove;
1350                         this.getCache().removeDNSEntry(cachedRecord);
1351                     }
1352                 } else {
1353                     // If the record content has changed we need to inform our listeners.
1354                     if (!newRecord.sameValue(cachedRecord) || (!newRecord.sameSubtype(cachedRecord) && (newRecord.getSubtype().length() > 0))) {
1355                         if (newRecord.isSingleValued()) {
1356                             cacheOperation = Operation.Update;
1357                             this.getCache().replaceDNSEntry(newRecord, cachedRecord);
1358                         } else {
1359                             // Address record can have more than one value on multi-homed machines
1360                             cacheOperation = Operation.Add;
1361                             this.getCache().addDNSEntry(newRecord);
1362                         }
1363                     } else {
1364                         cachedRecord.resetTTL(newRecord);
1365                         newRecord = cachedRecord;
1366                     }
1367                 }
1368             } else {
1369                 if (!expired) {
1370                     cacheOperation = Operation.Add;
1371                     this.getCache().addDNSEntry(newRecord);
1372                 }
1373             }
1374         }
1375 
1376         // Register new service types
1377         if (newRecord.getRecordType() == DNSRecordType.TYPE_PTR) {
1378             // handle DNSConstants.DNS_META_QUERY records
1379             boolean typeAdded = false;
1380             if (newRecord.isServicesDiscoveryMetaQuery()) {
1381                 // The service names are in the alias.
1382                 if (!expired) {
1383                     typeAdded = this.registerServiceType(((DNSRecord.Pointer) newRecord).getAlias());
1384                 }
1385                 return;
1386             }
1387             typeAdded |= this.registerServiceType(newRecord.getName());
1388             if (typeAdded && (cacheOperation == Operation.Noop)) {
1389                 cacheOperation = Operation.RegisterServiceType;
1390             }
1391         }
1392 
1393         // notify the listeners
1394         if (cacheOperation != Operation.Noop) {
1395             this.updateRecord(now, newRecord, cacheOperation);
1396         }
1397 
1398     }
1399 
1400     /**
1401      * Handle an incoming response. Cache answers, and pass them on to the appropriate questions.
1402      *
1403      * @exception IOException
1404      */
1405     void handleResponse(DNSIncoming msg) throws IOException {
1406         final long now = System.currentTimeMillis();
1407 
1408         boolean hostConflictDetected = false;
1409         boolean serviceConflictDetected = false;
1410 
1411         for (DNSRecord newRecord : msg.getAllAnswers()) {
1412             this.handleRecord(newRecord, now);
1413 
1414             if (DNSRecordType.TYPE_A.equals(newRecord.getRecordType()) || DNSRecordType.TYPE_AAAA.equals(newRecord.getRecordType())) {
1415                 hostConflictDetected |= newRecord.handleResponse(this);
1416             } else {
1417                 serviceConflictDetected |= newRecord.handleResponse(this);
1418             }
1419 
1420         }
1421 
1422         if (hostConflictDetected || serviceConflictDetected) {
1423             this.startProber();
1424         }
1425     }
1426 
1427     /**
1428      * Handle an incoming query. See if we can answer any part of it given our service infos.
1429      *
1430      * @param in
1431      * @param addr
1432      * @param port
1433      * @exception IOException
1434      */
1435     void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException {
1436         if (logger.isLoggable(Level.FINE)) {
1437             logger.fine(this.getName() + ".handle query: " + in);
1438         }
1439         // Track known answers
1440         boolean conflictDetected = false;
1441         final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
1442         for (DNSRecord answer : in.getAllAnswers()) {
1443             conflictDetected |= answer.handleQuery(this, expirationTime);
1444         }
1445 
1446         this.ioLock();
1447         try {
1448 
1449             if (_plannedAnswer != null) {
1450                 _plannedAnswer.append(in);
1451             } else {
1452                 DNSIncoming plannedAnswer = in.clone();
1453                 if (in.isTruncated()) {
1454                     _plannedAnswer = plannedAnswer;
1455                 }
1456                 this.startResponder(plannedAnswer, port);
1457             }
1458 
1459         } finally {
1460             this.ioUnlock();
1461         }
1462 
1463         final long now = System.currentTimeMillis();
1464         for (DNSRecord answer : in.getAnswers()) {
1465             this.handleRecord(answer, now);
1466         }
1467 
1468         if (conflictDetected) {
1469             this.startProber();
1470         }
1471     }
1472 
1473     public void respondToQuery(DNSIncoming in) {
1474         this.ioLock();
1475         try {
1476             if (_plannedAnswer == in) {
1477                 _plannedAnswer = null;
1478             }
1479         } finally {
1480             this.ioUnlock();
1481         }
1482     }
1483 
1484     /**
1485      * Add an answer to a question. Deal with the case when the outgoing packet overflows
1486      *
1487      * @param in
1488      * @param addr
1489      * @param port
1490      * @param out
1491      * @param rec
1492      * @return outgoing answer
1493      * @exception IOException
1494      */
1495     public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException {
1496         DNSOutgoing newOut = out;
1497         if (newOut == null) {
1498             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
1499         }
1500         try {
1501             newOut.addAnswer(in, rec);
1502         } catch (final IOException e) {
1503             newOut.setFlags(newOut.getFlags() | DNSConstants.FLAGS_TC);
1504             newOut.setId(in.getId());
1505             send(newOut);
1506 
1507             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
1508             newOut.addAnswer(in, rec);
1509         }
1510         return newOut;
1511     }
1512 
1513     /**
1514      * Send an outgoing multicast DNS message.
1515      *
1516      * @param out
1517      * @exception IOException
1518      */
1519     public void send(DNSOutgoing out) throws IOException {
1520         if (!out.isEmpty()) {
1521             byte[] message = out.data();
1522             final DatagramPacket packet = new DatagramPacket(message, message.length, _group, DNSConstants.MDNS_PORT);
1523 
1524             if (logger.isLoggable(Level.FINEST)) {
1525                 try {
1526                     final DNSIncoming msg = new DNSIncoming(packet);
1527                     if (logger.isLoggable(Level.FINEST)) {
1528                         logger.finest("send(" + this.getName() + ") JmDNS out:" + msg.print(true));
1529                     }
1530                 } catch (final IOException e) {
1531                     logger.throwing(getClass().toString(), "send(" + this.getName() + ") - JmDNS can not parse what it sends!!!", e);
1532                 }
1533             }
1534             final MulticastSocket ms = _socket;
1535             if (ms != null && !ms.isClosed()) {
1536                 ms.send(packet);
1537             }
1538         }
1539     }
1540 
1541     /*
1542      * (non-Javadoc)
1543      * @see javax.jmdns.impl.DNSTaskStarter#purgeTimer()
1544      */
1545     @Override
1546     public void purgeTimer() {
1547         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeTimer();
1548     }
1549 
1550     /*
1551      * (non-Javadoc)
1552      * @see javax.jmdns.impl.DNSTaskStarter#purgeStateTimer()
1553      */
1554     @Override
1555     public void purgeStateTimer() {
1556         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeStateTimer();
1557     }
1558 
1559     /*
1560      * (non-Javadoc)
1561      * @see javax.jmdns.impl.DNSTaskStarter#cancelTimer()
1562      */
1563     @Override
1564     public void cancelTimer() {
1565         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelTimer();
1566     }
1567 
1568     /*
1569      * (non-Javadoc)
1570      * @see javax.jmdns.impl.DNSTaskStarter#cancelStateTimer()
1571      */
1572     @Override
1573     public void cancelStateTimer() {
1574         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelStateTimer();
1575     }
1576 
1577     /*
1578      * (non-Javadoc)
1579      * @see javax.jmdns.impl.DNSTaskStarter#startProber()
1580      */
1581     @Override
1582     public void startProber() {
1583         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startProber();
1584     }
1585 
1586     /*
1587      * (non-Javadoc)
1588      * @see javax.jmdns.impl.DNSTaskStarter#startAnnouncer()
1589      */
1590     @Override
1591     public void startAnnouncer() {
1592         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startAnnouncer();
1593     }
1594 
1595     /*
1596      * (non-Javadoc)
1597      * @see javax.jmdns.impl.DNSTaskStarter#startRenewer()
1598      */
1599     @Override
1600     public void startRenewer() {
1601         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startRenewer();
1602     }
1603 
1604     /*
1605      * (non-Javadoc)
1606      * @see javax.jmdns.impl.DNSTaskStarter#startCanceler()
1607      */
1608     @Override
1609     public void startCanceler() {
1610         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startCanceler();
1611     }
1612 
1613     /*
1614      * (non-Javadoc)
1615      * @see javax.jmdns.impl.DNSTaskStarter#startReaper()
1616      */
1617     @Override
1618     public void startReaper() {
1619         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startReaper();
1620     }
1621 
1622     /*
1623      * (non-Javadoc)
1624      * @see javax.jmdns.impl.DNSTaskStarter#startServiceInfoResolver(javax.jmdns.impl.ServiceInfoImpl)
1625      */
1626     @Override
1627     public void startServiceInfoResolver(ServiceInfoImpl info) {
1628         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceInfoResolver(info);
1629     }
1630 
1631     /*
1632      * (non-Javadoc)
1633      * @see javax.jmdns.impl.DNSTaskStarter#startTypeResolver()
1634      */
1635     @Override
1636     public void startTypeResolver() {
1637         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startTypeResolver();
1638     }
1639 
1640     /*
1641      * (non-Javadoc)
1642      * @see javax.jmdns.impl.DNSTaskStarter#startServiceResolver(java.lang.String)
1643      */
1644     @Override
1645     public void startServiceResolver(String type) {
1646         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceResolver(type);
1647     }
1648 
1649     /*
1650      * (non-Javadoc)
1651      * @see javax.jmdns.impl.DNSTaskStarter#startResponder(javax.jmdns.impl.DNSIncoming, int)
1652      */
1653     @Override
1654     public void startResponder(DNSIncoming in, int port) {
1655         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startResponder(in, port);
1656     }
1657 
1658     // REMIND: Why is this not an anonymous inner class?
1659     /**
1660      * Shutdown operations.
1661      */
1662     protected class Shutdown implements Runnable {
1663         /** {@inheritDoc} */
1664         @Override
1665         public void run() {
1666             try {
1667                 _shutdown = null;
1668                 close();
1669             } catch (Throwable exception) {
1670                 System.err.println("Error while shuting down. " + exception);
1671             }
1672         }
1673     }
1674 
1675     private final Object _recoverLock = new Object();
1676 
1677     /**
1678      * Recover jmdns when there is an error.
1679      */
1680     public void recover() {
1681         logger.finer(this.getName() + "recover()");
1682         // We have an IO error so lets try to recover if anything happens lets close it.
1683         // This should cover the case of the IP address changing under our feet
1684         if (this.isClosing() || this.isClosed() || this.isCanceling() || this.isCanceled()) {
1685             return;
1686         }
1687 
1688         // 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
1689         // 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.
1690         synchronized (_recoverLock) {
1691             // Stop JmDNS
1692             // This protects against recursive calls
1693             if (this.cancelState()) {
1694                 logger.finer(this.getName() + "recover() thread " + Thread.currentThread().getName());
1695                 Thread recover = new Thread(this.getName() + ".recover()") {
1696                     /**
1697                      * {@inheritDoc}
1698                      */
1699                     @Override
1700                     public void run() {
1701                         __recover();
1702                     }
1703                 };
1704                 recover.start();
1705             }
1706         }
1707     }
1708 
1709     void __recover() {
1710         // Synchronize only if we are not already in process to prevent dead locks
1711         //
1712         if (logger.isLoggable(Level.FINER)) {
1713             logger.finer(this.getName() + "recover() Cleanning up");
1714         }
1715 
1716         logger.warning("RECOVERING");
1717         // Purge the timer
1718         this.purgeTimer();
1719 
1720         // We need to keep a copy for reregistration
1721         final Collection<ServiceInfo> oldServiceInfos = new ArrayList<ServiceInfo>(getServices().values());
1722 
1723         // Cancel all services
1724         this.unregisterAllServices();
1725         this.disposeServiceCollectors();
1726 
1727         this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1728 
1729         // Purge the canceler timer
1730         this.purgeStateTimer();
1731 
1732         //
1733         // close multicast socket
1734         this.closeMulticastSocket();
1735 
1736         //
1737         this.getCache().clear();
1738         if (logger.isLoggable(Level.FINER)) {
1739             logger.finer(this.getName() + "recover() All is clean");
1740         }
1741 
1742         if (this.isCanceled()) {
1743             //
1744             // All is clear now start the services
1745             //
1746             for (ServiceInfo info : oldServiceInfos) {
1747                 ((ServiceInfoImpl) info).recoverState();
1748             }
1749             this.recoverState();
1750 
1751             try {
1752                 this.openMulticastSocket(this.getLocalHost());
1753                 this.start(oldServiceInfos);
1754             } catch (final Exception exception) {
1755                 logger.log(Level.WARNING, this.getName() + "recover() Start services exception ", exception);
1756             }
1757             logger.log(Level.WARNING, this.getName() + "recover() We are back!");
1758         } else {
1759             // We have a problem. We could not clear the state.
1760             logger.log(Level.WARNING, this.getName() + "recover() Could not recover we are Down!");
1761             if (this.getDelegate() != null) {
1762                 this.getDelegate().cannotRecoverFromIOError(this.getDns(), oldServiceInfos);
1763             }
1764         }
1765 
1766     }
1767 
1768     public void cleanCache() {
1769         long now = System.currentTimeMillis();
1770         for (DNSEntry entry : this.getCache().allValues()) {
1771             try {
1772                 DNSRecord record = (DNSRecord) entry;
1773                 if (record.isExpired(now)) {
1774                     this.updateRecord(now, record, Operation.Remove);
1775                     this.getCache().removeDNSEntry(record);
1776                 } else if (record.isStale(now)) {
1777                     // we should query for the record we care about i.e. those in the service collectors
1778                     this.renewServiceCollector(record);
1779                 }
1780             } catch (Exception exception) {
1781                 logger.log(Level.SEVERE, this.getName() + ".Error while reaping records: " + entry, exception);
1782                 logger.severe(this.toString());
1783             }
1784         }
1785     }
1786 
1787     /**
1788      * {@inheritDoc}
1789      */
1790     @Override
1791     public void close() {
1792         if (this.isClosing()) {
1793             return;
1794         }
1795 
1796         if (logger.isLoggable(Level.FINER)) {
1797             logger.finer("Cancelling JmDNS: " + this);
1798         }
1799         // Stop JmDNS
1800         // This protects against recursive calls
1801         if (this.closeState()) {
1802             // We got the tie break now clean up
1803 
1804             // Stop the timer
1805             logger.finer("Canceling the timer");
1806             this.cancelTimer();
1807 
1808             // Cancel all services
1809             this.unregisterAllServices();
1810             this.disposeServiceCollectors();
1811 
1812             if (logger.isLoggable(Level.FINER)) {
1813                 logger.finer("Wait for JmDNS cancel: " + this);
1814             }
1815             this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
1816 
1817             // Stop the canceler timer
1818             logger.finer("Canceling the state timer");
1819             this.cancelStateTimer();
1820 
1821             // Stop the executor
1822             _executor.shutdown();
1823 
1824             // close socket
1825             this.closeMulticastSocket();
1826 
1827             // remove the shutdown hook
1828             if (_shutdown != null) {
1829                 Runtime.getRuntime().removeShutdownHook(_shutdown);
1830             }
1831 
1832             if (logger.isLoggable(Level.FINER)) {
1833                 logger.finer("JmDNS closed.");
1834             }
1835         }
1836         advanceState(null);
1837     }
1838 
1839     /**
1840      * {@inheritDoc}
1841      */
1842     @Override
1843     @Deprecated
1844     public void printServices() {
1845         System.err.println(toString());
1846     }
1847 
1848     /**
1849      * {@inheritDoc}
1850      */
1851     @Override
1852     public String toString() {
1853         final StringBuilder aLog = new StringBuilder(2048);
1854         aLog.append("\t---- Local Host -----");
1855         aLog.append("\n\t");
1856         aLog.append(_localHost);
1857         aLog.append("\n\t---- Services -----");
1858         for (String key : _services.keySet()) {
1859             aLog.append("\n\t\tService: ");
1860             aLog.append(key);
1861             aLog.append(": ");
1862             aLog.append(_services.get(key));
1863         }
1864         aLog.append("\n");
1865         aLog.append("\t---- Types ----");
1866         for (String key : _serviceTypes.keySet()) {
1867             ServiceTypeEntry subtypes = _serviceTypes.get(key);
1868             aLog.append("\n\t\tType: ");
1869             aLog.append(subtypes.getType());
1870             aLog.append(": ");
1871             aLog.append(subtypes.isEmpty() ? "no subtypes" : subtypes);
1872         }
1873         aLog.append("\n");
1874         aLog.append(_cache.toString());
1875         aLog.append("\n");
1876         aLog.append("\t---- Service Collectors ----");
1877         for (String key : _serviceCollectors.keySet()) {
1878             aLog.append("\n\t\tService Collector: ");
1879             aLog.append(key);
1880             aLog.append(": ");
1881             aLog.append(_serviceCollectors.get(key));
1882         }
1883         aLog.append("\n");
1884         aLog.append("\t---- Service Listeners ----");
1885         for (String key : _serviceListeners.keySet()) {
1886             aLog.append("\n\t\tService Listener: ");
1887             aLog.append(key);
1888             aLog.append(": ");
1889             aLog.append(_serviceListeners.get(key));
1890         }
1891         return aLog.toString();
1892     }
1893 
1894     /**
1895      * {@inheritDoc}
1896      */
1897     @Override
1898     public ServiceInfo[] list(String type) {
1899         return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
1900     }
1901 
1902     /**
1903      * {@inheritDoc}
1904      */
1905     @Override
1906     public ServiceInfo[] list(String type, long timeout) {
1907         this.cleanCache();
1908         // Implementation note: The first time a list for a given type is
1909         // requested, a ServiceCollector is created which collects service
1910         // infos. This greatly speeds up the performance of subsequent calls
1911         // to this method. The caveats are, that 1) the first call to this
1912         // method for a given type is slow, and 2) we spawn a ServiceCollector
1913         // instance for each service type which increases network traffic a
1914         // little.
1915 
1916         String loType = type.toLowerCase();
1917 
1918         boolean newCollectorCreated = false;
1919         if (this.isCanceling() || this.isCanceled()) {
1920             return new ServiceInfo[0];
1921         }
1922 
1923         ServiceCollector collector = _serviceCollectors.get(loType);
1924         if (collector == null) {
1925             newCollectorCreated = _serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null;
1926             collector = _serviceCollectors.get(loType);
1927             if (newCollectorCreated) {
1928                 this.addServiceListener(type, collector, ListenerStatus.SYNCHONEOUS);
1929             }
1930         }
1931         if (logger.isLoggable(Level.FINER)) {
1932             logger.finer(this.getName() + ".collector: " + collector);
1933         }
1934         // At this stage the collector should never be null but it keeps findbugs happy.
1935         return (collector != null ? collector.list(timeout) : new ServiceInfo[0]);
1936     }
1937 
1938     /**
1939      * {@inheritDoc}
1940      */
1941     @Override
1942     public Map<String, ServiceInfo[]> listBySubtype(String type) {
1943         return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT);
1944     }
1945 
1946     /**
1947      * {@inheritDoc}
1948      */
1949     @Override
1950     public Map<String, ServiceInfo[]> listBySubtype(String type, long timeout) {
1951         Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5);
1952         for (ServiceInfo info : this.list(type, timeout)) {
1953             String subtype = info.getSubtype().toLowerCase();
1954             if (!map.containsKey(subtype)) {
1955                 map.put(subtype, new ArrayList<ServiceInfo>(10));
1956             }
1957             map.get(subtype).add(info);
1958         }
1959 
1960         Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size());
1961         for (String subtype : map.keySet()) {
1962             List<ServiceInfo> infoForSubType = map.get(subtype);
1963             result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()]));
1964         }
1965 
1966         return result;
1967     }
1968 
1969     /**
1970      * This method disposes all ServiceCollector instances which have been created by calls to method <code>list(type)</code>.
1971      *
1972      * @see #list
1973      */
1974     private void disposeServiceCollectors() {
1975         if (logger.isLoggable(Level.FINER)) {
1976             logger.finer("disposeServiceCollectors()");
1977         }
1978         for (String type : _serviceCollectors.keySet()) {
1979             ServiceCollector collector = _serviceCollectors.get(type);
1980             if (collector != null) {
1981                 this.removeServiceListener(type, collector);
1982                 _serviceCollectors.remove(type, collector);
1983             }
1984         }
1985     }
1986 
1987     /**
1988      * Instances of ServiceCollector are used internally to speed up the performance of method <code>list(type)</code>.
1989      *
1990      * @see #list
1991      */
1992     private static class ServiceCollector implements ServiceListener {
1993         // private static Logger logger = Logger.getLogger(ServiceCollector.class.getName());
1994 
1995         /**
1996          * A set of collected service instance names.
1997          */
1998         private final ConcurrentMap<String, ServiceInfo>  _infos;
1999 
2000         /**
2001          * A set of collected service event waiting to be resolved.
2002          */
2003         private final ConcurrentMap<String, ServiceEvent> _events;
2004 
2005         /**
2006          * This is the type we are listening for (only used for debugging).
2007          */
2008         private final String                              _type;
2009 
2010         /**
2011          * This is used to force a wait on the first invocation of list.
2012          */
2013         private volatile boolean                          _needToWaitForInfos;
2014 
2015         public ServiceCollector(String type) {
2016             super();
2017             _infos = new ConcurrentHashMap<String, ServiceInfo>();
2018             _events = new ConcurrentHashMap<String, ServiceEvent>();
2019             _type = type;
2020             _needToWaitForInfos = true;
2021         }
2022 
2023         /**
2024          * A service has been added.
2025          *
2026          * @param event
2027          *            service event
2028          */
2029         @Override
2030         public void serviceAdded(ServiceEvent event) {
2031             synchronized (this) {
2032                 ServiceInfo info = event.getInfo();
2033                 if ((info != null) && (info.hasData())) {
2034                     _infos.put(event.getName(), info);
2035                 } else {
2036                     String subtype = (info != null ? info.getSubtype() : "");
2037                     info = ((JmDNSImpl) event.getDNS()).resolveServiceInfo(event.getType(), event.getName(), subtype, true);
2038                     if (info != null) {
2039                         _infos.put(event.getName(), info);
2040                     } else {
2041                         _events.put(event.getName(), event);
2042                     }
2043                 }
2044             }
2045         }
2046 
2047         /**
2048          * A service has been removed.
2049          *
2050          * @param event
2051          *            service event
2052          */
2053         @Override
2054         public void serviceRemoved(ServiceEvent event) {
2055             synchronized (this) {
2056                 _infos.remove(event.getName());
2057                 _events.remove(event.getName());
2058             }
2059         }
2060 
2061         /**
2062          * A service has been resolved. Its details are now available in the ServiceInfo record.
2063          *
2064          * @param event
2065          *            service event
2066          */
2067         @Override
2068         public void serviceResolved(ServiceEvent event) {
2069             synchronized (this) {
2070                 _infos.put(event.getName(), event.getInfo());
2071                 _events.remove(event.getName());
2072             }
2073         }
2074 
2075         /**
2076          * Returns an array of all service infos which have been collected by this ServiceCollector.
2077          *
2078          * @param timeout
2079          *            timeout if the info list is empty.
2080          * @return Service Info array
2081          */
2082         public ServiceInfo[] list(long timeout) {
2083             if (_infos.isEmpty() || !_events.isEmpty() || _needToWaitForInfos) {
2084                 long loops = (timeout / 200L);
2085                 if (loops < 1) {
2086                     loops = 1;
2087                 }
2088                 for (int i = 0; i < loops; i++) {
2089                     try {
2090                         Thread.sleep(200);
2091                     } catch (final InterruptedException e) {
2092                         /* Stub */
2093                     }
2094                     if (_events.isEmpty() && !_infos.isEmpty() && !_needToWaitForInfos) {
2095                         break;
2096                     }
2097                 }
2098             }
2099             _needToWaitForInfos = false;
2100             return _infos.values().toArray(new ServiceInfo[_infos.size()]);
2101         }
2102 
2103         /**
2104          * {@inheritDoc}
2105          */
2106         @Override
2107         public String toString() {
2108             final StringBuffer aLog = new StringBuffer();
2109             aLog.append("\n\tType: ");
2110             aLog.append(_type);
2111             if (_infos.isEmpty()) {
2112                 aLog.append("\n\tNo services collected.");
2113             } else {
2114                 aLog.append("\n\tServices");
2115                 for (String key : _infos.keySet()) {
2116                     aLog.append("\n\t\tService: ");
2117                     aLog.append(key);
2118                     aLog.append(": ");
2119                     aLog.append(_infos.get(key));
2120                 }
2121             }
2122             if (_events.isEmpty()) {
2123                 aLog.append("\n\tNo event queued.");
2124             } else {
2125                 aLog.append("\n\tEvents");
2126                 for (String key : _events.keySet()) {
2127                     aLog.append("\n\t\tEvent: ");
2128                     aLog.append(key);
2129                     aLog.append(": ");
2130                     aLog.append(_events.get(key));
2131                 }
2132             }
2133             return aLog.toString();
2134         }
2135     }
2136 
2137     static String toUnqualifiedName(String type, String qualifiedName) {
2138         String loType = type.toLowerCase();
2139         String loQualifiedName = qualifiedName.toLowerCase();
2140         if (loQualifiedName.endsWith(loType) && !(loQualifiedName.equals(loType))) {
2141             return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
2142         }
2143         return qualifiedName;
2144     }
2145 
2146     public Map<String, ServiceInfo> getServices() {
2147         return _services;
2148     }
2149 
2150     public void setLastThrottleIncrement(long lastThrottleIncrement) {
2151         this._lastThrottleIncrement = lastThrottleIncrement;
2152     }
2153 
2154     public long getLastThrottleIncrement() {
2155         return _lastThrottleIncrement;
2156     }
2157 
2158     public void setThrottle(int throttle) {
2159         this._throttle = throttle;
2160     }
2161 
2162     public int getThrottle() {
2163         return _throttle;
2164     }
2165 
2166     public static Random getRandom() {
2167         return _random;
2168     }
2169 
2170     public void ioLock() {
2171         _ioLock.lock();
2172     }
2173 
2174     public void ioUnlock() {
2175         _ioLock.unlock();
2176     }
2177 
2178     public void setPlannedAnswer(DNSIncoming plannedAnswer) {
2179         this._plannedAnswer = plannedAnswer;
2180     }
2181 
2182     public DNSIncoming getPlannedAnswer() {
2183         return _plannedAnswer;
2184     }
2185 
2186     void setLocalHost(HostInfo localHost) {
2187         this._localHost = localHost;
2188     }
2189 
2190     public Map<String, ServiceTypeEntry> getServiceTypes() {
2191         return _serviceTypes;
2192     }
2193 
2194     public MulticastSocket getSocket() {
2195         return _socket;
2196     }
2197 
2198     public InetAddress getGroup() {
2199         return _group;
2200     }
2201 
2202     @Override
2203     public Delegate getDelegate() {
2204         return this._delegate;
2205     }
2206 
2207     @Override
2208     public Delegate setDelegate(Delegate delegate) {
2209         Delegate previous = this._delegate;
2210         this._delegate = delegate;
2211         return previous;
2212     }
2213 
2214 }