Home | History | Annotate | Download | only in impl
      1 // /Copyright 2003-2005 Arthur van Hoff, Rick Blair
      2 // Licensed under Apache License version 2.0
      3 // Original license LGPL
      4 
      5 package javax.jmdns.impl;
      6 
      7 import java.io.IOException;
      8 import java.net.DatagramPacket;
      9 import java.net.Inet4Address;
     10 import java.net.Inet6Address;
     11 import java.net.InetAddress;
     12 import java.net.MulticastSocket;
     13 import java.net.SocketException;
     14 import java.util.AbstractMap;
     15 import java.util.ArrayList;
     16 import java.util.Collection;
     17 import java.util.Collections;
     18 import java.util.HashMap;
     19 import java.util.HashSet;
     20 import java.util.Iterator;
     21 import java.util.LinkedList;
     22 import java.util.List;
     23 import java.util.Map;
     24 import java.util.Properties;
     25 import java.util.Random;
     26 import java.util.Set;
     27 import java.util.concurrent.ConcurrentHashMap;
     28 import java.util.concurrent.ConcurrentMap;
     29 import java.util.concurrent.ExecutorService;
     30 import java.util.concurrent.Executors;
     31 import java.util.concurrent.locks.ReentrantLock;
     32 import java.util.logging.Level;
     33 import java.util.logging.Logger;
     34 
     35 import javax.jmdns.JmDNS;
     36 import javax.jmdns.ServiceEvent;
     37 import javax.jmdns.ServiceInfo;
     38 import javax.jmdns.ServiceInfo.Fields;
     39 import javax.jmdns.ServiceListener;
     40 import javax.jmdns.ServiceTypeListener;
     41 import javax.jmdns.impl.ListenerStatus.ServiceListenerStatus;
     42 import javax.jmdns.impl.ListenerStatus.ServiceTypeListenerStatus;
     43 import javax.jmdns.impl.constants.DNSConstants;
     44 import javax.jmdns.impl.constants.DNSRecordClass;
     45 import javax.jmdns.impl.constants.DNSRecordType;
     46 import javax.jmdns.impl.constants.DNSState;
     47 import javax.jmdns.impl.tasks.DNSTask;
     48 
     49 // REMIND: multiple IP addresses
     50 
     51 /**
     52  * mDNS implementation in Java.
     53  *
     54  * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Scott Lewis
     55  */
     56 public class JmDNSImpl extends JmDNS implements DNSStatefulObject, DNSTaskStarter {
     57     private static Logger logger = Logger.getLogger(JmDNSImpl.class.getName());
     58 
     59     public enum Operation {
     60         Remove, Update, Add, RegisterServiceType, Noop
     61     }
     62 
     63     /**
     64      * This is the multicast group, we are listening to for multicast DNS messages.
     65      */
     66     private volatile InetAddress                                     _group;
     67     /**
     68      * This is our multicast socket.
     69      */
     70     private volatile MulticastSocket                                 _socket;
     71 
     72     /**
     73      * Holds instances of JmDNS.DNSListener. Must by a synchronized collection, because it is updated from concurrent threads.
     74      */
     75     private final List<DNSListener>                                  _listeners;
     76 
     77     /**
     78      * Holds instances of ServiceListener's. Keys are Strings holding a fully qualified service type. Values are LinkedList's of ServiceListener's.
     79      */
     80     private final ConcurrentMap<String, List<ServiceListenerStatus>> _serviceListeners;
     81 
     82     /**
     83      * Holds instances of ServiceTypeListener's.
     84      */
     85     private final Set<ServiceTypeListenerStatus>                     _typeListeners;
     86 
     87     /**
     88      * Cache for DNSEntry's.
     89      */
     90     private final DNSCache                                           _cache;
     91 
     92     /**
     93      * This hashtable holds the services that have been registered. Keys are instances of String which hold an all lower-case version of the fully qualified service name. Values are instances of ServiceInfo.
     94      */
     95     private final ConcurrentMap<String, ServiceInfo>                 _services;
     96 
     97     /**
     98      * This hashtable holds the service types that have been registered or that have been received in an incoming datagram.<br/>
     99      * Keys are instances of String which hold an all lower-case version of the fully qualified service type.<br/>
    100      * Values hold the fully qualified service type.
    101      */
    102     private final ConcurrentMap<String, ServiceTypeEntry>            _serviceTypes;
    103 
    104     private volatile Delegate                                        _delegate;
    105 
    106     /**
    107      * This is used to store type entries. The type is stored as a call variable and the map support the subtypes.
    108      * <p>
    109      * The key is the lowercase version as the value is the case preserved version.
    110      * </p>
    111      */
    112     public static class ServiceTypeEntry extends AbstractMap<String, String> implements Cloneable {
    113 
    114         private final Set<Map.Entry<String, String>> _entrySet;
    115 
    116         private final String                         _type;
    117 
    118         private static class SubTypeEntry implements Entry<String, String>, java.io.Serializable, Cloneable {
    119 
    120             private static final long serialVersionUID = 9188503522395855322L;
    121 
    122             private final String      _key;
    123             private final String      _value;
    124 
    125             public SubTypeEntry(String subtype) {
    126                 super();
    127                 _value = (subtype != null ? subtype : "");
    128                 _key = _value.toLowerCase();
    129             }
    130 
    131             /**
    132              * {@inheritDoc}
    133              */
    134             @Override
    135             public String getKey() {
    136                 return _key;
    137             }
    138 
    139             /**
    140              * {@inheritDoc}
    141              */
    142             @Override
    143             public String getValue() {
    144                 return _value;
    145             }
    146 
    147             /**
    148              * Replaces the value corresponding to this entry with the specified value (optional operation). This implementation simply throws <tt>UnsupportedOperationException</tt>, as this class implements an <i>immutable</i> map entry.
    149              *
    150              * @param value
    151              *            new value to be stored in this entry
    152              * @return (Does not return)
    153              * @exception UnsupportedOperationException
    154              *                always
    155              */
    156             @Override
    157             public String setValue(String value) {
    158                 throw new UnsupportedOperationException();
    159             }
    160 
    161             /**
    162              * {@inheritDoc}
    163              */
    164             @Override
    165             public boolean equals(Object entry) {
    166                 if (!(entry instanceof Map.Entry)) {
    167                     return false;
    168                 }
    169                 return this.getKey().equals(((Map.Entry<?, ?>) entry).getKey()) && this.getValue().equals(((Map.Entry<?, ?>) entry).getValue());
    170             }
    171 
    172             /**
    173              * {@inheritDoc}
    174              */
    175             @Override
    176             public int hashCode() {
    177                 return (_key == null ? 0 : _key.hashCode()) ^ (_value == null ? 0 : _value.hashCode());
    178             }
    179 
    180             /*
    181              * (non-Javadoc)
    182              * @see java.lang.Object#clone()
    183              */
    184             @Override
    185             public SubTypeEntry clone() {
    186                 // Immutable object
    187                 return this;
    188             }
    189 
    190             /**
    191              * {@inheritDoc}
    192              */
    193             @Override
    194             public String toString() {
    195                 return _key + "=" + _value;
    196             }
    197 
    198         }
    199 
    200         public ServiceTypeEntry(String type) {
    201             super();
    202             this._type = type;
    203             this._entrySet = new HashSet<Map.Entry<String, String>>();
    204         }
    205 
    206         /**
    207          * The type associated with this entry.
    208          *
    209          * @return the type
    210          */
    211         public String getType() {
    212             return _type;
    213         }
    214 
    215         /*
    216          * (non-Javadoc)
    217          * @see java.util.AbstractMap#entrySet()
    218          */
    219         @Override
    220         public Set<Map.Entry<String, String>> entrySet() {
    221             return _entrySet;
    222         }
    223 
    224         /**
    225          * Returns <code>true</code> if this set contains the specified element. More formally, returns <code>true</code> if and only if this set contains an element <code>e</code> such that
    226          * <code>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</code>.
    227          *
    228          * @param subtype
    229          *            element whose presence in this set is to be tested
    230          * @return <code>true</code> if this set contains the specified element
    231          */
    232         public boolean contains(String subtype) {
    233             return subtype != null && this.containsKey(subtype.toLowerCase());
    234         }
    235 
    236         /**
    237          * Adds the specified element to this set if it is not already present. More formally, adds the specified element <code>e</code> to this set if this set contains no element <code>e2</code> such that
    238          * <code>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</code>. If this set already contains the element, the call leaves the set unchanged and returns <code>false</code>.
    239          *
    240          * @param subtype
    241          *            element to be added to this set
    242          * @return <code>true</code> if this set did not already contain the specified element
    243          */
    244         public boolean add(String subtype) {
    245             if (subtype == null || this.contains(subtype)) {
    246                 return false;
    247             }
    248             _entrySet.add(new SubTypeEntry(subtype));
    249             return true;
    250         }
    251 
    252         /**
    253          * Returns an iterator over the elements in this set. The elements are returned in no particular order (unless this set is an instance of some class that provides a guarantee).
    254          *
    255          * @return an iterator over the elements in this set
    256          */
    257         public Iterator<String> iterator() {
    258             return this.keySet().iterator();
    259         }
    260 
    261         /*
    262          * (non-Javadoc)
    263          * @see java.util.AbstractMap#clone()
    264          */
    265         @Override
    266         public ServiceTypeEntry clone() {
    267             ServiceTypeEntry entry = new ServiceTypeEntry(this.getType());
    268             for (Map.Entry<String, String> subTypeEntry : this.entrySet()) {
    269                 entry.add(subTypeEntry.getValue());
    270             }
    271             return entry;
    272         }
    273 
    274         /*
    275          * (non-Javadoc)
    276          * @see java.util.AbstractMap#toString()
    277          */
    278         @Override
    279         public String toString() {
    280             final StringBuilder aLog = new StringBuilder(200);
    281             if (this.isEmpty()) {
    282                 aLog.append("empty");
    283             } else {
    284                 for (String value : this.values()) {
    285                     aLog.append(value);
    286                     aLog.append(", ");
    287                 }
    288                 aLog.setLength(aLog.length() - 2);
    289             }
    290             return aLog.toString();
    291         }
    292 
    293     }
    294 
    295     /**
    296      * This is the shutdown hook, we registered with the java runtime.
    297      */
    298     protected Thread                                      _shutdown;
    299 
    300     /**
    301      * Handle on the local host
    302      */
    303     private HostInfo                                      _localHost;
    304 
    305     private Thread                                        _incomingListener;
    306 
    307     /**
    308      * Throttle count. This is used to count the overall number of probes sent by JmDNS. When the last throttle increment happened .
    309      */
    310     private int                                           _throttle;
    311 
    312     /**
    313      * Last throttle increment.
    314      */
    315     private long                                          _lastThrottleIncrement;
    316 
    317     private final ExecutorService                         _executor = Executors.newSingleThreadExecutor();
    318 
    319     //
    320     // 2009-09-16 ldeck: adding docbug patch with slight ammendments
    321     // 'Fixes two deadlock conditions involving JmDNS.close() - ID: 1473279'
    322     //
    323     // ---------------------------------------------------
    324     /**
    325      * The timer that triggers our announcements. We can't use the main timer object, because that could cause a deadlock where Prober waits on JmDNS.this lock held by close(), close() waits for us to finish, and we wait for Prober to give us back
    326      * the timer thread so we can announce. (Patch from docbug in 2006-04-19 still wasn't patched .. so I'm doing it!)
    327      */
    328     // private final Timer _cancelerTimer;
    329     // ---------------------------------------------------
    330 
    331     /**
    332      * The source for random values. This is used to introduce random delays in responses. This reduces the potential for collisions on the network.
    333      */
    334     private final static Random                           _random   = new Random();
    335 
    336     /**
    337      * This lock is used to coordinate processing of incoming and outgoing messages. This is needed, because the Rendezvous Conformance Test does not forgive race conditions.
    338      */
    339     private final ReentrantLock                           _ioLock   = new ReentrantLock();
    340 
    341     /**
    342      * If an incoming package which needs an answer is truncated, we store it here. We add more incoming DNSRecords to it, until the JmDNS.Responder timer picks it up.<br/>
    343      * FIXME [PJYF June 8 2010]: This does not work well with multiple planned answers for packages that came in from different clients.
    344      */
    345     private DNSIncoming                                   _plannedAnswer;
    346 
    347     // State machine
    348 
    349     /**
    350      * This hashtable is used to maintain a list of service types being collected by this JmDNS instance. The key of the hashtable is a service type name, the value is an instance of JmDNS.ServiceCollector.
    351      *
    352      * @see #list
    353      */
    354     private final ConcurrentMap<String, ServiceCollector> _serviceCollectors;
    355 
    356     private final String                                  _name;
    357 
    358     /**
    359      * Main method to display API information if run from java -jar
    360      *
    361      * @param argv
    362      *            the command line arguments
    363      */
    364     public static void main(String[] argv) {
    365         String version = null;
    366         try {
    367             final Properties pomProperties = new Properties();
    368             pomProperties.load(JmDNSImpl.class.getResourceAsStream("/META-INF/maven/javax.jmdns/jmdns/pom.properties"));
    369             version = pomProperties.getProperty("version");
    370         } catch (Exception e) {
    371             version = "RUNNING.IN.IDE.FULL";
    372         }
    373         System.out.println("JmDNS version \"" + version + "\"");
    374         System.out.println(" ");
    375 
    376         System.out.println("Running on java version \"" + System.getProperty("java.version") + "\"" + " (build " + System.getProperty("java.runtime.version") + ")" + " from " + System.getProperty("java.vendor"));
    377 
    378         System.out.println("Operating environment \"" + System.getProperty("os.name") + "\"" + " version " + System.getProperty("os.version") + " on " + System.getProperty("os.arch"));
    379 
    380         System.out.println("For more information on JmDNS please visit https://sourceforge.net/projects/jmdns/");
    381     }
    382 
    383     /**
    384      * Create an instance of JmDNS and bind it to a specific network interface given its IP-address.
    385      *
    386      * @param address
    387      *            IP address to bind to.
    388      * @param name
    389      *            name of the newly created JmDNS
    390      * @exception IOException
    391      */
    392     public JmDNSImpl(InetAddress address, String name) throws IOException {
    393         super();
    394         if (logger.isLoggable(Level.FINER)) {
    395             logger.finer("JmDNS instance created");
    396         }
    397         _cache = new DNSCache(100);
    398 
    399         _listeners = Collections.synchronizedList(new ArrayList<DNSListener>());
    400         _serviceListeners = new ConcurrentHashMap<String, List<ServiceListenerStatus>>();
    401         _typeListeners = Collections.synchronizedSet(new HashSet<ServiceTypeListenerStatus>());
    402         _serviceCollectors = new ConcurrentHashMap<String, ServiceCollector>();
    403 
    404         _services = new ConcurrentHashMap<String, ServiceInfo>(20);
    405         _serviceTypes = new ConcurrentHashMap<String, ServiceTypeEntry>(20);
    406 
    407         _localHost = HostInfo.newHostInfo(address, this, name);
    408         _name = (name != null ? name : _localHost.getName());
    409 
    410         // _cancelerTimer = new Timer("JmDNS.cancelerTimer");
    411 
    412         // (ldeck 2.1.1) preventing shutdown blocking thread
    413         // -------------------------------------------------
    414         // _shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
    415         // Runtime.getRuntime().addShutdownHook(_shutdown);
    416 
    417         // -------------------------------------------------
    418 
    419         // Bind to multicast socket
    420         this.openMulticastSocket(this.getLocalHost());
    421         this.start(this.getServices().values());
    422 
    423         this.startReaper();
    424     }
    425 
    426     private void start(Collection<? extends ServiceInfo> serviceInfos) {
    427         if (_incomingListener == null) {
    428             _incomingListener = new SocketListener(this);
    429             _incomingListener.start();
    430         }
    431         this.startProber();
    432         for (ServiceInfo info : serviceInfos) {
    433             try {
    434                 this.registerService(new ServiceInfoImpl(info));
    435             } catch (final Exception exception) {
    436                 logger.log(Level.WARNING, "start() Registration exception ", exception);
    437             }
    438         }
    439     }
    440 
    441     private void openMulticastSocket(HostInfo hostInfo) throws IOException {
    442         if (_group == null) {
    443             if (hostInfo.getInetAddress() instanceof Inet6Address) {
    444                 _group = InetAddress.getByName(DNSConstants.MDNS_GROUP_IPV6);
    445             } else {
    446                 _group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
    447             }
    448         }
    449         if (_socket != null) {
    450             this.closeMulticastSocket();
    451         }
    452         _socket = new MulticastSocket(DNSConstants.MDNS_PORT);
    453         if ((hostInfo != null) && (hostInfo.getInterface() != null)) {
    454             try {
    455                 _socket.setNetworkInterface(hostInfo.getInterface());
    456             } catch (SocketException e) {
    457                 if (logger.isLoggable(Level.FINE)) {
    458                     logger.fine("openMulticastSocket() Set network interface exception: " + e.getMessage());
    459                 }
    460             }
    461         }
    462         _socket.setTimeToLive(255);
    463         _socket.joinGroup(_group);
    464     }
    465 
    466     private void closeMulticastSocket() {
    467         // jP: 20010-01-18. See below. We'll need this monitor...
    468         // assert (Thread.holdsLock(this));
    469         if (logger.isLoggable(Level.FINER)) {
    470             logger.finer("closeMulticastSocket()");
    471         }
    472         if (_socket != null) {
    473             // close socket
    474             try {
    475                 try {
    476                     _socket.leaveGroup(_group);
    477                 } catch (SocketException exception) {
    478                     //
    479                 }
    480                 _socket.close();
    481                 // jP: 20010-01-18. It isn't safe to join() on the listener
    482                 // thread - it attempts to lock the IoLock object, and deadlock
    483                 // ensues. Per issue #2933183, changed this to wait on the JmDNS
    484                 // monitor, checking on each notify (or timeout) that the
    485                 // listener thread has stopped.
    486                 //
    487                 while (_incomingListener != null && _incomingListener.isAlive()) {
    488                     synchronized (this) {
    489                         try {
    490                             if (_incomingListener != null && _incomingListener.isAlive()) {
    491                                 // wait time is arbitrary, we're really expecting notification.
    492                                 if (logger.isLoggable(Level.FINER)) {
    493                                     logger.finer("closeMulticastSocket(): waiting for jmDNS monitor");
    494                                 }
    495                                 this.wait(1000);
    496                             }
    497                         } catch (InterruptedException ignored) {
    498                             // Ignored
    499                         }
    500                     }
    501                 }
    502                 _incomingListener = null;
    503             } catch (final Exception exception) {
    504                 logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", exception);
    505             }
    506             _socket = null;
    507         }
    508     }
    509 
    510     // State machine
    511     /**
    512      * {@inheritDoc}
    513      */
    514     @Override
    515     public boolean advanceState(DNSTask task) {
    516         return this._localHost.advanceState(task);
    517     }
    518 
    519     /**
    520      * {@inheritDoc}
    521      */
    522     @Override
    523     public boolean revertState() {
    524         return this._localHost.revertState();
    525     }
    526 
    527     /**
    528      * {@inheritDoc}
    529      */
    530     @Override
    531     public boolean cancelState() {
    532         return this._localHost.cancelState();
    533     }
    534 
    535     /**
    536      * {@inheritDoc}
    537      */
    538     @Override
    539     public boolean closeState() {
    540         return this._localHost.closeState();
    541     }
    542 
    543     /**
    544      * {@inheritDoc}
    545      */
    546     @Override
    547     public boolean recoverState() {
    548         return this._localHost.recoverState();
    549     }
    550 
    551     /**
    552      * {@inheritDoc}
    553      */
    554     @Override
    555     public JmDNSImpl getDns() {
    556         return this;
    557     }
    558 
    559     /**
    560      * {@inheritDoc}
    561      */
    562     @Override
    563     public void associateWithTask(DNSTask task, DNSState state) {
    564         this._localHost.associateWithTask(task, state);
    565     }
    566 
    567     /**
    568      * {@inheritDoc}
    569      */
    570     @Override
    571     public void removeAssociationWithTask(DNSTask task) {
    572         this._localHost.removeAssociationWithTask(task);
    573     }
    574 
    575     /**
    576      * {@inheritDoc}
    577      */
    578     @Override
    579     public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
    580         return this._localHost.isAssociatedWithTask(task, state);
    581     }
    582 
    583     /**
    584      * {@inheritDoc}
    585      */
    586     @Override
    587     public boolean isProbing() {
    588         return this._localHost.isProbing();
    589     }
    590 
    591     /**
    592      * {@inheritDoc}
    593      */
    594     @Override
    595     public boolean isAnnouncing() {
    596         return this._localHost.isAnnouncing();
    597     }
    598 
    599     /**
    600      * {@inheritDoc}
    601      */
    602     @Override
    603     public boolean isAnnounced() {
    604         return this._localHost.isAnnounced();
    605     }
    606 
    607     /**
    608      * {@inheritDoc}
    609      */
    610     @Override
    611     public boolean isCanceling() {
    612         return this._localHost.isCanceling();
    613     }
    614 
    615     /**
    616      * {@inheritDoc}
    617      */
    618     @Override
    619     public boolean isCanceled() {
    620         return this._localHost.isCanceled();
    621     }
    622 
    623     /**
    624      * {@inheritDoc}
    625      */
    626     @Override
    627     public boolean isClosing() {
    628         return this._localHost.isClosing();
    629     }
    630 
    631     /**
    632      * {@inheritDoc}
    633      */
    634     @Override
    635     public boolean isClosed() {
    636         return this._localHost.isClosed();
    637     }
    638 
    639     /**
    640      * {@inheritDoc}
    641      */
    642     @Override
    643     public boolean waitForAnnounced(long timeout) {
    644         return this._localHost.waitForAnnounced(timeout);
    645     }
    646 
    647     /**
    648      * {@inheritDoc}
    649      */
    650     @Override
    651     public boolean waitForCanceled(long timeout) {
    652         return this._localHost.waitForCanceled(timeout);
    653     }
    654 
    655     /**
    656      * Return the DNSCache associated with the cache variable
    657      *
    658      * @return DNS cache
    659      */
    660     public DNSCache getCache() {
    661         return _cache;
    662     }
    663 
    664     /**
    665      * {@inheritDoc}
    666      */
    667     @Override
    668     public String getName() {
    669         return _name;
    670     }
    671 
    672     /**
    673      * {@inheritDoc}
    674      */
    675     @Override
    676     public String getHostName() {
    677         return _localHost.getName();
    678     }
    679 
    680     /**
    681      * Returns the local host info
    682      *
    683      * @return local host info
    684      */
    685     public HostInfo getLocalHost() {
    686         return _localHost;
    687     }
    688 
    689     /**
    690      * {@inheritDoc}
    691      */
    692     @Override
    693     public InetAddress getInetAddress() throws IOException {
    694         return _localHost.getInetAddress();
    695     }
    696 
    697     /**
    698      * {@inheritDoc}
    699      */
    700     @Override
    701     @Deprecated
    702     public InetAddress getInterface() throws IOException {
    703         return _socket.getInterface();
    704     }
    705 
    706     /**
    707      * {@inheritDoc}
    708      */
    709     @Override
    710     public ServiceInfo getServiceInfo(String type, String name) {
    711         return this.getServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    712     }
    713 
    714     /**
    715      * {@inheritDoc}
    716      */
    717     @Override
    718     public ServiceInfo getServiceInfo(String type, String name, long timeout) {
    719         return this.getServiceInfo(type, name, false, timeout);
    720     }
    721 
    722     /**
    723      * {@inheritDoc}
    724      */
    725     @Override
    726     public ServiceInfo getServiceInfo(String type, String name, boolean persistent) {
    727         return this.getServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
    728     }
    729 
    730     /**
    731      * {@inheritDoc}
    732      */
    733     @Override
    734     public ServiceInfo getServiceInfo(String type, String name, boolean persistent, long timeout) {
    735         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
    736         this.waitForInfoData(info, timeout);
    737         return (info.hasData() ? info : null);
    738     }
    739 
    740     ServiceInfoImpl resolveServiceInfo(String type, String name, String subtype, boolean persistent) {
    741         this.cleanCache();
    742         String loType = type.toLowerCase();
    743         this.registerServiceType(type);
    744         if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
    745             this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
    746         }
    747 
    748         // Check if the answer is in the cache.
    749         final ServiceInfoImpl info = this.getServiceInfoFromCache(type, name, subtype, persistent);
    750         // We still run the resolver to do the dispatch but if the info is already there it will quit immediately
    751         this.startServiceInfoResolver(info);
    752 
    753         return info;
    754     }
    755 
    756     ServiceInfoImpl getServiceInfoFromCache(String type, String name, String subtype, boolean persistent) {
    757         // Check if the answer is in the cache.
    758         ServiceInfoImpl info = new ServiceInfoImpl(type, name, subtype, 0, 0, 0, persistent, (byte[]) null);
    759         DNSEntry pointerEntry = this.getCache().getDNSEntry(new DNSRecord.Pointer(type, DNSRecordClass.CLASS_ANY, false, 0, info.getQualifiedName()));
    760         if (pointerEntry instanceof DNSRecord) {
    761             ServiceInfoImpl cachedInfo = (ServiceInfoImpl) ((DNSRecord) pointerEntry).getServiceInfo(persistent);
    762             if (cachedInfo != null) {
    763                 // To get a complete info record we need to retrieve the service, address and the text bytes.
    764 
    765                 Map<Fields, String> map = cachedInfo.getQualifiedNameMap();
    766                 byte[] srvBytes = null;
    767                 String server = "";
    768                 DNSEntry serviceEntry = this.getCache().getDNSEntry(info.getQualifiedName(), DNSRecordType.TYPE_SRV, DNSRecordClass.CLASS_ANY);
    769                 if (serviceEntry instanceof DNSRecord) {
    770                     ServiceInfo cachedServiceEntryInfo = ((DNSRecord) serviceEntry).getServiceInfo(persistent);
    771                     if (cachedServiceEntryInfo != null) {
    772                         cachedInfo = new ServiceInfoImpl(map, cachedServiceEntryInfo.getPort(), cachedServiceEntryInfo.getWeight(), cachedServiceEntryInfo.getPriority(), persistent, (byte[]) null);
    773                         srvBytes = cachedServiceEntryInfo.getTextBytes();
    774                         server = cachedServiceEntryInfo.getServer();
    775                     }
    776                 }
    777                 DNSEntry addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_ANY);
    778                 if (addressEntry instanceof DNSRecord) {
    779                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
    780                     if (cachedAddressInfo != null) {
    781                         for (Inet4Address address : cachedAddressInfo.getInet4Addresses()) {
    782                             cachedInfo.addAddress(address);
    783                         }
    784                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
    785                     }
    786                 }
    787                 addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_ANY);
    788                 if (addressEntry instanceof DNSRecord) {
    789                     ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
    790                     if (cachedAddressInfo != null) {
    791                         for (Inet6Address address : cachedAddressInfo.getInet6Addresses()) {
    792                             cachedInfo.addAddress(address);
    793                         }
    794                         cachedInfo._setText(cachedAddressInfo.getTextBytes());
    795                     }
    796                 }
    797                 DNSEntry textEntry = this.getCache().getDNSEntry(cachedInfo.getQualifiedName(), DNSRecordType.TYPE_TXT, DNSRecordClass.CLASS_ANY);
    798                 if (textEntry instanceof DNSRecord) {
    799                     ServiceInfo cachedTextInfo = ((DNSRecord) textEntry).getServiceInfo(persistent);
    800                     if (cachedTextInfo != null) {
    801                         cachedInfo._setText(cachedTextInfo.getTextBytes());
    802                     }
    803                 }
    804                 if (cachedInfo.getTextBytes().length == 0) {
    805                     cachedInfo._setText(srvBytes);
    806                 }
    807                 if (cachedInfo.hasData()) {
    808                     info = cachedInfo;
    809                 }
    810             }
    811         }
    812         return info;
    813     }
    814 
    815     private void waitForInfoData(ServiceInfo info, long timeout) {
    816         synchronized (info) {
    817             long loops = (timeout / 200L);
    818             if (loops < 1) {
    819                 loops = 1;
    820             }
    821             for (int i = 0; i < loops; i++) {
    822                 if (info.hasData()) {
    823                     break;
    824                 }
    825                 try {
    826                     info.wait(200);
    827                 } catch (final InterruptedException e) {
    828                     /* Stub */
    829                 }
    830             }
    831         }
    832     }
    833 
    834     /**
    835      * {@inheritDoc}
    836      */
    837     @Override
    838     public void requestServiceInfo(String type, String name) {
    839         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    840     }
    841 
    842     /**
    843      * {@inheritDoc}
    844      */
    845     @Override
    846     public void requestServiceInfo(String type, String name, boolean persistent) {
    847         this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
    848     }
    849 
    850     /**
    851      * {@inheritDoc}
    852      */
    853     @Override
    854     public void requestServiceInfo(String type, String name, long timeout) {
    855         this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    856     }
    857 
    858     /**
    859      * {@inheritDoc}
    860      */
    861     @Override
    862     public void requestServiceInfo(String type, String name, boolean persistent, long timeout) {
    863         final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
    864         this.waitForInfoData(info, timeout);
    865     }
    866 
    867     void handleServiceResolved(ServiceEvent event) {
    868         List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
    869         final List<ServiceListenerStatus> listCopy;
    870         if ((list != null) && (!list.isEmpty())) {
    871             if ((event.getInfo() != null) && event.getInfo().hasData()) {
    872                 final ServiceEvent localEvent = event;
    873                 synchronized (list) {
    874                     listCopy = new ArrayList<ServiceListenerStatus>(list);
    875                 }
    876                 for (final ServiceListenerStatus listener : listCopy) {
    877                     _executor.submit(new Runnable() {
    878                         /** {@inheritDoc} */
    879                         @Override
    880                         public void run() {
    881                             listener.serviceResolved(localEvent);
    882                         }
    883                     });
    884                 }
    885             }
    886         }
    887     }
    888 
    889     /**
    890      * {@inheritDoc}
    891      */
    892     @Override
    893     public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
    894         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
    895         _typeListeners.add(status);
    896 
    897         // report cached service types
    898         for (String type : _serviceTypes.keySet()) {
    899             status.serviceTypeAdded(new ServiceEventImpl(this, type, "", null));
    900         }
    901 
    902         this.startTypeResolver();
    903     }
    904 
    905     /**
    906      * {@inheritDoc}
    907      */
    908     @Override
    909     public void removeServiceTypeListener(ServiceTypeListener listener) {
    910         ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
    911         _typeListeners.remove(status);
    912     }
    913 
    914     /**
    915      * {@inheritDoc}
    916      */
    917     @Override
    918     public void addServiceListener(String type, ServiceListener listener) {
    919         this.addServiceListener(type, listener, ListenerStatus.ASYNCHONEOUS);
    920     }
    921 
    922     private void addServiceListener(String type, ServiceListener listener, boolean synch) {
    923         ServiceListenerStatus status = new ServiceListenerStatus(listener, synch);
    924         final String loType = type.toLowerCase();
    925         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
    926         if (list == null) {
    927             if (_serviceListeners.putIfAbsent(loType, new LinkedList<ServiceListenerStatus>()) == null) {
    928                 if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
    929                     // We have a problem here. The service collectors must be called synchronously so that their cache get cleaned up immediately or we will report .
    930                     this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS);
    931                 }
    932             }
    933             list = _serviceListeners.get(loType);
    934         }
    935         if (list != null) {
    936             synchronized (list) {
    937                 if (!list.contains(listener)) {
    938                     list.add(status);
    939                 }
    940             }
    941         }
    942         // report cached service types
    943         final List<ServiceEvent> serviceEvents = new ArrayList<ServiceEvent>();
    944         Collection<DNSEntry> dnsEntryLits = this.getCache().allValues();
    945         for (DNSEntry entry : dnsEntryLits) {
    946             final DNSRecord record = (DNSRecord) entry;
    947             if (record.getRecordType() == DNSRecordType.TYPE_SRV) {
    948                 if (record.getKey().endsWith(loType)) {
    949                     // Do not used the record embedded method for generating event this will not work.
    950                     // serviceEvents.add(record.getServiceEvent(this));
    951                     serviceEvents.add(new ServiceEventImpl(this, record.getType(), toUnqualifiedName(record.getType(), record.getName()), record.getServiceInfo()));
    952                 }
    953             }
    954         }
    955         // Actually call listener with all service events added above
    956         for (ServiceEvent serviceEvent : serviceEvents) {
    957             status.serviceAdded(serviceEvent);
    958         }
    959         // Create/start ServiceResolver
    960         this.startServiceResolver(type);
    961     }
    962 
    963     /**
    964      * {@inheritDoc}
    965      */
    966     @Override
    967     public void removeServiceListener(String type, ServiceListener listener) {
    968         String loType = type.toLowerCase();
    969         List<ServiceListenerStatus> list = _serviceListeners.get(loType);
    970         if (list != null) {
    971             synchronized (list) {
    972                 ServiceListenerStatus status = new ServiceListenerStatus(listener, ListenerStatus.ASYNCHONEOUS);
    973                 list.remove(status);
    974                 if (list.isEmpty()) {
    975                     _serviceListeners.remove(loType, list);
    976                 }
    977             }
    978         }
    979     }
    980 
    981     /**
    982      * {@inheritDoc}
    983      */
    984     @Override
    985     public void registerService(ServiceInfo infoAbstract) throws IOException {
    986         if (this.isClosing() || this.isClosed()) {
    987             throw new IllegalStateException("This DNS is closed.");
    988         }
    989         final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;
    990 
    991         if (info.getDns() != null) {
    992             if (info.getDns() != this) {
    993                 throw new IllegalStateException("A service information can only be registered with a single instamce of JmDNS.");
    994             } else if (_services.get(info.getKey()) != null) {
    995                 throw new IllegalStateException("A service information can only be registered once.");
    996             }
    997         }
    998         info.setDns(this);
    999 
   1000         this.registerServiceType(info.getTypeWithSubtype());
   1001 
   1002         // bind the service to this address
   1003         info.recoverState();
   1004         info.setServer(_localHost.getName());
   1005         info.addAddress(_localHost.getInet4Address());
   1006         info.addAddress(_localHost.getInet6Address());
   1007 
   1008         this.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
   1009 
   1010         this.makeServiceNameUnique(info);
   1011         while (_services.putIfAbsent(info.getKey(), info) != null) {
   1012             this.makeServiceNameUnique(info);
   1013         }
   1014 
   1015         this.startProber();
   1016         info.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT);
   1017 
   1018         if (logger.isLoggable(Level.FINE)) {
   1019             logger.fine("registerService() JmDNS registered service as " + info);
   1020         }
   1021     }
   1022 
   1023     /**
   1024      * {@inheritDoc}
   1025      */
   1026     @Override
   1027     public void unregisterService(ServiceInfo infoAbstract) {
   1028         final ServiceInfoImpl info = (ServiceInfoImpl) _services.get(infoAbstract.getKey());
   1029 
   1030         if (info != null) {
   1031             info.cancelState();
   1032             this.startCanceler();
   1033             info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
   1034 
   1035             _services.remove(info.getKey(), info);
   1036             if (logger.isLoggable(Level.FINE)) {
   1037                 logger.fine("unregisterService() JmDNS unregistered service as " + info);
   1038             }
   1039         } else {
   1040             logger.warning("Removing unregistered service info: " + infoAbstract.getKey());
   1041         }
   1042     }
   1043 
   1044     /**
   1045      * {@inheritDoc}
   1046      */
   1047     @Override
   1048     public void unregisterAllServices() {
   1049         if (logger.isLoggable(Level.FINER)) {
   1050             logger.finer("unregisterAllServices()");
   1051         }
   1052 
   1053         for (String name : _services.keySet()) {
   1054             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
   1055             if (info != null) {
   1056                 if (logger.isLoggable(Level.FINER)) {
   1057                     logger.finer("Cancelling service info: " + info);
   1058                 }
   1059                 info.cancelState();
   1060             }
   1061         }
   1062         this.startCanceler();
   1063 
   1064         for (String name : _services.keySet()) {
   1065             ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name);
   1066             if (info != null) {
   1067                 if (logger.isLoggable(Level.FINER)) {
   1068                     logger.finer("Wait for service info cancel: " + info);
   1069                 }
   1070                 info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
   1071                 _services.remove(name, info);
   1072             }
   1073         }
   1074 
   1075     }
   1076 
   1077     /**
   1078      * {@inheritDoc}
   1079      */
   1080     @Override
   1081     public boolean registerServiceType(String type) {
   1082         boolean typeAdded = false;
   1083         Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(type);
   1084         String domain = map.get(Fields.Domain);
   1085         String protocol = map.get(Fields.Protocol);
   1086         String application = map.get(Fields.Application);
   1087         String subtype = map.get(Fields.Subtype);
   1088 
   1089         final String name = (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
   1090         final String loname = name.toLowerCase();
   1091         if (logger.isLoggable(Level.FINE)) {
   1092             logger.fine(this.getName() + ".registering service type: " + type + " as: " + name + (subtype.length() > 0 ? " subtype: " + subtype : ""));
   1093         }
   1094         if (!_serviceTypes.containsKey(loname) && !application.toLowerCase().equals("dns-sd") && !domain.toLowerCase().endsWith("in-addr.arpa") && !domain.toLowerCase().endsWith("ip6.arpa")) {
   1095             typeAdded = _serviceTypes.putIfAbsent(loname, new ServiceTypeEntry(name)) == null;
   1096             if (typeAdded) {
   1097                 final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
   1098                 final ServiceEvent event = new ServiceEventImpl(this, name, "", null);
   1099                 for (final ServiceTypeListenerStatus status : list) {
   1100                     _executor.submit(new Runnable() {
   1101                         /** {@inheritDoc} */
   1102                         @Override
   1103                         public void run() {
   1104                             status.serviceTypeAdded(event);
   1105                         }
   1106                     });
   1107                 }
   1108             }
   1109         }
   1110         if (subtype.length() > 0) {
   1111             ServiceTypeEntry subtypes = _serviceTypes.get(loname);
   1112             if ((subtypes != null) && (!subtypes.contains(subtype))) {
   1113                 synchronized (subtypes) {
   1114                     if (!subtypes.contains(subtype)) {
   1115                         typeAdded = true;
   1116                         subtypes.add(subtype);
   1117                         final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
   1118                         final ServiceEvent event = new ServiceEventImpl(this, "_" + subtype + "._sub." + name, "", null);
   1119                         for (final ServiceTypeListenerStatus status : list) {
   1120                             _executor.submit(new Runnable() {
   1121                                 /** {@inheritDoc} */
   1122                                 @Override
   1123                                 public void run() {
   1124                                     status.subTypeForServiceTypeAdded(event);
   1125                                 }
   1126                             });
   1127                         }
   1128                     }
   1129                 }
   1130             }
   1131         }
   1132         return typeAdded;
   1133     }
   1134 
   1135     /**
   1136      * Generate a possibly unique name for a service using the information we have in the cache.
   1137      *
   1138      * @return returns true, if the name of the service info had to be changed.
   1139      */
   1140     private boolean makeServiceNameUnique(ServiceInfoImpl info) {
   1141         final String originalQualifiedName = info.getKey();
   1142         final long now = System.currentTimeMillis();
   1143 
   1144         boolean collision;
   1145         do {
   1146             collision = false;
   1147 
   1148             // Check for collision in cache
   1149             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(info.getKey())) {
   1150                 if (DNSRecordType.TYPE_SRV.equals(dnsEntry.getRecordType()) && !dnsEntry.isExpired(now)) {
   1151                     final DNSRecord.Service s = (DNSRecord.Service) dnsEntry;
   1152                     if (s.getPort() != info.getPort() || !s.getServer().equals(_localHost.getName())) {
   1153                         if (logger.isLoggable(Level.FINER)) {
   1154                             logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + dnsEntry + " s.server=" + s.getServer() + " " + _localHost.getName() + " equals:" + (s.getServer().equals(_localHost.getName())));
   1155                         }
   1156                         info.setName(incrementName(info.getName()));
   1157                         collision = true;
   1158                         break;
   1159                     }
   1160                 }
   1161             }
   1162 
   1163             // Check for collision with other service infos published by JmDNS
   1164             final ServiceInfo selfService = _services.get(info.getKey());
   1165             if (selfService != null && selfService != info) {
   1166                 info.setName(incrementName(info.getName()));
   1167                 collision = true;
   1168             }
   1169         }
   1170         while (collision);
   1171 
   1172         return !(originalQualifiedName.equals(info.getKey()));
   1173     }
   1174 
   1175     String incrementName(String name) {
   1176         String aName = name;
   1177         try {
   1178             final int l = aName.lastIndexOf('(');
   1179             final int r = aName.lastIndexOf(')');
   1180             if ((l >= 0) && (l < r)) {
   1181                 aName = aName.substring(0, l) + "(" + (Integer.parseInt(aName.substring(l + 1, r)) + 1) + ")";
   1182             } else {
   1183                 aName += " (2)";
   1184             }
   1185         } catch (final NumberFormatException e) {
   1186             aName += " (2)";
   1187         }
   1188         return aName;
   1189     }
   1190 
   1191     /**
   1192      * Add a listener for a question. The listener will receive updates of answers to the question as they arrive, or from the cache if they are already available.
   1193      *
   1194      * @param listener
   1195      *            DSN listener
   1196      * @param question
   1197      *            DNS query
   1198      */
   1199     public void addListener(DNSListener listener, DNSQuestion question) {
   1200         final long now = System.currentTimeMillis();
   1201 
   1202         // add the new listener
   1203         _listeners.add(listener);
   1204 
   1205         // report existing matched records
   1206 
   1207         if (question != null) {
   1208             for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(question.getName().toLowerCase())) {
   1209                 if (question.answeredBy(dnsEntry) && !dnsEntry.isExpired(now)) {
   1210                     listener.updateRecord(this.getCache(), now, dnsEntry);
   1211                 }
   1212             }
   1213         }
   1214     }
   1215 
   1216     /**
   1217      * Remove a listener from all outstanding questions. The listener will no longer receive any updates.
   1218      *
   1219      * @param listener
   1220      *            DSN listener
   1221      */
   1222     public void removeListener(DNSListener listener) {
   1223         _listeners.remove(listener);
   1224     }
   1225 
   1226     /**
   1227      * Renew a service when the record become stale. If there is no service collector for the type this method does nothing.
   1228      *
   1229      * @param record
   1230      *            DNS record
   1231      */
   1232     public void renewServiceCollector(DNSRecord record) {
   1233         ServiceInfo info = record.getServiceInfo();
   1234         if (_serviceCollectors.containsKey(info.getType().toLowerCase())) {
   1235             // Create/start ServiceResolver
   1236             this.startServiceResolver(info.getType());
   1237         }
   1238     }
   1239 
   1240     // Remind: Method updateRecord should receive a better name.
   1241     /**
   1242      * Notify all listeners that a record was updated.
   1243      *
   1244      * @param now
   1245      *            update date
   1246      * @param rec
   1247      *            DNS record
   1248      * @param operation
   1249      *            DNS cache operation
   1250      */
   1251     public void updateRecord(long now, DNSRecord rec, Operation operation) {
   1252         // We do not want to block the entire DNS while we are updating the record for each listener (service info)
   1253         {
   1254             List<DNSListener> listenerList = null;
   1255             synchronized (_listeners) {
   1256                 listenerList = new ArrayList<DNSListener>(_listeners);
   1257             }
   1258             for (DNSListener listener : listenerList) {
   1259                 listener.updateRecord(this.getCache(), now, rec);
   1260             }
   1261         }
   1262         if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()))
   1263         // if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()) || DNSRecordType.TYPE_SRV.equals(rec.getRecordType()))
   1264         {
   1265             ServiceEvent event = rec.getServiceEvent(this);
   1266             if ((event.getInfo() == null) || !event.getInfo().hasData()) {
   1267                 // We do not care about the subtype because the info is only used if complete and the subtype will then be included.
   1268                 ServiceInfo info = this.getServiceInfoFromCache(event.getType(), event.getName(), "", false);
   1269                 if (info.hasData()) {
   1270                     event = new ServiceEventImpl(this, event.getType(), event.getName(), info);
   1271                 }
   1272             }
   1273 
   1274             List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
   1275             final List<ServiceListenerStatus> serviceListenerList;
   1276             if (list != null) {
   1277                 synchronized (list) {
   1278                     serviceListenerList = new ArrayList<ServiceListenerStatus>(list);
   1279                 }
   1280             } else {
   1281                 serviceListenerList = Collections.emptyList();
   1282             }
   1283             if (logger.isLoggable(Level.FINEST)) {
   1284                 logger.finest(this.getName() + ".updating record for event: " + event + " list " + serviceListenerList + " operation: " + operation);
   1285             }
   1286             if (!serviceListenerList.isEmpty()) {
   1287                 final ServiceEvent localEvent = event;
   1288 
   1289                 switch (operation) {
   1290                     case Add:
   1291                         for (final ServiceListenerStatus listener : serviceListenerList) {
   1292                             if (listener.isSynchronous()) {
   1293                                 listener.serviceAdded(localEvent);
   1294                             } else {
   1295                                 _executor.submit(new Runnable() {
   1296                                     /** {@inheritDoc} */
   1297                                     @Override
   1298                                     public void run() {
   1299                                         listener.serviceAdded(localEvent);
   1300                                     }
   1301                                 });
   1302                             }
   1303                         }
   1304                         break;
   1305                     case Remove:
   1306                         for (final ServiceListenerStatus listener : serviceListenerList) {
   1307                             if (listener.isSynchronous()) {
   1308                                 listener.serviceRemoved(localEvent);
   1309                             } else {
   1310                                 _executor.submit(new Runnable() {
   1311                                     /** {@inheritDoc} */
   1312                                     @Override
   1313                                     public void run() {
   1314                                         listener.serviceRemoved(localEvent);
   1315                                     }
   1316                                 });
   1317                             }
   1318                         }
   1319                         break;
   1320                     default:
   1321                         break;
   1322                 }
   1323             }
   1324         }
   1325     }
   1326 
   1327     void handleRecord(DNSRecord record, long now) {
   1328         DNSRecord newRecord = record;
   1329 
   1330         Operation cacheOperation = Operation.Noop;
   1331         final boolean expired = newRecord.isExpired(now);
   1332         if (logger.isLoggable(Level.FINE)) {
   1333             logger.fine(this.getName() + " handle response: " + newRecord);
   1334         }
   1335 
   1336         // update the cache
   1337         if (!newRecord.isServicesDiscoveryMetaQuery() && !newRecord.isDomainDiscoveryQuery()) {
   1338             final boolean unique = newRecord.isUnique();
   1339             final DNSRecord cachedRecord = (DNSRecord) this.getCache().getDNSEntry(newRecord);
   1340             if (logger.isLoggable(Level.FINE)) {
   1341                 logger.fine(this.getName() + " handle response cached record: " + cachedRecord);
   1342             }
   1343             if (unique) {
   1344                 for (DNSEntry entry : this.getCache().getDNSEntryList(newRecord.getKey())) {
   1345                     if (newRecord.getRecordType().equals(entry.getRecordType()) && newRecord.getRecordClass().equals(entry.getRecordClass()) && (entry != cachedRecord)) {
   1346                         ((DNSRecord) entry).setWillExpireSoon(now);
   1347                     }
   1348                 }
   1349             }
   1350             if (cachedRecord != null) {
   1351                 if (expired) {
   1352                     // if the record has a 0 ttl that means we have a cancel record we need to delay the removal by 1s
   1353                     if (newRecord.getTTL() == 0) {
   1354                         cacheOperation = Operation.Noop;
   1355                         cachedRecord.setWillExpireSoon(now);
   1356                         // the actual record will be disposed of by the record reaper.
   1357                     } else {
   1358                         cacheOperation = Operation.Remove;
   1359                         this.getCache().removeDNSEntry(cachedRecord);
   1360                     }
   1361                 } else {
   1362                     // If the record content has changed we need to inform our listeners.
   1363                     if (!newRecord.sameValue(cachedRecord) || (!newRecord.sameSubtype(cachedRecord) && (newRecord.getSubtype().length() > 0))) {
   1364                         if (newRecord.isSingleValued()) {
   1365                             cacheOperation = Operation.Update;
   1366                             this.getCache().replaceDNSEntry(newRecord, cachedRecord);
   1367                         } else {
   1368                             // Address record can have more than one value on multi-homed machines
   1369                             cacheOperation = Operation.Add;
   1370                             this.getCache().addDNSEntry(newRecord);
   1371                         }
   1372                     } else {
   1373                         cachedRecord.resetTTL(newRecord);
   1374                         newRecord = cachedRecord;
   1375                     }
   1376                 }
   1377             } else {
   1378                 if (!expired) {
   1379                     cacheOperation = Operation.Add;
   1380                     this.getCache().addDNSEntry(newRecord);
   1381                 }
   1382             }
   1383         }
   1384 
   1385         // Register new service types
   1386         if (newRecord.getRecordType() == DNSRecordType.TYPE_PTR) {
   1387             // handle DNSConstants.DNS_META_QUERY records
   1388             boolean typeAdded = false;
   1389             if (newRecord.isServicesDiscoveryMetaQuery()) {
   1390                 // The service names are in the alias.
   1391                 if (!expired) {
   1392                     typeAdded = this.registerServiceType(((DNSRecord.Pointer) newRecord).getAlias());
   1393                 }
   1394                 return;
   1395             }
   1396             typeAdded |= this.registerServiceType(newRecord.getName());
   1397             if (typeAdded && (cacheOperation == Operation.Noop)) {
   1398                 cacheOperation = Operation.RegisterServiceType;
   1399             }
   1400         }
   1401 
   1402         // notify the listeners
   1403         if (cacheOperation != Operation.Noop) {
   1404             this.updateRecord(now, newRecord, cacheOperation);
   1405         }
   1406 
   1407     }
   1408 
   1409     /**
   1410      * Handle an incoming response. Cache answers, and pass them on to the appropriate questions.
   1411      *
   1412      * @exception IOException
   1413      */
   1414     void handleResponse(DNSIncoming msg) throws IOException {
   1415         final long now = System.currentTimeMillis();
   1416 
   1417         boolean hostConflictDetected = false;
   1418         boolean serviceConflictDetected = false;
   1419 
   1420         for (DNSRecord newRecord : msg.getAllAnswers()) {
   1421             this.handleRecord(newRecord, now);
   1422 
   1423             if (DNSRecordType.TYPE_A.equals(newRecord.getRecordType()) || DNSRecordType.TYPE_AAAA.equals(newRecord.getRecordType())) {
   1424                 hostConflictDetected |= newRecord.handleResponse(this);
   1425             } else {
   1426                 serviceConflictDetected |= newRecord.handleResponse(this);
   1427             }
   1428 
   1429         }
   1430 
   1431         if (hostConflictDetected || serviceConflictDetected) {
   1432             this.startProber();
   1433         }
   1434     }
   1435 
   1436     /**
   1437      * Handle an incoming query. See if we can answer any part of it given our service infos.
   1438      *
   1439      * @param in
   1440      * @param addr
   1441      * @param port
   1442      * @exception IOException
   1443      */
   1444     void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException {
   1445         if (logger.isLoggable(Level.FINE)) {
   1446             logger.fine(this.getName() + ".handle query: " + in);
   1447         }
   1448         // Track known answers
   1449         boolean conflictDetected = false;
   1450         final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
   1451         for (DNSRecord answer : in.getAllAnswers()) {
   1452             conflictDetected |= answer.handleQuery(this, expirationTime);
   1453         }
   1454 
   1455         this.ioLock();
   1456         try {
   1457 
   1458             if (_plannedAnswer != null) {
   1459                 _plannedAnswer.append(in);
   1460             } else {
   1461                 DNSIncoming plannedAnswer = in.clone();
   1462                 if (in.isTruncated()) {
   1463                     _plannedAnswer = plannedAnswer;
   1464                 }
   1465                 this.startResponder(plannedAnswer, port);
   1466             }
   1467 
   1468         } finally {
   1469             this.ioUnlock();
   1470         }
   1471 
   1472         final long now = System.currentTimeMillis();
   1473         for (DNSRecord answer : in.getAnswers()) {
   1474             this.handleRecord(answer, now);
   1475         }
   1476 
   1477         if (conflictDetected) {
   1478             this.startProber();
   1479         }
   1480     }
   1481 
   1482     public void respondToQuery(DNSIncoming in) {
   1483         this.ioLock();
   1484         try {
   1485             if (_plannedAnswer == in) {
   1486                 _plannedAnswer = null;
   1487             }
   1488         } finally {
   1489             this.ioUnlock();
   1490         }
   1491     }
   1492 
   1493     /**
   1494      * Add an answer to a question. Deal with the case when the outgoing packet overflows
   1495      *
   1496      * @param in
   1497      * @param addr
   1498      * @param port
   1499      * @param out
   1500      * @param rec
   1501      * @return outgoing answer
   1502      * @exception IOException
   1503      */
   1504     public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException {
   1505         DNSOutgoing newOut = out;
   1506         if (newOut == null) {
   1507             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
   1508         }
   1509         try {
   1510             newOut.addAnswer(in, rec);
   1511         } catch (final IOException e) {
   1512             newOut.setFlags(newOut.getFlags() | DNSConstants.FLAGS_TC);
   1513             newOut.setId(in.getId());
   1514             send(newOut);
   1515 
   1516             newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
   1517             newOut.addAnswer(in, rec);
   1518         }
   1519         return newOut;
   1520     }
   1521 
   1522     /**
   1523      * Send an outgoing multicast DNS message.
   1524      *
   1525      * @param out
   1526      * @exception IOException
   1527      */
   1528     public void send(DNSOutgoing out) throws IOException {
   1529         if (!out.isEmpty()) {
   1530             byte[] message = out.data();
   1531             final DatagramPacket packet = new DatagramPacket(message, message.length, _group, DNSConstants.MDNS_PORT);
   1532 
   1533             if (logger.isLoggable(Level.FINEST)) {
   1534                 try {
   1535                     final DNSIncoming msg = new DNSIncoming(packet);
   1536                     if (logger.isLoggable(Level.FINEST)) {
   1537                         logger.finest("send(" + this.getName() + ") JmDNS out:" + msg.print(true));
   1538                     }
   1539                 } catch (final IOException e) {
   1540                     logger.throwing(getClass().toString(), "send(" + this.getName() + ") - JmDNS can not parse what it sends!!!", e);
   1541                 }
   1542             }
   1543             final MulticastSocket ms = _socket;
   1544             if (ms != null && !ms.isClosed()) {
   1545                 ms.send(packet);
   1546             }
   1547         }
   1548     }
   1549 
   1550     /*
   1551      * (non-Javadoc)
   1552      * @see javax.jmdns.impl.DNSTaskStarter#purgeTimer()
   1553      */
   1554     @Override
   1555     public void purgeTimer() {
   1556         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeTimer();
   1557     }
   1558 
   1559     /*
   1560      * (non-Javadoc)
   1561      * @see javax.jmdns.impl.DNSTaskStarter#purgeStateTimer()
   1562      */
   1563     @Override
   1564     public void purgeStateTimer() {
   1565         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeStateTimer();
   1566     }
   1567 
   1568     /*
   1569      * (non-Javadoc)
   1570      * @see javax.jmdns.impl.DNSTaskStarter#cancelTimer()
   1571      */
   1572     @Override
   1573     public void cancelTimer() {
   1574         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelTimer();
   1575     }
   1576 
   1577     /*
   1578      * (non-Javadoc)
   1579      * @see javax.jmdns.impl.DNSTaskStarter#cancelStateTimer()
   1580      */
   1581     @Override
   1582     public void cancelStateTimer() {
   1583         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelStateTimer();
   1584     }
   1585 
   1586     /*
   1587      * (non-Javadoc)
   1588      * @see javax.jmdns.impl.DNSTaskStarter#startProber()
   1589      */
   1590     @Override
   1591     public void startProber() {
   1592         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startProber();
   1593     }
   1594 
   1595     /*
   1596      * (non-Javadoc)
   1597      * @see javax.jmdns.impl.DNSTaskStarter#startAnnouncer()
   1598      */
   1599     @Override
   1600     public void startAnnouncer() {
   1601         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startAnnouncer();
   1602     }
   1603 
   1604     /*
   1605      * (non-Javadoc)
   1606      * @see javax.jmdns.impl.DNSTaskStarter#startRenewer()
   1607      */
   1608     @Override
   1609     public void startRenewer() {
   1610         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startRenewer();
   1611     }
   1612 
   1613     /*
   1614      * (non-Javadoc)
   1615      * @see javax.jmdns.impl.DNSTaskStarter#startCanceler()
   1616      */
   1617     @Override
   1618     public void startCanceler() {
   1619         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startCanceler();
   1620     }
   1621 
   1622     /*
   1623      * (non-Javadoc)
   1624      * @see javax.jmdns.impl.DNSTaskStarter#startReaper()
   1625      */
   1626     @Override
   1627     public void startReaper() {
   1628         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startReaper();
   1629     }
   1630 
   1631     /*
   1632      * (non-Javadoc)
   1633      * @see javax.jmdns.impl.DNSTaskStarter#startServiceInfoResolver(javax.jmdns.impl.ServiceInfoImpl)
   1634      */
   1635     @Override
   1636     public void startServiceInfoResolver(ServiceInfoImpl info) {
   1637         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceInfoResolver(info);
   1638     }
   1639 
   1640     /*
   1641      * (non-Javadoc)
   1642      * @see javax.jmdns.impl.DNSTaskStarter#startTypeResolver()
   1643      */
   1644     @Override
   1645     public void startTypeResolver() {
   1646         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startTypeResolver();
   1647     }
   1648 
   1649     /*
   1650      * (non-Javadoc)
   1651      * @see javax.jmdns.impl.DNSTaskStarter#startServiceResolver(java.lang.String)
   1652      */
   1653     @Override
   1654     public void startServiceResolver(String type) {
   1655         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceResolver(type);
   1656     }
   1657 
   1658     /*
   1659      * (non-Javadoc)
   1660      * @see javax.jmdns.impl.DNSTaskStarter#startResponder(javax.jmdns.impl.DNSIncoming, int)
   1661      */
   1662     @Override
   1663     public void startResponder(DNSIncoming in, int port) {
   1664         DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startResponder(in, port);
   1665     }
   1666 
   1667     // REMIND: Why is this not an anonymous inner class?
   1668     /**
   1669      * Shutdown operations.
   1670      */
   1671     protected class Shutdown implements Runnable {
   1672         /** {@inheritDoc} */
   1673         @Override
   1674         public void run() {
   1675             try {
   1676                 _shutdown = null;
   1677                 close();
   1678             } catch (Throwable exception) {
   1679                 System.err.println("Error while shuting down. " + exception);
   1680             }
   1681         }
   1682     }
   1683 
   1684     private final Object _recoverLock = new Object();
   1685 
   1686     /**
   1687      * Recover jmdns when there is an error.
   1688      */
   1689     public void recover() {
   1690         logger.finer(this.getName() + "recover()");
   1691         // We have an IO error so lets try to recover if anything happens lets close it.
   1692         // This should cover the case of the IP address changing under our feet
   1693         if (this.isClosing() || this.isClosed() || this.isCanceling() || this.isCanceled()) {
   1694             return;
   1695         }
   1696 
   1697         // We need some definite lock here as we may have multiple timer running in the same thread that will not be stopped by the reentrant lock
   1698         // in the state object. This is only a problem in this case as we are going to execute in seperate thread so that the timer can clear.
   1699         synchronized (_recoverLock) {
   1700             // Stop JmDNS
   1701             // This protects against recursive calls
   1702             if (this.cancelState()) {
   1703                 logger.finer(this.getName() + "recover() thread " + Thread.currentThread().getName());
   1704                 Thread recover = new Thread(this.getName() + ".recover()") {
   1705                     /**
   1706                      * {@inheritDoc}
   1707                      */
   1708                     @Override
   1709                     public void run() {
   1710                         __recover();
   1711                     }
   1712                 };
   1713                 recover.start();
   1714             }
   1715         }
   1716     }
   1717 
   1718     void __recover() {
   1719         // Synchronize only if we are not already in process to prevent dead locks
   1720         //
   1721         if (logger.isLoggable(Level.FINER)) {
   1722             logger.finer(this.getName() + "recover() Cleanning up");
   1723         }
   1724 
   1725         logger.warning("RECOVERING");
   1726         // Purge the timer
   1727         this.purgeTimer();
   1728 
   1729         // We need to keep a copy for reregistration
   1730         final Collection<ServiceInfo> oldServiceInfos = new ArrayList<ServiceInfo>(getServices().values());
   1731 
   1732         // Cancel all services
   1733         this.unregisterAllServices();
   1734         this.disposeServiceCollectors();
   1735 
   1736         this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
   1737 
   1738         // Purge the canceler timer
   1739         this.purgeStateTimer();
   1740 
   1741         //
   1742         // close multicast socket
   1743         this.closeMulticastSocket();
   1744 
   1745         //
   1746         this.getCache().clear();
   1747         if (logger.isLoggable(Level.FINER)) {
   1748             logger.finer(this.getName() + "recover() All is clean");
   1749         }
   1750 
   1751         if (this.isCanceled()) {
   1752             //
   1753             // All is clear now start the services
   1754             //
   1755             for (ServiceInfo info : oldServiceInfos) {
   1756                 ((ServiceInfoImpl) info).recoverState();
   1757             }
   1758             this.recoverState();
   1759 
   1760             try {
   1761                 this.openMulticastSocket(this.getLocalHost());
   1762                 this.start(oldServiceInfos);
   1763             } catch (final Exception exception) {
   1764                 logger.log(Level.WARNING, this.getName() + "recover() Start services exception ", exception);
   1765             }
   1766             logger.log(Level.WARNING, this.getName() + "recover() We are back!");
   1767         } else {
   1768             // We have a problem. We could not clear the state.
   1769             logger.log(Level.WARNING, this.getName() + "recover() Could not recover we are Down!");
   1770             if (this.getDelegate() != null) {
   1771                 this.getDelegate().cannotRecoverFromIOError(this.getDns(), oldServiceInfos);
   1772             }
   1773         }
   1774 
   1775     }
   1776 
   1777     public void cleanCache() {
   1778         long now = System.currentTimeMillis();
   1779         for (DNSEntry entry : this.getCache().allValues()) {
   1780             try {
   1781                 DNSRecord record = (DNSRecord) entry;
   1782                 if (record.isExpired(now)) {
   1783                     this.updateRecord(now, record, Operation.Remove);
   1784                     this.getCache().removeDNSEntry(record);
   1785                 } else if (record.isStale(now)) {
   1786                     // we should query for the record we care about i.e. those in the service collectors
   1787                     this.renewServiceCollector(record);
   1788                 }
   1789             } catch (Exception exception) {
   1790                 logger.log(Level.SEVERE, this.getName() + ".Error while reaping records: " + entry, exception);
   1791                 logger.severe(this.toString());
   1792             }
   1793         }
   1794     }
   1795 
   1796     /**
   1797      * {@inheritDoc}
   1798      */
   1799     @Override
   1800     public void close() {
   1801         if (this.isClosing()) {
   1802             return;
   1803         }
   1804 
   1805         if (logger.isLoggable(Level.FINER)) {
   1806             logger.finer("Cancelling JmDNS: " + this);
   1807         }
   1808         // Stop JmDNS
   1809         // This protects against recursive calls
   1810         if (this.closeState()) {
   1811             // We got the tie break now clean up
   1812 
   1813             // Stop the timer
   1814             logger.finer("Canceling the timer");
   1815             this.cancelTimer();
   1816 
   1817             // Cancel all services
   1818             this.unregisterAllServices();
   1819             this.disposeServiceCollectors();
   1820 
   1821             if (logger.isLoggable(Level.FINER)) {
   1822                 logger.finer("Wait for JmDNS cancel: " + this);
   1823             }
   1824             this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
   1825 
   1826             // Stop the canceler timer
   1827             logger.finer("Canceling the state timer");
   1828             this.cancelStateTimer();
   1829 
   1830             // Stop the executor
   1831             _executor.shutdown();
   1832 
   1833             // close socket
   1834             this.closeMulticastSocket();
   1835 
   1836             // remove the shutdown hook
   1837             if (_shutdown != null) {
   1838                 Runtime.getRuntime().removeShutdownHook(_shutdown);
   1839             }
   1840 
   1841             if (logger.isLoggable(Level.FINER)) {
   1842                 logger.finer("JmDNS closed.");
   1843             }
   1844         }
   1845         advanceState(null);
   1846     }
   1847 
   1848     /**
   1849      * {@inheritDoc}
   1850      */
   1851     @Override
   1852     @Deprecated
   1853     public void printServices() {
   1854         System.err.println(toString());
   1855     }
   1856 
   1857     /**
   1858      * {@inheritDoc}
   1859      */
   1860     @Override
   1861     public String toString() {
   1862         final StringBuilder aLog = new StringBuilder(2048);
   1863         aLog.append("\t---- Local Host -----");
   1864         aLog.append("\n\t");
   1865         aLog.append(_localHost);
   1866         aLog.append("\n\t---- Services -----");
   1867         for (String key : _services.keySet()) {
   1868             aLog.append("\n\t\tService: ");
   1869             aLog.append(key);
   1870             aLog.append(": ");
   1871             aLog.append(_services.get(key));
   1872         }
   1873         aLog.append("\n");
   1874         aLog.append("\t---- Types ----");
   1875         for (String key : _serviceTypes.keySet()) {
   1876             ServiceTypeEntry subtypes = _serviceTypes.get(key);
   1877             aLog.append("\n\t\tType: ");
   1878             aLog.append(subtypes.getType());
   1879             aLog.append(": ");
   1880             aLog.append(subtypes.isEmpty() ? "no subtypes" : subtypes);
   1881         }
   1882         aLog.append("\n");
   1883         aLog.append(_cache.toString());
   1884         aLog.append("\n");
   1885         aLog.append("\t---- Service Collectors ----");
   1886         for (String key : _serviceCollectors.keySet()) {
   1887             aLog.append("\n\t\tService Collector: ");
   1888             aLog.append(key);
   1889             aLog.append(": ");
   1890             aLog.append(_serviceCollectors.get(key));
   1891         }
   1892         aLog.append("\n");
   1893         aLog.append("\t---- Service Listeners ----");
   1894         for (String key : _serviceListeners.keySet()) {
   1895             aLog.append("\n\t\tService Listener: ");
   1896             aLog.append(key);
   1897             aLog.append(": ");
   1898             aLog.append(_serviceListeners.get(key));
   1899         }
   1900         return aLog.toString();
   1901     }
   1902 
   1903     /**
   1904      * {@inheritDoc}
   1905      */
   1906     @Override
   1907     public ServiceInfo[] list(String type) {
   1908         return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
   1909     }
   1910 
   1911     /**
   1912      * {@inheritDoc}
   1913      */
   1914     @Override
   1915     public ServiceInfo[] list(String type, long timeout) {
   1916         this.cleanCache();
   1917         // Implementation note: The first time a list for a given type is
   1918         // requested, a ServiceCollector is created which collects service
   1919         // infos. This greatly speeds up the performance of subsequent calls
   1920         // to this method. The caveats are, that 1) the first call to this
   1921         // method for a given type is slow, and 2) we spawn a ServiceCollector
   1922         // instance for each service type which increases network traffic a
   1923         // little.
   1924 
   1925         String loType = type.toLowerCase();
   1926 
   1927         boolean newCollectorCreated = false;
   1928         if (this.isCanceling() || this.isCanceled()) {
   1929             return new ServiceInfo[0];
   1930         }
   1931 
   1932         ServiceCollector collector = _serviceCollectors.get(loType);
   1933         if (collector == null) {
   1934             newCollectorCreated = _serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null;
   1935             collector = _serviceCollectors.get(loType);
   1936             if (newCollectorCreated) {
   1937                 this.addServiceListener(type, collector, ListenerStatus.SYNCHONEOUS);
   1938             }
   1939         }
   1940         if (logger.isLoggable(Level.FINER)) {
   1941             logger.finer(this.getName() + ".collector: " + collector);
   1942         }
   1943         // At this stage the collector should never be null but it keeps findbugs happy.
   1944         return (collector != null ? collector.list(timeout) : new ServiceInfo[0]);
   1945     }
   1946 
   1947     /**
   1948      * {@inheritDoc}
   1949      */
   1950     @Override
   1951     public Map<String, ServiceInfo[]> listBySubtype(String type) {
   1952         return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT);
   1953     }
   1954 
   1955     /**
   1956      * {@inheritDoc}
   1957      */
   1958     @Override
   1959     public Map<String, ServiceInfo[]> listBySubtype(String type, long timeout) {
   1960         Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5);
   1961         for (ServiceInfo info : this.list(type, timeout)) {
   1962             String subtype = info.getSubtype().toLowerCase();
   1963             if (!map.containsKey(subtype)) {
   1964                 map.put(subtype, new ArrayList<ServiceInfo>(10));
   1965             }
   1966             map.get(subtype).add(info);
   1967         }
   1968 
   1969         Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size());
   1970         for (String subtype : map.keySet()) {
   1971             List<ServiceInfo> infoForSubType = map.get(subtype);
   1972             result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()]));
   1973         }
   1974 
   1975         return result;
   1976     }
   1977 
   1978     /**
   1979      * This method disposes all ServiceCollector instances which have been created by calls to method <code>list(type)</code>.
   1980      *
   1981      * @see #list
   1982      */
   1983     private void disposeServiceCollectors() {
   1984         if (logger.isLoggable(Level.FINER)) {
   1985             logger.finer("disposeServiceCollectors()");
   1986         }
   1987         for (String type : _serviceCollectors.keySet()) {
   1988             ServiceCollector collector = _serviceCollectors.get(type);
   1989             if (collector != null) {
   1990                 this.removeServiceListener(type, collector);
   1991                 _serviceCollectors.remove(type, collector);
   1992             }
   1993         }
   1994     }
   1995 
   1996     /**
   1997      * Instances of ServiceCollector are used internally to speed up the performance of method <code>list(type)</code>.
   1998      *
   1999      * @see #list
   2000      */
   2001     private static class ServiceCollector implements ServiceListener {
   2002         // private static Logger logger = Logger.getLogger(ServiceCollector.class.getName());
   2003 
   2004         /**
   2005          * A set of collected service instance names.
   2006          */
   2007         private final ConcurrentMap<String, ServiceInfo>  _infos;
   2008 
   2009         /**
   2010          * A set of collected service event waiting to be resolved.
   2011          */
   2012         private final ConcurrentMap<String, ServiceEvent> _events;
   2013 
   2014         /**
   2015          * This is the type we are listening for (only used for debugging).
   2016          */
   2017         private final String                              _type;
   2018 
   2019         /**
   2020          * This is used to force a wait on the first invocation of list.
   2021          */
   2022         private volatile boolean                          _needToWaitForInfos;
   2023 
   2024         public ServiceCollector(String type) {
   2025             super();
   2026             _infos = new ConcurrentHashMap<String, ServiceInfo>();
   2027             _events = new ConcurrentHashMap<String, ServiceEvent>();
   2028             _type = type;
   2029             _needToWaitForInfos = true;
   2030         }
   2031 
   2032         /**
   2033          * A service has been added.
   2034          *
   2035          * @param event
   2036          *            service event
   2037          */
   2038         @Override
   2039         public void serviceAdded(ServiceEvent event) {
   2040             synchronized (this) {
   2041                 ServiceInfo info = event.getInfo();
   2042                 if ((info != null) && (info.hasData())) {
   2043                     _infos.put(event.getName(), info);
   2044                 } else {
   2045                     String subtype = (info != null ? info.getSubtype() : "");
   2046                     info = ((JmDNSImpl) event.getDNS()).resolveServiceInfo(event.getType(), event.getName(), subtype, true);
   2047                     if (info != null) {
   2048                         _infos.put(event.getName(), info);
   2049                     } else {
   2050                         _events.put(event.getName(), event);
   2051                     }
   2052                 }
   2053             }
   2054         }
   2055 
   2056         /**
   2057          * A service has been removed.
   2058          *
   2059          * @param event
   2060          *            service event
   2061          */
   2062         @Override
   2063         public void serviceRemoved(ServiceEvent event) {
   2064             synchronized (this) {
   2065                 _infos.remove(event.getName());
   2066                 _events.remove(event.getName());
   2067             }
   2068         }
   2069 
   2070         /**
   2071          * A service has been resolved. Its details are now available in the ServiceInfo record.
   2072          *
   2073          * @param event
   2074          *            service event
   2075          */
   2076         @Override
   2077         public void serviceResolved(ServiceEvent event) {
   2078             synchronized (this) {
   2079                 _infos.put(event.getName(), event.getInfo());
   2080                 _events.remove(event.getName());
   2081             }
   2082         }
   2083 
   2084         /**
   2085          * Returns an array of all service infos which have been collected by this ServiceCollector.
   2086          *
   2087          * @param timeout
   2088          *            timeout if the info list is empty.
   2089          * @return Service Info array
   2090          */
   2091         public ServiceInfo[] list(long timeout) {
   2092             if (_infos.isEmpty() || !_events.isEmpty() || _needToWaitForInfos) {
   2093                 long loops = (timeout / 200L);
   2094                 if (loops < 1) {
   2095                     loops = 1;
   2096                 }
   2097                 for (int i = 0; i < loops; i++) {
   2098                     try {
   2099                         Thread.sleep(200);
   2100                     } catch (final InterruptedException e) {
   2101                         /* Stub */
   2102                     }
   2103                     if (_events.isEmpty() && !_infos.isEmpty() && !_needToWaitForInfos) {
   2104                         break;
   2105                     }
   2106                 }
   2107             }
   2108             _needToWaitForInfos = false;
   2109             return _infos.values().toArray(new ServiceInfo[_infos.size()]);
   2110         }
   2111 
   2112         /**
   2113          * {@inheritDoc}
   2114          */
   2115         @Override
   2116         public String toString() {
   2117             final StringBuffer aLog = new StringBuffer();
   2118             aLog.append("\n\tType: ");
   2119             aLog.append(_type);
   2120             if (_infos.isEmpty()) {
   2121                 aLog.append("\n\tNo services collected.");
   2122             } else {
   2123                 aLog.append("\n\tServices");
   2124                 for (String key : _infos.keySet()) {
   2125                     aLog.append("\n\t\tService: ");
   2126                     aLog.append(key);
   2127                     aLog.append(": ");
   2128                     aLog.append(_infos.get(key));
   2129                 }
   2130             }
   2131             if (_events.isEmpty()) {
   2132                 aLog.append("\n\tNo event queued.");
   2133             } else {
   2134                 aLog.append("\n\tEvents");
   2135                 for (String key : _events.keySet()) {
   2136                     aLog.append("\n\t\tEvent: ");
   2137                     aLog.append(key);
   2138                     aLog.append(": ");
   2139                     aLog.append(_events.get(key));
   2140                 }
   2141             }
   2142             return aLog.toString();
   2143         }
   2144     }
   2145 
   2146     static String toUnqualifiedName(String type, String qualifiedName) {
   2147         String loType = type.toLowerCase();
   2148         String loQualifiedName = qualifiedName.toLowerCase();
   2149         if (loQualifiedName.endsWith(loType) && !(loQualifiedName.equals(loType))) {
   2150             return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
   2151         }
   2152         return qualifiedName;
   2153     }
   2154 
   2155     public Map<String, ServiceInfo> getServices() {
   2156         return _services;
   2157     }
   2158 
   2159     public void setLastThrottleIncrement(long lastThrottleIncrement) {
   2160         this._lastThrottleIncrement = lastThrottleIncrement;
   2161     }
   2162 
   2163     public long getLastThrottleIncrement() {
   2164         return _lastThrottleIncrement;
   2165     }
   2166 
   2167     public void setThrottle(int throttle) {
   2168         this._throttle = throttle;
   2169     }
   2170 
   2171     public int getThrottle() {
   2172         return _throttle;
   2173     }
   2174 
   2175     public static Random getRandom() {
   2176         return _random;
   2177     }
   2178 
   2179     public void ioLock() {
   2180         _ioLock.lock();
   2181     }
   2182 
   2183     public void ioUnlock() {
   2184         _ioLock.unlock();
   2185     }
   2186 
   2187     public void setPlannedAnswer(DNSIncoming plannedAnswer) {
   2188         this._plannedAnswer = plannedAnswer;
   2189     }
   2190 
   2191     public DNSIncoming getPlannedAnswer() {
   2192         return _plannedAnswer;
   2193     }
   2194 
   2195     void setLocalHost(HostInfo localHost) {
   2196         this._localHost = localHost;
   2197     }
   2198 
   2199     public Map<String, ServiceTypeEntry> getServiceTypes() {
   2200         return _serviceTypes;
   2201     }
   2202 
   2203     public MulticastSocket getSocket() {
   2204         return _socket;
   2205     }
   2206 
   2207     public InetAddress getGroup() {
   2208         return _group;
   2209     }
   2210 
   2211     @Override
   2212     public Delegate getDelegate() {
   2213         return this._delegate;
   2214     }
   2215 
   2216     @Override
   2217     public Delegate setDelegate(Delegate delegate) {
   2218         Delegate previous = this._delegate;
   2219         this._delegate = delegate;
   2220         return previous;
   2221     }
   2222 
   2223 }
   2224