1// /Copyright 2003-2005 Arthur van Hoff, Rick Blair
2// Licensed under Apache License version 2.0
3// Original license LGPL
4
5package javax.jmdns.impl;
6
7import java.io.IOException;
8import java.net.DatagramPacket;
9import java.net.Inet4Address;
10import java.net.Inet6Address;
11import java.net.InetAddress;
12import java.net.MulticastSocket;
13import java.net.SocketException;
14import java.util.AbstractMap;
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.Collections;
18import java.util.HashMap;
19import java.util.HashSet;
20import java.util.Iterator;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Map;
24import java.util.Properties;
25import java.util.Random;
26import java.util.Set;
27import java.util.concurrent.ConcurrentHashMap;
28import java.util.concurrent.ConcurrentMap;
29import java.util.concurrent.ExecutorService;
30import java.util.concurrent.Executors;
31import java.util.concurrent.locks.ReentrantLock;
32import java.util.logging.Level;
33import java.util.logging.Logger;
34
35import javax.jmdns.JmDNS;
36import javax.jmdns.ServiceEvent;
37import javax.jmdns.ServiceInfo;
38import javax.jmdns.ServiceInfo.Fields;
39import javax.jmdns.ServiceListener;
40import javax.jmdns.ServiceTypeListener;
41import javax.jmdns.impl.ListenerStatus.ServiceListenerStatus;
42import javax.jmdns.impl.ListenerStatus.ServiceTypeListenerStatus;
43import javax.jmdns.impl.constants.DNSConstants;
44import javax.jmdns.impl.constants.DNSRecordClass;
45import javax.jmdns.impl.constants.DNSRecordType;
46import javax.jmdns.impl.constants.DNSState;
47import 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 */
56public 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 getInetAddress() throws IOException {
694        return _localHost.getInetAddress();
695    }
696
697    /**
698     * {@inheritDoc}
699     */
700    @Override
701    @Deprecated
702    public InetAddress getInterface() throws IOException {
703        return _socket.getInterface();
704    }
705
706    /**
707     * {@inheritDoc}
708     */
709    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
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
918    public void addServiceListener(String type, ServiceListener listener) {
919        this.addServiceListener(type, listener, ListenerStatus.ASYNCHONEOUS);
920    }
921
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
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
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
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
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
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     */
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
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     */
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     */
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     */
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     */
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
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     */
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     */
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
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     */
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     */
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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     */
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
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
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
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
1853    public void printServices() {
1854        System.err.println(toString());
1855    }
1856
1857    /**
1858     * {@inheritDoc}
1859     */
1860    @Override
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
1907    public ServiceInfo[] list(String type) {
1908        return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
1909    }
1910
1911    /**
1912     * {@inheritDoc}
1913     */
1914    @Override
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
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
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     */
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
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
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
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
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         */
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
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
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
2155    public Map<String, ServiceInfo> getServices() {
2156        return _services;
2157    }
2158
2159    public void setLastThrottleIncrement(long lastThrottleIncrement) {
2160        this._lastThrottleIncrement = lastThrottleIncrement;
2161    }
2162
2163    public long getLastThrottleIncrement() {
2164        return _lastThrottleIncrement;
2165    }
2166
2167    public void setThrottle(int throttle) {
2168        this._throttle = throttle;
2169    }
2170
2171    public int getThrottle() {
2172        return _throttle;
2173    }
2174
2175    public static Random getRandom() {
2176        return _random;
2177    }
2178
2179    public void ioLock() {
2180        _ioLock.lock();
2181    }
2182
2183    public void ioUnlock() {
2184        _ioLock.unlock();
2185    }
2186
2187    public void setPlannedAnswer(DNSIncoming plannedAnswer) {
2188        this._plannedAnswer = plannedAnswer;
2189    }
2190
2191    public DNSIncoming getPlannedAnswer() {
2192        return _plannedAnswer;
2193    }
2194
2195    void setLocalHost(HostInfo localHost) {
2196        this._localHost = localHost;
2197    }
2198
2199    public Map<String, ServiceTypeEntry> getServiceTypes() {
2200        return _serviceTypes;
2201    }
2202
2203    public MulticastSocket getSocket() {
2204        return _socket;
2205    }
2206
2207    public InetAddress getGroup() {
2208        return _group;
2209    }
2210
2211    @Override
2212    public Delegate getDelegate() {
2213        return this._delegate;
2214    }
2215
2216    @Override
2217    public Delegate setDelegate(Delegate delegate) {
2218        Delegate previous = this._delegate;
2219        this._delegate = delegate;
2220        return previous;
2221    }
2222
2223}
2224