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