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