001    /**
002     * Copyright 2003-2005 Arthur van Hoff, Rick Blair
003     *
004     * Licensed to the Apache Software Foundation (ASF) under one or more
005     * contributor license agreements.  See the NOTICE file distributed with
006     * this work for additional information regarding copyright ownership.
007     * The ASF licenses this file to You under the Apache License, Version 2.0
008     * (the "License"); you may not use this file except in compliance with
009     * the License.  You may obtain a copy of the License at
010     *
011     *      http://www.apache.org/licenses/LICENSE-2.0
012     *
013     * Unless required by applicable law or agreed to in writing, software
014     * distributed under the License is distributed on an "AS IS" BASIS,
015     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016     * See the License for the specific language governing permissions and
017     * limitations under the License.
018     */
019    package org.apache.activemq.jmdns;
020    
021    import java.io.IOException;
022    import java.net.DatagramPacket;
023    import java.net.InetAddress;
024    import java.net.MulticastSocket;
025    import java.util.*;
026    import java.util.logging.Level;
027    import java.util.logging.Logger;
028    
029    // REMIND: multiple IP addresses
030    
031    /**
032     * mDNS implementation in Java.
033     *
034     * @version %I%, %G%
035     * @author      Arthur van Hoff, Rick Blair, Jeff Sonstein,
036     * Werner Randelshofer, Pierre Frisch, Scott Lewis
037     */
038    public class JmDNS
039    {
040        private static Logger logger = Logger.getLogger(JmDNS.class.toString());
041        /**
042         * The version of JmDNS.
043         */
044        public static String VERSION = "2.0";
045    
046        /**
047         * This is the multicast group, we are listening to for multicast DNS messages.
048         */
049        private InetAddress group;
050        /**
051         * This is our multicast socket.
052         */
053        private MulticastSocket socket;
054    
055        /**
056         * Used to fix live lock problem on unregester.
057         */
058    
059         protected boolean closed = false;
060    
061        /**
062         * Holds instances of JmDNS.DNSListener.
063         * Must by a synchronized collection, because it is updated from
064         * concurrent threads.
065         */
066        private List listeners;
067        /**
068         * Holds instances of ServiceListener's.
069         * Keys are Strings holding a fully qualified service type.
070         * Values are LinkedList's of ServiceListener's.
071         */
072        private Map serviceListeners;
073        /**
074         * Holds instances of ServiceTypeListener's.
075         */
076        private List typeListeners;
077    
078    
079        /**
080         * Cache for DNSEntry's.
081         */
082        private DNSCache cache;
083    
084        /**
085         * This hashtable holds the services that have been registered.
086         * Keys are instances of String which hold an all lower-case version of the
087         * fully qualified service name.
088         * Values are instances of ServiceInfo.
089         */
090        Map services;
091    
092        /**
093         * This hashtable holds the service types that have been registered or
094         * that have been received in an incoming datagram.
095         * Keys are instances of String which hold an all lower-case version of the
096         * fully qualified service type.
097         * Values hold the fully qualified service type.
098         */
099        Map serviceTypes;
100        /**
101         * This is the shutdown hook, we registered with the java runtime.
102         */
103        private Thread shutdown;
104    
105        /**
106         * Handle on the local host
107         */
108        HostInfo localHost;
109    
110        private Thread incomingListener = null;
111    
112        /**
113         * Throttle count.
114         * This is used to count the overall number of probes sent by JmDNS.
115         * When the last throttle increment happened .
116         */
117        private int throttle;
118        /**
119         * Last throttle increment.
120         */
121        private long lastThrottleIncrement;
122    
123        /**
124         * The timer is used to dispatch all outgoing messages of JmDNS.
125         * It is also used to dispatch maintenance tasks for the DNS cache.
126         */
127        private Timer timer;
128    
129        /**
130         * The source for random values.
131         * This is used to introduce random delays in responses. This reduces the
132         * potential for collisions on the network.
133         */
134        private final static Random random = new Random();
135    
136        /**
137         * This lock is used to coordinate processing of incoming and outgoing
138         * messages. This is needed, because the Rendezvous Conformance Test
139         * does not forgive race conditions.
140         */
141        private Object ioLock = new Object();
142    
143        /**
144         * If an incoming package which needs an answer is truncated, we store it
145         * here. We add more incoming DNSRecords to it, until the JmDNS.Responder
146         * timer picks it up.
147         * Remind: This does not work well with multiple planned answers for packages
148         * that came in from different clients.
149         */
150        private DNSIncoming plannedAnswer;
151        
152        // State machine
153        /**
154         * The state of JmDNS.
155         * <p/>
156         * For proper handling of concurrency, this variable must be
157         * changed only using methods advanceState(), revertState() and cancel().
158         */
159        private DNSState state = DNSState.PROBING_1;
160    
161        /**
162         * Timer task associated to the host name.
163         * This is used to prevent from having multiple tasks associated to the host
164         * name at the same time.
165         */
166        TimerTask task;
167    
168        /**
169         * This hashtable is used to maintain a list of service types being collected
170         * by this JmDNS instance.
171         * The key of the hashtable is a service type name, the value is an instance
172         * of JmDNS.ServiceCollector.
173         *
174         * @see #list
175         */
176        private HashMap serviceCollectors = new HashMap();
177    
178        /**
179         * Create an instance of JmDNS.
180         */
181        public JmDNS() throws IOException
182        {
183            logger.finer("JmDNS instance created");
184            try
185            {
186                InetAddress addr = InetAddress.getLocalHost();
187                init(addr.isLoopbackAddress() ? null : addr, addr.getHostName()); // [PJYF Oct 14 2004] Why do we disallow the loopback address?
188            }
189            catch (IOException e)
190            {
191                init(null, "computer");
192            }
193        }
194    
195        /**
196         * Create an instance of JmDNS and bind it to a
197         * specific network interface given its IP-address.
198         */
199        public JmDNS(InetAddress addr) throws IOException
200        {
201            try
202            {
203                init(addr, addr.getHostName());
204            }
205            catch (IOException e)
206            {
207                init(null, "computer");
208            }
209        }
210    
211        /**
212         * Initialize everything.
213         *
214         * @param address The interface to which JmDNS binds to.
215         * @param name    The host name of the interface.
216         */
217        private void init(InetAddress address, String name) throws IOException
218        {
219            // A host name with "." is illegal. so strip off everything and append .local.
220            int idx = name.indexOf(".");
221            if (idx > 0)
222            {
223                name = name.substring(0, idx);
224            }
225            name += ".local.";
226            // localHost to IP address binding
227            localHost = new HostInfo(address, name);
228    
229            cache = new DNSCache(100);
230    
231            listeners = Collections.synchronizedList(new ArrayList());
232            serviceListeners = new HashMap();
233            typeListeners = new ArrayList();
234    
235            services = new Hashtable(20);
236            serviceTypes = new Hashtable(20);
237    
238            timer = new Timer("JmDNS.Timer");
239            new RecordReaper().start();
240            shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
241            Runtime.getRuntime().addShutdownHook(shutdown);
242    
243            incomingListener = new Thread(new SocketListener(), "JmDNS.SocketListener");
244    
245            // Bind to multicast socket
246            openMulticastSocket(localHost);
247            start(services.values());
248        }
249    
250        private void start(Collection serviceInfos)
251        {
252            state = DNSState.PROBING_1;
253            incomingListener.start();
254            new Prober().start();
255            for (Iterator iterator = serviceInfos.iterator(); iterator.hasNext();)
256            {
257                try
258                {
259                    registerService(new ServiceInfo((ServiceInfo) iterator.next()));
260                }
261                catch (Exception exception)
262                {
263                    logger.log(Level.WARNING, "start() Registration exception ", exception);
264                }
265            }
266        }
267    
268        private void openMulticastSocket(HostInfo hostInfo) throws IOException
269        {
270            if (group == null)
271            {
272                group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
273            }
274            if (socket != null)
275            {
276                this.closeMulticastSocket();
277            }
278            socket = new MulticastSocket(DNSConstants.MDNS_PORT);
279            if ((hostInfo != null) && (localHost.getInterface() != null))
280            {
281                socket.setNetworkInterface(hostInfo.getInterface());
282            }
283            socket.setTimeToLive(255);
284            socket.joinGroup(group);
285        }
286    
287        private void closeMulticastSocket()
288        {
289            logger.finer("closeMulticastSocket()");
290            if (socket != null)
291            {
292                // close socket
293                try
294                {
295                    socket.leaveGroup(group);
296                    socket.close();
297                    if (incomingListener != null)
298                    {
299                        incomingListener.join();
300                    }
301                }
302                catch (Exception exception)
303                {
304                    logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", exception);
305                }
306                socket = null;
307            }
308        }
309        
310        // State machine
311        /**
312         * Sets the state and notifies all objects that wait on JmDNS.
313         */
314        synchronized void advanceState()
315        {
316            state = state.advance();
317            notifyAll();
318        }
319    
320        /**
321         * Sets the state and notifies all objects that wait on JmDNS.
322         */
323        synchronized void revertState()
324        {
325            state = state.revert();
326            notifyAll();
327        }
328    
329        /**
330         * Sets the state and notifies all objects that wait on JmDNS.
331         */
332        synchronized void cancel()
333        {
334            state = DNSState.CANCELED;
335            notifyAll();
336        }
337    
338        /**
339         * Returns the current state of this info.
340         */
341        DNSState getState()
342        {
343            return state;
344        }
345    
346    
347        /**
348         * Return the DNSCache associated with the cache variable
349         */
350        DNSCache getCache()
351        {
352            return cache;
353        }
354    
355        /**
356         * Return the HostName associated with this JmDNS instance.
357         * Note: May not be the same as what started.  The host name is subject to
358         * negotiation.
359         */
360        public String getHostName()
361        {
362            return localHost.getName();
363        }
364    
365        public HostInfo getLocalHost()
366        {
367            return localHost;
368        }
369    
370        /**
371         * Return the address of the interface to which this instance of JmDNS is
372         * bound.
373         */
374        public InetAddress getInterface() throws IOException
375        {
376            return socket.getInterface();
377        }
378    
379        /**
380         * Get service information. If the information is not cached, the method
381         * will block until updated information is received.
382         * <p/>
383         * Usage note: Do not call this method from the AWT event dispatcher thread.
384         * You will make the user interface unresponsive.
385         *
386         * @param type fully qualified service type, such as <code>_http._tcp.local.</code> .
387         * @param name unqualified service name, such as <code>foobar</code> .
388         * @return null if the service information cannot be obtained
389         */
390        public ServiceInfo getServiceInfo(String type, String name)
391        {
392            return getServiceInfo(type, name, 3 * 1000);
393        }
394    
395        /**
396         * Get service information. If the information is not cached, the method
397         * will block for the given timeout until updated information is received.
398         * <p/>
399         * Usage note: If you call this method from the AWT event dispatcher thread,
400         * use a small timeout, or you will make the user interface unresponsive.
401         *
402         * @param type    full qualified service type, such as <code>_http._tcp.local.</code> .
403         * @param name    unqualified service name, such as <code>foobar</code> .
404         * @param timeout timeout in milliseconds
405         * @return null if the service information cannot be obtained
406         */
407        public ServiceInfo getServiceInfo(String type, String name, int timeout)
408        {
409            ServiceInfo info = new ServiceInfo(type, name);
410            new ServiceInfoResolver(info).start();
411    
412            try
413            {
414                long end = System.currentTimeMillis() + timeout;
415                long delay;
416                synchronized (info)
417                {
418                    while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0)
419                    {
420                        info.wait(delay);
421                    }
422                }
423            }
424            catch (InterruptedException e)
425            {
426                // empty
427            }
428    
429            return (info.hasData()) ? info : null;
430        }
431    
432        /**
433         * Request service information. The information about the service is
434         * requested and the ServiceListener.resolveService method is called as soon
435         * as it is available.
436         * <p/>
437         * Usage note: Do not call this method from the AWT event dispatcher thread.
438         * You will make the user interface unresponsive.
439         *
440         * @param type full qualified service type, such as <code>_http._tcp.local.</code> .
441         * @param name unqualified service name, such as <code>foobar</code> .
442         */
443        public void requestServiceInfo(String type, String name)
444        {
445            requestServiceInfo(type, name, 3 * 1000);
446        }
447    
448        /**
449         * Request service information. The information about the service is requested
450         * and the ServiceListener.resolveService method is called as soon as it is available.
451         *
452         * @param type    full qualified service type, such as <code>_http._tcp.local.</code> .
453         * @param name    unqualified service name, such as <code>foobar</code> .
454         * @param timeout timeout in milliseconds
455         */
456        public void requestServiceInfo(String type, String name, int timeout)
457        {
458            registerServiceType(type);
459            ServiceInfo info = new ServiceInfo(type, name);
460            new ServiceInfoResolver(info).start();
461    
462            try
463            {
464                long end = System.currentTimeMillis() + timeout;
465                long delay;
466                synchronized (info)
467                {
468                    while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0)
469                    {
470                        info.wait(delay);
471                    }
472                }
473            }
474            catch (InterruptedException e)
475            {
476                // empty
477            }
478        }
479    
480        void handleServiceResolved(ServiceInfo info)
481        {
482            List list = (List) serviceListeners.get(info.type.toLowerCase());
483            if (list != null)
484            {
485                ServiceEvent event = new ServiceEvent(this, info.type, info.getName(), info);
486                // Iterate on a copy in case listeners will modify it
487                final ArrayList listCopy = new ArrayList(list);
488                for (Iterator iterator = listCopy.iterator(); iterator.hasNext();)
489                {
490                    ((ServiceListener) iterator.next()).serviceResolved(event);
491                }
492            }
493        }
494    
495        /**
496         * Listen for service types.
497         *
498         * @param listener listener for service types
499         */
500        public void addServiceTypeListener(ServiceTypeListener listener) throws IOException
501        {
502            synchronized (this)
503            {
504                typeListeners.remove(listener);
505                typeListeners.add(listener);
506            }
507    
508            // report cached service types
509            for (Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();)
510            {
511                listener.serviceTypeAdded(new ServiceEvent(this, (String) iterator.next(), null, null));
512            }
513    
514            new TypeResolver().start();
515        }
516    
517        /**
518         * Remove listener for service types.
519         *
520         * @param listener listener for service types
521         */
522        public void removeServiceTypeListener(ServiceTypeListener listener)
523        {
524            synchronized (this)
525            {
526                typeListeners.remove(listener);
527            }
528        }
529    
530        /**
531         * Listen for services of a given type. The type has to be a fully qualified
532         * type name such as <code>_http._tcp.local.</code>.
533         *
534         * @param type     full qualified service type, such as <code>_http._tcp.local.</code>.
535         * @param listener listener for service updates
536         */
537        public void addServiceListener(String type, ServiceListener listener)
538        {
539            String lotype = type.toLowerCase();
540            removeServiceListener(lotype, listener);
541            List list = null;
542            synchronized (this)
543            {
544                list = (List) serviceListeners.get(lotype);
545                if (list == null)
546                {
547                    list = Collections.synchronizedList(new LinkedList());
548                    serviceListeners.put(lotype, list);
549                }
550                list.add(listener);
551            }
552    
553            // report cached service types
554            for (Iterator i = cache.iterator(); i.hasNext();)
555            {
556                for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next())
557                {
558                    DNSRecord rec = (DNSRecord) n.getValue();
559                    if (rec.type == DNSConstants.TYPE_SRV)
560                    {
561                        if (rec.name.endsWith(type))
562                        {
563                            listener.serviceAdded(new ServiceEvent(this, type, toUnqualifiedName(type, rec.name), null));
564                        }
565                    }
566                }
567            }
568            new ServiceResolver(type).start();
569        }
570    
571        /**
572         * Remove listener for services of a given type.
573         *
574         * @param listener listener for service updates
575         */
576        public void removeServiceListener(String type, ServiceListener listener)
577        {
578            type = type.toLowerCase();
579            List list = (List) serviceListeners.get(type);
580            if (list != null)
581            {
582                synchronized (this)
583                {
584                    list.remove(listener);
585                    if (list.size() == 0)
586                    {
587                        serviceListeners.remove(type);
588                    }
589                }
590            }
591        }
592    
593        /**
594         * Register a service. The service is registered for access by other jmdns clients.
595         * The name of the service may be changed to make it unique.
596         */
597        public void registerService(ServiceInfo info) throws IOException
598        {
599            registerServiceType(info.type);
600    
601            // bind the service to this address
602            info.server = localHost.getName();
603            info.addr = localHost.getAddress();
604    
605            synchronized (this)
606            {
607                makeServiceNameUnique(info);
608                services.put(info.getQualifiedName().toLowerCase(), info);
609            }
610    
611            new /*Service*/Prober().start();
612            try
613            {
614                synchronized (info)
615                {
616                    while (info.getState().compareTo(DNSState.ANNOUNCED) < 0)
617                    {
618                        info.wait();
619                    }
620                }
621            }
622            catch (InterruptedException e)
623            {
624                //empty
625            }
626            logger.fine("registerService() JmDNS registered service as " + info);
627        }
628    
629        /**
630         * Unregister a service. The service should have been registered.
631         */
632        public void unregisterService(ServiceInfo info)
633        {
634            synchronized (this)
635            {
636                services.remove(info.getQualifiedName().toLowerCase());
637            }
638            info.cancel();
639    
640            // Note: We use this lock object to synchronize on it.
641            //       Synchronizing on another object (e.g. the ServiceInfo) does
642            //       not make sense, because the sole purpose of the lock is to
643            //       wait until the canceler has finished. If we synchronized on
644            //       the ServiceInfo or on the Canceler, we would block all
645            //       accesses to synchronized methods on that object. This is not
646            //       what we want!
647            Object lock = new Object();
648            new Canceler(info, lock).start();
649    
650            // Remind: We get a deadlock here, if the Canceler does not run!
651            try
652            {
653                synchronized (lock)
654                {
655                    lock.wait();
656                }
657            }
658            catch (InterruptedException e)
659            {
660                // empty
661            }
662        }
663    
664        /**
665         * Unregister all services.
666         */
667        public void unregisterAllServices()
668        {
669            logger.finer("unregisterAllServices()");
670            if (services.size() == 0)
671            {
672                return;
673            }
674    
675            Collection list;
676            synchronized (this)
677            {
678                list = new LinkedList(services.values());
679                services.clear();
680            }
681            for (Iterator iterator = list.iterator(); iterator.hasNext();)
682            {
683                ((ServiceInfo) iterator.next()).cancel();
684            }
685    
686    
687            Object lock = new Object();
688            new Canceler(list, lock).start();
689                  // Remind: We get a livelock here, if the Canceler does not run!
690            try {
691                synchronized (lock) {
692                    if (!closed) {
693                        lock.wait();
694                    }
695                }
696            } catch (InterruptedException e) {
697                // empty
698            }
699    
700    
701        }
702    
703        /**
704         * Register a service type. If this service type was not already known,
705         * all service listeners will be notified of the new service type. Service types
706         * are automatically registered as they are discovered.
707         */
708        public void registerServiceType(String type)
709        {
710            String name = type.toLowerCase();
711            if (serviceTypes.get(name) == null)
712            {
713                if ((type.indexOf("._mdns._udp.") < 0) && !type.endsWith(".in-addr.arpa."))
714                {
715                    Collection list;
716                    synchronized (this)
717                    {
718                        serviceTypes.put(name, type);
719                        list = new LinkedList(typeListeners);
720                    }
721                    for (Iterator iterator = list.iterator(); iterator.hasNext();)
722                    {
723                        ((ServiceTypeListener) iterator.next()).serviceTypeAdded(new ServiceEvent(this, type, null, null));
724                    }
725                }
726            }
727        }
728    
729        /**
730         * Generate a possibly unique name for a host using the information we
731         * have in the cache.
732         *
733         * @return returns true, if the name of the host had to be changed.
734         */
735        private boolean makeHostNameUnique(DNSRecord.Address host)
736        {
737            String originalName = host.getName();
738            long now = System.currentTimeMillis();
739    
740            boolean collision;
741            do
742            {
743                collision = false;
744    
745                // Check for collision in cache
746                for (DNSCache.CacheNode j = cache.find(host.getName().toLowerCase()); j != null; j = j.next())
747                {
748                    DNSRecord a = (DNSRecord) j.getValue();
749                    if (false)
750                    {
751                        host.name = incrementName(host.getName());
752                        collision = true;
753                        break;
754                    }
755                }
756            }
757            while (collision);
758    
759            if (originalName.equals(host.getName()))
760            {
761                return false;
762            }
763            else
764            {
765                return true;
766            }
767        }
768    
769        /**
770         * Generate a possibly unique name for a service using the information we
771         * have in the cache.
772         *
773         * @return returns true, if the name of the service info had to be changed.
774         */
775        private boolean makeServiceNameUnique(ServiceInfo info)
776        {
777            String originalQualifiedName = info.getQualifiedName();
778            long now = System.currentTimeMillis();
779    
780            boolean collision;
781            do
782            {
783                collision = false;
784    
785                // Check for collision in cache
786                for (DNSCache.CacheNode j = cache.find(info.getQualifiedName().toLowerCase()); j != null; j = j.next())
787                {
788                    DNSRecord a = (DNSRecord) j.getValue();
789                    if ((a.type == DNSConstants.TYPE_SRV) && !a.isExpired(now))
790                    {
791                        DNSRecord.Service s = (DNSRecord.Service) a;
792                        if (s.port != info.port || !s.server.equals(localHost.getName()))
793                        {
794                            logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + a + " s.server=" + s.server + " " + localHost.getName() + " equals:" + (s.server.equals(localHost.getName())));
795                            info.setName(incrementName(info.getName()));
796                            collision = true;
797                            break;
798                        }
799                    }
800                }
801    
802                // Check for collision with other service infos published by JmDNS
803                Object selfService = services.get(info.getQualifiedName().toLowerCase());
804                if (selfService != null && selfService != info)
805                {
806                    info.setName(incrementName(info.getName()));
807                    collision = true;
808                }
809            }
810            while (collision);
811    
812            return !(originalQualifiedName.equals(info.getQualifiedName()));
813        }
814    
815        String incrementName(String name)
816        {
817            try
818            {
819                int l = name.lastIndexOf('(');
820                int r = name.lastIndexOf(')');
821                if ((l >= 0) && (l < r))
822                {
823                    name = name.substring(0, l) + "(" + (Integer.parseInt(name.substring(l + 1, r)) + 1) + ")";
824                }
825                else
826                {
827                    name += " (2)";
828                }
829            }
830            catch (NumberFormatException e)
831            {
832                name += " (2)";
833            }
834            return name;
835        }
836    
837        /**
838         * Add a listener for a question. The listener will receive updates
839         * of answers to the question as they arrive, or from the cache if they
840         * are already available.
841         */
842        void addListener(DNSListener listener, DNSQuestion question)
843        {
844            long now = System.currentTimeMillis();
845    
846            // add the new listener
847            synchronized (this)
848            {
849                listeners.add(listener);
850            }
851    
852            // report existing matched records
853            if (question != null)
854            {
855                for (DNSCache.CacheNode i = cache.find(question.name); i != null; i = i.next())
856                {
857                    DNSRecord c = (DNSRecord) i.getValue();
858                    if (question.answeredBy(c) && !c.isExpired(now))
859                    {
860                        listener.updateRecord(this, now, c);
861                    }
862                }
863            }
864        }
865    
866        /**
867         * Remove a listener from all outstanding questions. The listener will no longer
868         * receive any updates.
869         */
870        void removeListener(DNSListener listener)
871        {
872            synchronized (this)
873            {
874                listeners.remove(listener);
875            }
876        }
877        
878        
879        // Remind: Method updateRecord should receive a better name.
880        /**
881         * Notify all listeners that a record was updated.
882         */
883        void updateRecord(long now, DNSRecord rec)
884        {
885            // We do not want to block the entire DNS while we are updating the record for each listener (service info)
886            List listenerList = null;
887            synchronized (this)
888            {
889                listenerList = new ArrayList(listeners);
890            }
891            for (Iterator iterator = listenerList.iterator(); iterator.hasNext();)
892            {
893                DNSListener listener = (DNSListener) iterator.next();
894                listener.updateRecord(this, now, rec);
895            }
896            if (rec.type == DNSConstants.TYPE_PTR || rec.type == DNSConstants.TYPE_SRV)
897            {
898                List serviceListenerList = null;
899                synchronized (this)
900                {
901                    serviceListenerList = (List) serviceListeners.get(rec.name.toLowerCase());
902                    // Iterate on a copy in case listeners will modify it
903                    if (serviceListenerList != null)
904                    {
905                        serviceListenerList = new ArrayList(serviceListenerList);
906                    }
907                }
908                if (serviceListenerList != null)
909                {
910                    boolean expired = rec.isExpired(now);
911                    String type = rec.getName();
912                    String name = ((DNSRecord.Pointer) rec).getAlias();
913                    // DNSRecord old = (DNSRecord)services.get(name.toLowerCase());
914                    if (!expired)
915                    {
916                        // new record
917                        ServiceEvent event = new ServiceEvent(this, type, toUnqualifiedName(type, name), null);
918                        for (Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();)
919                        {
920                            ((ServiceListener) iterator.next()).serviceAdded(event);
921                        }
922                    }
923                    else
924                    {
925                        // expire record
926                        ServiceEvent event = new ServiceEvent(this, type, toUnqualifiedName(type, name), null);
927                        for (Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();)
928                        {
929                            ((ServiceListener) iterator.next()).serviceRemoved(event);
930                        }
931                    }
932                }
933            }
934        }
935    
936        /**
937         * Handle an incoming response. Cache answers, and pass them on to
938         * the appropriate questions.
939         */
940        private void handleResponse(DNSIncoming msg) throws IOException
941        {
942            long now = System.currentTimeMillis();
943    
944            boolean hostConflictDetected = false;
945            boolean serviceConflictDetected = false;
946    
947            for (Iterator i = msg.answers.iterator(); i.hasNext();)
948            {
949                boolean isInformative = false;
950                DNSRecord rec = (DNSRecord) i.next();
951                boolean expired = rec.isExpired(now);
952    
953                // update the cache
954                DNSRecord c = (DNSRecord) cache.get(rec);
955                if (c != null)
956                {
957                    if (expired)
958                    {
959                        isInformative = true;
960                        cache.remove(c);
961                    }
962                    else
963                    {
964                        c.resetTTL(rec);
965                        rec = c;
966                    }
967                }
968                else
969                {
970                    if (!expired)
971                    {
972                        isInformative = true;
973                        cache.add(rec);
974                    }
975                }
976                switch (rec.type)
977                {
978                    case DNSConstants.TYPE_PTR:
979                        // handle _mdns._udp records
980                        if (rec.getName().indexOf("._mdns._udp.") >= 0)
981                        {
982                            if (!expired && rec.name.startsWith("_services._mdns._udp."))
983                            {
984                                isInformative = true;
985                                registerServiceType(((DNSRecord.Pointer) rec).alias);
986                            }
987                            continue;
988                        }
989                        registerServiceType(rec.name);
990                        break;
991                }
992    
993                if ((rec.getType() == DNSConstants.TYPE_A) || (rec.getType() == DNSConstants.TYPE_AAAA))
994                {
995                    hostConflictDetected |= rec.handleResponse(this);
996                }
997                else
998                {
999                    serviceConflictDetected |= rec.handleResponse(this);
1000                }
1001    
1002                // notify the listeners
1003                if (isInformative)
1004                {
1005                    updateRecord(now, rec);
1006                }
1007            }
1008    
1009            if (hostConflictDetected || serviceConflictDetected)
1010            {
1011                new Prober().start();
1012            }
1013        }
1014    
1015        /**
1016         * Handle an incoming query. See if we can answer any part of it
1017         * given our service infos.
1018         */
1019        private void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException
1020        {
1021            // Track known answers
1022            boolean hostConflictDetected = false;
1023            boolean serviceConflictDetected = false;
1024            long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
1025            for (Iterator i = in.answers.iterator(); i.hasNext();)
1026            {
1027                DNSRecord answer = (DNSRecord) i.next();
1028                if ((answer.getType() == DNSConstants.TYPE_A) || (answer.getType() == DNSConstants.TYPE_AAAA))
1029                {
1030                    hostConflictDetected |= answer.handleQuery(this, expirationTime);
1031                }
1032                else
1033                {
1034                    serviceConflictDetected |= answer.handleQuery(this, expirationTime);
1035                }
1036            }
1037    
1038            if (plannedAnswer != null)
1039            {
1040                plannedAnswer.append(in);
1041            }
1042            else
1043            {
1044                if (in.isTruncated())
1045                {
1046                    plannedAnswer = in;
1047                }
1048    
1049                new Responder(in, addr, port).start();
1050            }
1051    
1052            if (hostConflictDetected || serviceConflictDetected)
1053            {
1054                new Prober().start();
1055            }
1056        }
1057    
1058        /**
1059         * Add an answer to a question. Deal with the case when the
1060         * outgoing packet overflows
1061         */
1062        DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException
1063        {
1064            if (out == null)
1065            {
1066                out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1067            }
1068            try
1069            {
1070                out.addAnswer(in, rec);
1071            }
1072            catch (IOException e)
1073            {
1074                out.flags |= DNSConstants.FLAGS_TC;
1075                out.id = in.id;
1076                out.finish();
1077                send(out);
1078    
1079                out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1080                out.addAnswer(in, rec);
1081            }
1082            return out;
1083        }
1084    
1085    
1086        /**
1087         * Send an outgoing multicast DNS message.
1088         */
1089        private void send(DNSOutgoing out) throws IOException
1090        {
1091            out.finish();
1092            if (!out.isEmpty())
1093            {
1094                DatagramPacket packet = new DatagramPacket(out.data, out.off, group, DNSConstants.MDNS_PORT);
1095    
1096                try
1097                {
1098                    DNSIncoming msg = new DNSIncoming(packet);
1099                    logger.finest("send() JmDNS out:" + msg.print(true));
1100                }
1101                catch (IOException e)
1102                {
1103                    logger.throwing(getClass().toString(), "send(DNSOutgoing) - JmDNS can not parse what it sends!!!", e);
1104                }
1105                socket.send(packet);
1106            }
1107        }
1108    
1109        /**
1110         * Listen for multicast packets.
1111         */
1112        class SocketListener implements Runnable
1113        {
1114            public void run()
1115            {
1116                try
1117                {
1118                    byte buf[] = new byte[DNSConstants.MAX_MSG_ABSOLUTE];
1119                    DatagramPacket packet = new DatagramPacket(buf, buf.length);
1120                    while (state != DNSState.CANCELED)
1121                    {
1122                        packet.setLength(buf.length);
1123                        socket.receive(packet);
1124                        if (state == DNSState.CANCELED)
1125                        {
1126                            break;
1127                        }
1128                        try
1129                        {
1130                            if (localHost.shouldIgnorePacket(packet))
1131                            {
1132                                continue;
1133                            }
1134    
1135                            DNSIncoming msg = new DNSIncoming(packet);
1136                            logger.finest("SocketListener.run() JmDNS in:" + msg.print(true));
1137    
1138                            synchronized (ioLock)
1139                            {
1140                                if (msg.isQuery())
1141                                {
1142                                    if (packet.getPort() != DNSConstants.MDNS_PORT)
1143                                    {
1144                                        handleQuery(msg, packet.getAddress(), packet.getPort());
1145                                    }
1146                                    handleQuery(msg, group, DNSConstants.MDNS_PORT);
1147                                }
1148                                else
1149                                {
1150                                    handleResponse(msg);
1151                                }
1152                            }
1153                        }
1154                        catch (IOException e)
1155                        {
1156                            logger.log(Level.WARNING, "run() exception ", e);
1157                        }
1158                    }
1159                }
1160                catch (IOException e)
1161                {
1162                    if (state != DNSState.CANCELED)
1163                    {
1164                        logger.log(Level.WARNING, "run() exception ", e);
1165                        recover();
1166                    }
1167                }
1168            }
1169        }
1170    
1171    
1172        /**
1173         * Periodicaly removes expired entries from the cache.
1174         */
1175        private class RecordReaper extends TimerTask
1176        {
1177            public void start()
1178            {
1179                timer.schedule(this, DNSConstants.RECORD_REAPER_INTERVAL, DNSConstants.RECORD_REAPER_INTERVAL);
1180            }
1181    
1182            public void run()
1183            {
1184                synchronized (JmDNS.this)
1185                {
1186                    if (state == DNSState.CANCELED)
1187                    {
1188                        return;
1189                    }
1190                    logger.finest("run() JmDNS reaping cache");
1191    
1192                    // Remove expired answers from the cache
1193                    // -------------------------------------
1194                    // To prevent race conditions, we defensively copy all cache
1195                    // entries into a list.
1196                    List list = new ArrayList();
1197                    synchronized (cache)
1198                    {
1199                        for (Iterator i = cache.iterator(); i.hasNext();)
1200                        {
1201                            for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next())
1202                            {
1203                                list.add(n.getValue());
1204                            }
1205                        }
1206                    }
1207                    // Now, we remove them.
1208                    long now = System.currentTimeMillis();
1209                    for (Iterator i = list.iterator(); i.hasNext();)
1210                    {
1211                        DNSRecord c = (DNSRecord) i.next();
1212                        if (c.isExpired(now))
1213                        {
1214                            updateRecord(now, c);
1215                            cache.remove(c);
1216                        }
1217                    }
1218                }
1219            }
1220        }
1221    
1222    
1223        /**
1224         * The Prober sends three consecutive probes for all service infos
1225         * that needs probing as well as for the host name.
1226         * The state of each service info of the host name is advanced, when a probe has
1227         * been sent for it.
1228         * When the prober has run three times, it launches an Announcer.
1229         * <p/>
1230         * If a conflict during probes occurs, the affected service infos (and affected
1231         * host name) are taken away from the prober. This eventually causes the prober
1232         * tho cancel itself.
1233         */
1234        private class Prober extends TimerTask
1235        {
1236            /**
1237             * The state of the prober.
1238             */
1239            DNSState taskState = DNSState.PROBING_1;
1240    
1241            public Prober()
1242            {
1243                // Associate the host name to this, if it needs probing
1244                if (state == DNSState.PROBING_1)
1245                {
1246                    task = this;
1247                }
1248                // Associate services to this, if they need probing
1249                synchronized (JmDNS.this)
1250                {
1251                    for (Iterator iterator = services.values().iterator(); iterator.hasNext();)
1252                    {
1253                        ServiceInfo info = (ServiceInfo) iterator.next();
1254                        if (info.getState() == DNSState.PROBING_1)
1255                        {
1256                            info.task = this;
1257                        }
1258                    }
1259                }
1260            }
1261    
1262    
1263            public void start()
1264            {
1265                long now = System.currentTimeMillis();
1266                if (now - lastThrottleIncrement < DNSConstants.PROBE_THROTTLE_COUNT_INTERVAL)
1267                {
1268                    throttle++;
1269                }
1270                else
1271                {
1272                    throttle = 1;
1273                }
1274                lastThrottleIncrement = now;
1275    
1276                if (state == DNSState.ANNOUNCED && throttle < DNSConstants.PROBE_THROTTLE_COUNT)
1277                {
1278                    timer.schedule(this, random.nextInt(1 + DNSConstants.PROBE_WAIT_INTERVAL), DNSConstants.PROBE_WAIT_INTERVAL);
1279                }
1280                else
1281                {
1282                    timer.schedule(this, DNSConstants.PROBE_CONFLICT_INTERVAL, DNSConstants.PROBE_CONFLICT_INTERVAL);
1283                }
1284            }
1285    
1286            public boolean cancel()
1287            {
1288                // Remove association from host name to this
1289                if (task == this)
1290                {
1291                    task = null;
1292                }
1293    
1294                // Remove associations from services to this
1295                synchronized (JmDNS.this)
1296                {
1297                    for (Iterator i = services.values().iterator(); i.hasNext();)
1298                    {
1299                        ServiceInfo info = (ServiceInfo) i.next();
1300                        if (info.task == this)
1301                        {
1302                            info.task = null;
1303                        }
1304                    }
1305                }
1306    
1307                return super.cancel();
1308            }
1309    
1310            public void run()
1311            {
1312                synchronized (ioLock)
1313                {
1314                    DNSOutgoing out = null;
1315                    try
1316                    {
1317                        // send probes for JmDNS itself
1318                        if (state == taskState && task == this)
1319                        {
1320                            if (out == null)
1321                            {
1322                                out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
1323                            }
1324                            out.addQuestion(new DNSQuestion(localHost.getName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
1325                            DNSRecord answer = localHost.getDNS4AddressRecord();
1326                            if (answer != null)
1327                            {
1328                                out.addAuthorativeAnswer(answer);
1329                            }
1330                            answer = localHost.getDNS6AddressRecord();
1331                            if (answer != null)
1332                            {
1333                                out.addAuthorativeAnswer(answer);
1334                            }
1335                            advanceState();
1336                        }
1337                        // send probes for services
1338                        // Defensively copy the services into a local list,
1339                        // to prevent race conditions with methods registerService
1340                        // and unregisterService.
1341                        List list;
1342                        synchronized (JmDNS.this)
1343                        {
1344                            list = new LinkedList(services.values());
1345                        }
1346                        for (Iterator i = list.iterator(); i.hasNext();)
1347                        {
1348                            ServiceInfo info = (ServiceInfo) i.next();
1349    
1350                            synchronized (info)
1351                            {
1352                                if (info.getState() == taskState && info.task == this)
1353                                {
1354                                    info.advanceState();
1355                                    logger.fine("run() JmDNS probing " + info.getQualifiedName() + " state " + info.getState());
1356                                    if (out == null)
1357                                    {
1358                                        out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
1359                                        out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
1360                                    }
1361                                    out.addAuthorativeAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()));
1362                                }
1363                            }
1364                        }
1365                        if (out != null)
1366                        {
1367                            logger.finer("run() JmDNS probing #" + taskState);
1368                            send(out);
1369                        }
1370                        else
1371                        {
1372                            // If we have nothing to send, another timer taskState ahead
1373                            // of us has done the job for us. We can cancel.
1374                            cancel();
1375                            return;
1376                        }
1377                    }
1378                    catch (Throwable e)
1379                    {
1380                        logger.log(Level.WARNING, "run() exception ", e);
1381                        recover();
1382                    }
1383    
1384                    taskState = taskState.advance();
1385                    if (!taskState.isProbing())
1386                    {
1387                        cancel();
1388    
1389                        new Announcer().start();
1390                    }
1391                }
1392            }
1393    
1394        }
1395    
1396        /**
1397         * The Announcer sends an accumulated query of all announces, and advances
1398         * the state of all serviceInfos, for which it has sent an announce.
1399         * The Announcer also sends announcements and advances the state of JmDNS itself.
1400         * <p/>
1401         * When the announcer has run two times, it finishes.
1402         */
1403        private class Announcer extends TimerTask
1404        {
1405            /**
1406             * The state of the announcer.
1407             */
1408            DNSState taskState = DNSState.ANNOUNCING_1;
1409    
1410            public Announcer()
1411            {
1412                // Associate host to this, if it needs announcing
1413                if (state == DNSState.ANNOUNCING_1)
1414                {
1415                    task = this;
1416                }
1417                // Associate services to this, if they need announcing
1418                synchronized (JmDNS.this)
1419                {
1420                    for (Iterator s = services.values().iterator(); s.hasNext();)
1421                    {
1422                        ServiceInfo info = (ServiceInfo) s.next();
1423                        if (info.getState() == DNSState.ANNOUNCING_1)
1424                        {
1425                            info.task = this;
1426                        }
1427                    }
1428                }
1429            }
1430    
1431            public void start()
1432            {
1433                timer.schedule(this, DNSConstants.ANNOUNCE_WAIT_INTERVAL, DNSConstants.ANNOUNCE_WAIT_INTERVAL);
1434            }
1435    
1436            public boolean cancel()
1437            {
1438                // Remove association from host to this
1439                if (task == this)
1440                {
1441                    task = null;
1442                }
1443    
1444                // Remove associations from services to this
1445                synchronized (JmDNS.this)
1446                {
1447                    for (Iterator i = services.values().iterator(); i.hasNext();)
1448                    {
1449                        ServiceInfo info = (ServiceInfo) i.next();
1450                        if (info.task == this)
1451                        {
1452                            info.task = null;
1453                        }
1454                    }
1455                }
1456    
1457                return super.cancel();
1458            }
1459    
1460            public void run()
1461            {
1462                DNSOutgoing out = null;
1463                try
1464                {
1465                    // send probes for JmDNS itself
1466                    if (state == taskState)
1467                    {
1468                        if (out == null)
1469                        {
1470                            out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1471                        }
1472                        DNSRecord answer = localHost.getDNS4AddressRecord();
1473                        if (answer != null)
1474                        {
1475                            out.addAnswer(answer, 0);
1476                        }
1477                        answer = localHost.getDNS6AddressRecord();
1478                        if (answer != null)
1479                        {
1480                            out.addAnswer(answer, 0);
1481                        }
1482                        advanceState();
1483                    }
1484                    // send announces for services
1485                    // Defensively copy the services into a local list,
1486                    // to prevent race conditions with methods registerService
1487                    // and unregisterService.
1488                    List list;
1489                    synchronized (JmDNS.this)
1490                    {
1491                        list = new ArrayList(services.values());
1492                    }
1493                    for (Iterator i = list.iterator(); i.hasNext();)
1494                    {
1495                        ServiceInfo info = (ServiceInfo) i.next();
1496                        synchronized (info)
1497                        {
1498                            if (info.getState() == taskState && info.task == this)
1499                            {
1500                                info.advanceState();
1501                                logger.finer("run() JmDNS announcing " + info.getQualifiedName() + " state " + info.getState());
1502                                if (out == null)
1503                                {
1504                                    out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1505                                }
1506                                out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), 0);
1507                                out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()), 0);
1508                                out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0);
1509                            }
1510                        }
1511                    }
1512                    if (out != null)
1513                    {
1514                        logger.finer("run() JmDNS announcing #" + taskState);
1515                        send(out);
1516                    }
1517                    else
1518                    {
1519                        // If we have nothing to send, another timer taskState ahead
1520                        // of us has done the job for us. We can cancel.
1521                        cancel();
1522                    }
1523                }
1524                catch (Throwable e)
1525                {
1526                    logger.log(Level.WARNING, "run() exception ", e);
1527                    recover();
1528                }
1529    
1530                taskState = taskState.advance();
1531                if (!taskState.isAnnouncing())
1532                {
1533                    cancel();
1534    
1535                    new Renewer().start();
1536                }
1537            }
1538        }
1539    
1540        /**
1541         * The Renewer is there to send renewal announcment when the record expire for ours infos.
1542         */
1543        private class Renewer extends TimerTask
1544        {
1545            /**
1546             * The state of the announcer.
1547             */
1548            DNSState taskState = DNSState.ANNOUNCED;
1549    
1550            public Renewer()
1551            {
1552                // Associate host to this, if it needs renewal
1553                if (state == DNSState.ANNOUNCED)
1554                {
1555                    task = this;
1556                }
1557                // Associate services to this, if they need renewal
1558                synchronized (JmDNS.this)
1559                {
1560                    for (Iterator s = services.values().iterator(); s.hasNext();)
1561                    {
1562                        ServiceInfo info = (ServiceInfo) s.next();
1563                        if (info.getState() == DNSState.ANNOUNCED)
1564                        {
1565                            info.task = this;
1566                        }
1567                    }
1568                }
1569            }
1570    
1571            public void start()
1572            {
1573                timer.schedule(this, DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL, DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL);
1574            }
1575    
1576            public boolean cancel()
1577            {
1578                // Remove association from host to this
1579                if (task == this)
1580                {
1581                    task = null;
1582                }
1583    
1584                // Remove associations from services to this
1585                synchronized (JmDNS.this)
1586                {
1587                    for (Iterator i = services.values().iterator(); i.hasNext();)
1588                    {
1589                        ServiceInfo info = (ServiceInfo) i.next();
1590                        if (info.task == this)
1591                        {
1592                            info.task = null;
1593                        }
1594                    }
1595                }
1596    
1597                return super.cancel();
1598            }
1599    
1600            public void run()
1601            {
1602                DNSOutgoing out = null;
1603                try
1604                {
1605                    // send probes for JmDNS itself
1606                    if (state == taskState)
1607                    {
1608                        if (out == null)
1609                        {
1610                            out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1611                        }
1612                        DNSRecord answer = localHost.getDNS4AddressRecord();
1613                        if (answer != null)
1614                        {
1615                            out.addAnswer(answer, 0);
1616                        }
1617                        answer = localHost.getDNS6AddressRecord();
1618                        if (answer != null)
1619                        {
1620                            out.addAnswer(answer, 0);
1621                        }
1622                        advanceState();
1623                    }
1624                    // send announces for services
1625                    // Defensively copy the services into a local list,
1626                    // to prevent race conditions with methods registerService
1627                    // and unregisterService.
1628                    List list;
1629                    synchronized (JmDNS.this)
1630                    {
1631                        list = new ArrayList(services.values());
1632                    }
1633                    for (Iterator i = list.iterator(); i.hasNext();)
1634                    {
1635                        ServiceInfo info = (ServiceInfo) i.next();
1636                        synchronized (info)
1637                        {
1638                            if (info.getState() == taskState && info.task == this)
1639                            {
1640                                info.advanceState();
1641                                logger.finer("run() JmDNS announced " + info.getQualifiedName() + " state " + info.getState());
1642                                if (out == null)
1643                                {
1644                                    out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
1645                                }
1646                                out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), 0);
1647                                out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()), 0);
1648                                out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0);
1649                            }
1650                        }
1651                    }
1652                    if (out != null)
1653                    {
1654                        logger.finer("run() JmDNS announced");
1655                        send(out);
1656                    }
1657                    else
1658                    {
1659                        // If we have nothing to send, another timer taskState ahead
1660                        // of us has done the job for us. We can cancel.
1661                        cancel();
1662                    }
1663                }
1664                catch (Throwable e)
1665                {
1666                    logger.log(Level.WARNING, "run() exception ", e);
1667                    recover();
1668                }
1669    
1670                taskState = taskState.advance();
1671                if (!taskState.isAnnounced())
1672                {
1673                    cancel();
1674    
1675                }
1676            }
1677        }
1678    
1679        /**
1680         * The Responder sends a single answer for the specified service infos
1681         * and for the host name.
1682         */
1683        private class Responder extends TimerTask
1684        {
1685            private DNSIncoming in;
1686            private InetAddress addr;
1687            private int port;
1688    
1689            public Responder(DNSIncoming in, InetAddress addr, int port)
1690            {
1691                this.in = in;
1692                this.addr = addr;
1693                this.port = port;
1694            }
1695    
1696            public void start()
1697            {
1698                // According to draft-cheshire-dnsext-multicastdns.txt
1699                // chapter "8 Responding":
1700                // We respond immediately if we know for sure, that we are
1701                // the only one who can respond to the query.
1702                // In all other cases, we respond within 20-120 ms.
1703                //
1704                // According to draft-cheshire-dnsext-multicastdns.txt
1705                // chapter "7.2 Multi-Packet Known Answer Suppression":
1706                // We respond after 20-120 ms if the query is truncated.
1707    
1708                boolean iAmTheOnlyOne = true;
1709                for (Iterator i = in.questions.iterator(); i.hasNext();)
1710                {
1711                    DNSEntry entry = (DNSEntry) i.next();
1712                    if (entry instanceof DNSQuestion)
1713                    {
1714                        DNSQuestion q = (DNSQuestion) entry;
1715                        logger.finest("start() question=" + q);
1716                        iAmTheOnlyOne &= (q.type == DNSConstants.TYPE_SRV
1717                            || q.type == DNSConstants.TYPE_TXT
1718                            || q.type == DNSConstants.TYPE_A
1719                            || q.type == DNSConstants.TYPE_AAAA
1720                            || localHost.getName().equalsIgnoreCase(q.name)
1721                            || services.containsKey(q.name.toLowerCase()));
1722                        if (!iAmTheOnlyOne)
1723                        {
1724                            break;
1725                        }
1726                    }
1727                }
1728                int delay = (iAmTheOnlyOne && !in.isTruncated()) ? 0 : DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + random.nextInt(DNSConstants.RESPONSE_MAX_WAIT_INTERVAL - DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + 1) - in.elapseSinceArrival();
1729                if (delay < 0)
1730                {
1731                    delay = 0;
1732                }
1733                logger.finest("start() Responder chosen delay=" + delay);
1734                timer.schedule(this, delay);
1735            }
1736    
1737            public void run()
1738            {
1739                synchronized (ioLock)
1740                {
1741                    if (plannedAnswer == in)
1742                    {
1743                        plannedAnswer = null;
1744                    }
1745    
1746                    // We use these sets to prevent duplicate records
1747                    // FIXME - This should be moved into DNSOutgoing
1748                    HashSet questions = new HashSet();
1749                    HashSet answers = new HashSet();
1750    
1751    
1752                    if (state == DNSState.ANNOUNCED)
1753                    {
1754                        try
1755                        {
1756                            long now = System.currentTimeMillis();
1757                            long expirationTime = now + 1; //=now+DNSConstants.KNOWN_ANSWER_TTL;
1758                            boolean isUnicast = (port != DNSConstants.MDNS_PORT);
1759    
1760    
1761                            // Answer questions
1762                            for (Iterator iterator = in.questions.iterator(); iterator.hasNext();)
1763                            {
1764                                DNSEntry entry = (DNSEntry) iterator.next();
1765                                if (entry instanceof DNSQuestion)
1766                                {
1767                                    DNSQuestion q = (DNSQuestion) entry;
1768    
1769                                    // for unicast responses the question must be included
1770                                    if (isUnicast)
1771                                    {
1772                                        //out.addQuestion(q);
1773                                        questions.add(q);
1774                                    }
1775    
1776                                    int type = q.type;
1777                                    if (type == DNSConstants.TYPE_ANY || type == DNSConstants.TYPE_SRV)
1778                                    { // I ama not sure of why there is a special case here [PJYF Oct 15 2004]
1779                                        if (localHost.getName().equalsIgnoreCase(q.getName()))
1780                                        {
1781                                            // type = DNSConstants.TYPE_A;
1782                                            DNSRecord answer = localHost.getDNS4AddressRecord();
1783                                            if (answer != null)
1784                                            {
1785                                                answers.add(answer);
1786                                            }
1787                                            answer = localHost.getDNS6AddressRecord();
1788                                            if (answer != null)
1789                                            {
1790                                                answers.add(answer);
1791                                            }
1792                                            type = DNSConstants.TYPE_IGNORE;
1793                                        }
1794                                        else
1795                                        {
1796                                            if (serviceTypes.containsKey(q.getName().toLowerCase()))
1797                                            {
1798                                                type = DNSConstants.TYPE_PTR;
1799                                            }
1800                                        }
1801                                    }
1802    
1803                                    switch (type)
1804                                    {
1805                                        case DNSConstants.TYPE_A:
1806                                            {
1807                                                // Answer a query for a domain name
1808                                                //out = addAnswer( in, addr, port, out, host );
1809                                                DNSRecord answer = localHost.getDNS4AddressRecord();
1810                                                if (answer != null)
1811                                                {
1812                                                    answers.add(answer);
1813                                                }
1814                                                break;
1815                                            }
1816                                        case DNSConstants.TYPE_AAAA:
1817                                            {
1818                                                // Answer a query for a domain name
1819                                                DNSRecord answer = localHost.getDNS6AddressRecord();
1820                                                if (answer != null)
1821                                                {
1822                                                    answers.add(answer);
1823                                                }
1824                                                break;
1825                                            }
1826                                        case DNSConstants.TYPE_PTR:
1827                                            {
1828                                                // Answer a query for services of a given type
1829    
1830                                                // find matching services
1831                                                for (Iterator serviceIterator = services.values().iterator(); serviceIterator.hasNext();)
1832                                                {
1833                                                    ServiceInfo info = (ServiceInfo) serviceIterator.next();
1834                                                    if (info.getState() == DNSState.ANNOUNCED)
1835                                                    {
1836                                                        if (q.name.equalsIgnoreCase(info.type))
1837                                                        {
1838                                                            DNSRecord answer = localHost.getDNS4AddressRecord();
1839                                                            if (answer != null)
1840                                                            {
1841                                                                answers.add(answer);
1842                                                            }
1843                                                            answer = localHost.getDNS6AddressRecord();
1844                                                            if (answer != null)
1845                                                            {
1846                                                                answers.add(answer);
1847                                                            }
1848                                                            answers.add(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()));
1849                                                            answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()));
1850                                                            answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.text));
1851                                                        }
1852                                                    }
1853                                                }
1854                                                if (q.name.equalsIgnoreCase("_services._mdns._udp.local."))
1855                                                {
1856                                                    for (Iterator serviceTypeIterator = serviceTypes.values().iterator(); serviceTypeIterator.hasNext();)
1857                                                    {
1858                                                        answers.add(new DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) serviceTypeIterator.next()));
1859                                                    }
1860                                                }
1861                                                break;
1862                                            }
1863                                        case DNSConstants.TYPE_SRV:
1864                                        case DNSConstants.TYPE_ANY:
1865                                        case DNSConstants.TYPE_TXT:
1866                                            {
1867                                                ServiceInfo info = (ServiceInfo) services.get(q.name.toLowerCase());
1868                                                if (info != null && info.getState() == DNSState.ANNOUNCED)
1869                                                {
1870                                                    DNSRecord answer = localHost.getDNS4AddressRecord();
1871                                                    if (answer != null)
1872                                                    {
1873                                                        answers.add(answer);
1874                                                    }
1875                                                    answer = localHost.getDNS6AddressRecord();
1876                                                    if (answer != null)
1877                                                    {
1878                                                        answers.add(answer);
1879                                                    }
1880                                                    answers.add(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()));
1881                                                    answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()));
1882                                                    answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.text));
1883                                                }
1884                                                break;
1885                                            }
1886                                        default :
1887                                            {
1888                                                //System.out.println("JmDNSResponder.unhandled query:"+q);
1889                                                break;
1890                                            }
1891                                    }
1892                                }
1893                            }
1894    
1895    
1896                            // remove known answers, if the ttl is at least half of
1897                            // the correct value. (See Draft Cheshire chapter 7.1.).
1898                            for (Iterator i = in.answers.iterator(); i.hasNext();)
1899                            {
1900                                DNSRecord knownAnswer = (DNSRecord) i.next();
1901                                if (knownAnswer.ttl > DNSConstants.DNS_TTL / 2 && answers.remove(knownAnswer))
1902                                {
1903                                    logger.log(Level.FINER, "JmDNS Responder Known Answer Removed");
1904                                }
1905                            }
1906    
1907    
1908                            // responde if we have answers
1909                            if (answers.size() != 0)
1910                            {
1911                                logger.finer("run() JmDNS responding");
1912                                DNSOutgoing out = null;
1913                                if (isUnicast)
1914                                {
1915                                    out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false);
1916                                }
1917    
1918                                for (Iterator i = questions.iterator(); i.hasNext();)
1919                                {
1920                                    out.addQuestion((DNSQuestion) i.next());
1921                                }
1922                                for (Iterator i = answers.iterator(); i.hasNext();)
1923                                {
1924                                    out = addAnswer(in, addr, port, out, (DNSRecord) i.next());
1925                                }
1926                                send(out);
1927                            }
1928                            cancel();
1929                        }
1930                        catch (Throwable e)
1931                        {
1932                            logger.log(Level.WARNING, "run() exception ", e);
1933                            close();
1934                        }
1935                    }
1936                }
1937            }
1938        }
1939    
1940        /**
1941         * Helper class to resolve service types.
1942         * <p/>
1943         * The TypeResolver queries three times consecutively for service types, and then
1944         * removes itself from the timer.
1945         * <p/>
1946         * The TypeResolver will run only if JmDNS is in state ANNOUNCED.
1947         */
1948        private class TypeResolver extends TimerTask
1949        {
1950            public void start()
1951            {
1952                timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL);
1953            }
1954    
1955            /**
1956             * Counts the number of queries that were sent.
1957             */
1958            int count = 0;
1959    
1960            public void run()
1961            {
1962                try
1963                {
1964                    if (state == DNSState.ANNOUNCED)
1965                    {
1966                        if (++count < 3)
1967                        {
1968                            logger.finer("run() JmDNS querying type");
1969                            DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
1970                            out.addQuestion(new DNSQuestion("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN));
1971                            for (Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();)
1972                            {
1973                                out.addAnswer(new DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) iterator.next()), 0);
1974                            }
1975                            send(out);
1976                        }
1977                        else
1978                        {
1979                            // After three queries, we can quit.
1980                            cancel();
1981                        }
1982                        ;
1983                    }
1984                    else
1985                    {
1986                        if (state == DNSState.CANCELED)
1987                        {
1988                            cancel();
1989                        }
1990                    }
1991                }
1992                catch (Throwable e)
1993                {
1994                    logger.log(Level.WARNING, "run() exception ", e);
1995                    recover();
1996                }
1997            }
1998        }
1999    
2000        /**
2001         * The ServiceResolver queries three times consecutively for services of
2002         * a given type, and then removes itself from the timer.
2003         * <p/>
2004         * The ServiceResolver will run only if JmDNS is in state ANNOUNCED.
2005         * REMIND: Prevent having multiple service resolvers for the same type in the
2006         * timer queue.
2007         */
2008        private class ServiceResolver extends TimerTask
2009        {
2010            /**
2011             * Counts the number of queries being sent.
2012             */
2013            int count = 0;
2014            private String type;
2015    
2016            public ServiceResolver(String type)
2017            {
2018                this.type = type;
2019            }
2020    
2021            public void start()
2022            {
2023                timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL);
2024            }
2025    
2026            public void run()
2027            {
2028                try
2029                {
2030                    if (state == DNSState.ANNOUNCED)
2031                    {
2032                        if (count++ < 3)
2033                        {
2034                            logger.finer("run() JmDNS querying service");
2035                            long now = System.currentTimeMillis();
2036                            DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
2037                            out.addQuestion(new DNSQuestion(type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN));
2038                            for (Iterator s = services.values().iterator(); s.hasNext();)
2039                            {
2040                                final ServiceInfo info = (ServiceInfo) s.next();
2041                                try
2042                                {
2043                                    out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), now);
2044                                }
2045                                catch (IOException ee)
2046                                {
2047                                    break;
2048                                }
2049                            }
2050                            send(out);
2051                        }
2052                        else
2053                        {
2054                            // After three queries, we can quit.
2055                            cancel();
2056                        }
2057                        ;
2058                    }
2059                    else
2060                    {
2061                        if (state == DNSState.CANCELED)
2062                        {
2063                            cancel();
2064                        }
2065                    }
2066                }
2067                catch (Throwable e)
2068                {
2069                    logger.log(Level.WARNING, "run() exception ", e);
2070                    recover();
2071                }
2072            }
2073        }
2074    
2075        /**
2076         * The ServiceInfoResolver queries up to three times consecutively for
2077         * a service info, and then removes itself from the timer.
2078         * <p/>
2079         * The ServiceInfoResolver will run only if JmDNS is in state ANNOUNCED.
2080         * REMIND: Prevent having multiple service resolvers for the same info in the
2081         * timer queue.
2082         */
2083        private class ServiceInfoResolver extends TimerTask
2084        {
2085            /**
2086             * Counts the number of queries being sent.
2087             */
2088            int count = 0;
2089            private ServiceInfo info;
2090    
2091            public ServiceInfoResolver(ServiceInfo info)
2092            {
2093                this.info = info;
2094                info.dns = JmDNS.this;
2095                addListener(info, new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
2096            }
2097    
2098            public void start()
2099            {
2100                timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL);
2101            }
2102    
2103            public void run()
2104            {
2105                try
2106                {
2107                    if (state == DNSState.ANNOUNCED)
2108                    {
2109                        if (count++ < 3 && !info.hasData())
2110                        {
2111                            long now = System.currentTimeMillis();
2112                            DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
2113                            out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN));
2114                            out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN));
2115                            if (info.server != null)
2116                            {
2117                                out.addQuestion(new DNSQuestion(info.server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
2118                            }
2119                            out.addAnswer((DNSRecord) cache.get(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN), now);
2120                            out.addAnswer((DNSRecord) cache.get(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN), now);
2121                            if (info.server != null)
2122                            {
2123                                out.addAnswer((DNSRecord) cache.get(info.server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN), now);
2124                            }
2125                            send(out);
2126                        }
2127                        else
2128                        {
2129                            // After three queries, we can quit.
2130                            cancel();
2131                            removeListener(info);
2132                        }
2133                        ;
2134                    }
2135                    else
2136                    {
2137                        if (state == DNSState.CANCELED)
2138                        {
2139                            cancel();
2140                            removeListener(info);
2141                        }
2142                    }
2143                }
2144                catch (Throwable e)
2145                {
2146                    logger.log(Level.WARNING, "run() exception ", e);
2147                    recover();
2148                }
2149            }
2150        }
2151    
2152        /**
2153         * The Canceler sends two announces with TTL=0 for the specified services.
2154         */
2155        private class Canceler extends TimerTask
2156        {
2157            /**
2158             * Counts the number of announces being sent.
2159             */
2160            int count = 0;
2161            /**
2162             * The services that need cancelling.
2163             * Note: We have to use a local variable here, because the services
2164             * that are canceled, are removed immediately from variable JmDNS.services.
2165             */
2166            private ServiceInfo[] infos;
2167            /**
2168             * We call notifyAll() on the lock object, when we have canceled the
2169             * service infos.
2170             * This is used by method JmDNS.unregisterService() and
2171             * JmDNS.unregisterAllServices, to ensure that the JmDNS
2172             * socket stays open until the Canceler has canceled all services.
2173             * <p/>
2174             * Note: We need this lock, because ServiceInfos do the transition from
2175             * state ANNOUNCED to state CANCELED before we get here. We could get
2176             * rid of this lock, if we added a state named CANCELLING to DNSState.
2177             */
2178            private Object lock;
2179            int ttl = 0;
2180    
2181            public Canceler(ServiceInfo info, Object lock)
2182            {
2183                this.infos = new ServiceInfo[]{info};
2184                this.lock = lock;
2185                addListener(info, new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
2186            }
2187    
2188            public Canceler(ServiceInfo[] infos, Object lock)
2189            {
2190                this.infos = infos;
2191                this.lock = lock;
2192            }
2193    
2194            public Canceler(Collection infos, Object lock)
2195            {
2196                this.infos = (ServiceInfo[]) infos.toArray(new ServiceInfo[infos.size()]);
2197                this.lock = lock;
2198            }
2199    
2200            public void start()
2201            {
2202                timer.schedule(this, 0, DNSConstants.ANNOUNCE_WAIT_INTERVAL);
2203            }
2204    
2205            public void run()
2206            {
2207                try
2208                {
2209                    if (++count < 3)
2210                    {
2211                        logger.finer("run() JmDNS canceling service");
2212                        // announce the service
2213                        //long now = System.currentTimeMillis();
2214                        DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
2215                        for (int i = 0; i < infos.length; i++)
2216                        {
2217                            ServiceInfo info = infos[i];
2218                            out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, ttl, info.getQualifiedName()), 0);
2219                            out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, ttl, info.priority, info.weight, info.port, localHost.getName()), 0);
2220                            out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, ttl, info.text), 0);
2221                            DNSRecord answer = localHost.getDNS4AddressRecord();
2222                            if (answer != null)
2223                            {
2224                                out.addAnswer(answer, 0);
2225                            }
2226                            answer = localHost.getDNS6AddressRecord();
2227                            if (answer != null)
2228                            {
2229                                out.addAnswer(answer, 0);
2230                            }
2231                        }
2232                        send(out);
2233                    }
2234                    else
2235                    {
2236                        // After three successful announcements, we are finished.
2237                        synchronized (lock)
2238                        {
2239                            closed=true;
2240                            lock.notifyAll();
2241                        }
2242                        cancel();
2243                    }
2244                }
2245                catch (Throwable e)
2246                {
2247                    logger.log(Level.WARNING, "run() exception ", e);
2248                    recover();
2249                }
2250            }
2251        }
2252        
2253        // REMIND: Why is this not an anonymous inner class?
2254        /**
2255         * Shutdown operations.
2256         */
2257        private class Shutdown implements Runnable
2258        {
2259            public void run()
2260            {
2261                shutdown = null;
2262                close();
2263            }
2264        }
2265    
2266        /**
2267         * Recover jmdns when there is an error.
2268         */
2269        protected void recover()
2270        {
2271            logger.finer("recover()");
2272            // We have an IO error so lets try to recover if anything happens lets close it.
2273            // This should cover the case of the IP address changing under our feet
2274            if (DNSState.CANCELED != state)
2275            {
2276                synchronized (this)
2277                { // Synchronize only if we are not already in process to prevent dead locks
2278                    //
2279                    logger.finer("recover() Cleanning up");
2280                    // Stop JmDNS
2281                    state = DNSState.CANCELED; // This protects against recursive calls
2282    
2283                    // We need to keep a copy for reregistration
2284                    Collection oldServiceInfos = new ArrayList(services.values());
2285    
2286                    // Cancel all services
2287                    unregisterAllServices();
2288                    disposeServiceCollectors();
2289                    //
2290                    // close multicast socket
2291                    closeMulticastSocket();
2292                    //
2293                    cache.clear();
2294                    logger.finer("recover() All is clean");
2295                    //
2296                    // All is clear now start the services
2297                    //
2298                    try
2299                    {
2300                        openMulticastSocket(localHost);
2301                        start(oldServiceInfos);
2302                    }
2303                    catch (Exception exception)
2304                    {
2305                        logger.log(Level.WARNING, "recover() Start services exception ", exception);
2306                    }
2307                    logger.log(Level.WARNING, "recover() We are back!");
2308                }
2309            }
2310        }
2311    
2312        /**
2313         * Close down jmdns. Release all resources and unregister all services.
2314         */
2315        public void close()
2316        {
2317            if (state != DNSState.CANCELED)
2318            {
2319                synchronized (this)
2320                { // Synchronize only if we are not already in process to prevent dead locks
2321                    // Stop JmDNS
2322                    state = DNSState.CANCELED; // This protects against recursive calls
2323    
2324                    unregisterAllServices();
2325                    disposeServiceCollectors();
2326    
2327                    // close socket
2328                    closeMulticastSocket();
2329    
2330                    // Stop the timer
2331                    timer.cancel();
2332    
2333                    // remove the shutdown hook
2334                    if (shutdown != null)
2335                    {
2336                        Runtime.getRuntime().removeShutdownHook(shutdown);
2337                    }
2338    
2339                }
2340            }
2341        }
2342    
2343        /**
2344         * List cache entries, for debugging only.
2345         */
2346        void print()
2347        {
2348            System.out.println("---- cache ----");
2349            cache.print();
2350            System.out.println();
2351        }
2352    
2353        /**
2354         * List Services and serviceTypes.
2355         * Debugging Only
2356         */
2357    
2358        public void printServices()
2359        {
2360            System.err.println(toString());
2361        }
2362    
2363        public String toString()
2364        {
2365            StringBuffer aLog = new StringBuffer();
2366            aLog.append("\t---- Services -----");
2367            if (services != null)
2368            {
2369                for (Iterator k = services.keySet().iterator(); k.hasNext();)
2370                {
2371                    Object key = k.next();
2372                    aLog.append("\n\t\tService: " + key + ": " + services.get(key));
2373                }
2374            }
2375            aLog.append("\n");
2376            aLog.append("\t---- Types ----");
2377            if (serviceTypes != null)
2378            {
2379                for (Iterator k = serviceTypes.keySet().iterator(); k.hasNext();)
2380                {
2381                    Object key = k.next();
2382                    aLog.append("\n\t\tType: " + key + ": " + serviceTypes.get(key));
2383                }
2384            }
2385            aLog.append("\n");
2386            aLog.append(cache.toString());
2387            aLog.append("\n");
2388            aLog.append("\t---- Service Collectors ----");
2389            if (serviceCollectors != null)
2390            {
2391                synchronized (serviceCollectors)
2392                {
2393                    for (Iterator k = serviceCollectors.keySet().iterator(); k.hasNext();)
2394                    {
2395                        Object key = k.next();
2396                        aLog.append("\n\t\tService Collector: " + key + ": " + serviceCollectors.get(key));
2397                    }
2398                    serviceCollectors.clear();
2399                }
2400            }
2401            return aLog.toString();
2402        }
2403    
2404        /**
2405         * Returns a list of service infos of the specified type.
2406         *
2407         * @param type Service type name, such as <code>_http._tcp.local.</code>.
2408         * @return An array of service instance names.
2409         */
2410        public ServiceInfo[] list(String type)
2411        {
2412            // Implementation note: The first time a list for a given type is
2413            // requested, a ServiceCollector is created which collects service
2414            // infos. This greatly speeds up the performance of subsequent calls
2415            // to this method. The caveats are, that 1) the first call to this method
2416            // for a given type is slow, and 2) we spawn a ServiceCollector
2417            // instance for each service type which increases network traffic a
2418            // little.
2419    
2420            ServiceCollector collector;
2421    
2422            boolean newCollectorCreated;
2423            synchronized (serviceCollectors)
2424            {
2425                collector = (ServiceCollector) serviceCollectors.get(type);
2426                if (collector == null)
2427                {
2428                    collector = new ServiceCollector(type);
2429                    serviceCollectors.put(type, collector);
2430                    addServiceListener(type, collector);
2431                    newCollectorCreated = true;
2432                }
2433                else
2434                {
2435                    newCollectorCreated = false;
2436                }
2437            }
2438    
2439            // After creating a new ServiceCollector, we collect service infos for
2440            // 200 milliseconds. This should be enough time, to get some service
2441            // infos from the network.
2442            if (newCollectorCreated)
2443            {
2444                try
2445                {
2446                    Thread.sleep(200);
2447                }
2448                catch (InterruptedException e)
2449                {
2450                }
2451            }
2452    
2453            return collector.list();
2454        }
2455    
2456        /**
2457         * This method disposes all ServiceCollector instances which have been
2458         * created by calls to method <code>list(type)</code>.
2459         *
2460         * @see #list
2461         */
2462        private void disposeServiceCollectors()
2463        {
2464            logger.finer("disposeServiceCollectors()");
2465            synchronized (serviceCollectors)
2466            {
2467                for (Iterator i = serviceCollectors.values().iterator(); i.hasNext();)
2468                {
2469                    ServiceCollector collector = (ServiceCollector) i.next();
2470                    removeServiceListener(collector.type, collector);
2471                }
2472                serviceCollectors.clear();
2473            }
2474        }
2475    
2476        /**
2477         * Instances of ServiceCollector are used internally to speed up the
2478         * performance of method <code>list(type)</code>.
2479         *
2480         * @see #list
2481         */
2482        private static class ServiceCollector implements ServiceListener
2483        {
2484            private static Logger logger = Logger.getLogger(ServiceCollector.class.toString());
2485            /**
2486             * A set of collected service instance names.
2487             */
2488            private Map infos = Collections.synchronizedMap(new HashMap());
2489    
2490            public String type;
2491    
2492            public ServiceCollector(String type)
2493            {
2494                this.type = type;
2495            }
2496    
2497            /**
2498             * A service has been added.
2499             */
2500            public void serviceAdded(ServiceEvent event)
2501            {
2502                synchronized (infos)
2503                {
2504                    event.getDNS().requestServiceInfo(event.getType(), event.getName(), 0);
2505                }
2506            }
2507    
2508            /**
2509             * A service has been removed.
2510             */
2511            public void serviceRemoved(ServiceEvent event)
2512            {
2513                synchronized (infos)
2514                {
2515                    infos.remove(event.getName());
2516                }
2517            }
2518    
2519            /**
2520             * A service hase been resolved. Its details are now available in the
2521             * ServiceInfo record.
2522             */
2523            public void serviceResolved(ServiceEvent event)
2524            {
2525                synchronized (infos)
2526                {
2527                    infos.put(event.getName(), event.getInfo());
2528                }
2529            }
2530    
2531            /**
2532             * Returns an array of all service infos which have been collected by this
2533             * ServiceCollector.
2534             */
2535            public ServiceInfo[] list()
2536            {
2537                synchronized (infos)
2538                {
2539                    return (ServiceInfo[]) infos.values().toArray(new ServiceInfo[infos.size()]);
2540                }
2541            }
2542    
2543            public String toString()
2544            {
2545                StringBuffer aLog = new StringBuffer();
2546                synchronized (infos)
2547                {
2548                    for (Iterator k = infos.keySet().iterator(); k.hasNext();)
2549                    {
2550                        Object key = k.next();
2551                        aLog.append("\n\t\tService: " + key + ": " + infos.get(key));
2552                    }
2553                }
2554                return aLog.toString();
2555            }
2556        };
2557        
2558        private static String toUnqualifiedName(String type, String qualifiedName)
2559        {
2560            if (qualifiedName.endsWith(type))
2561            {
2562                return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
2563            }
2564            else
2565            {
2566                return qualifiedName;
2567            }
2568        }
2569    }
2570