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