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