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