// /Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceInfo.Fields;
import javax.jmdns.ServiceListener;
import javax.jmdns.ServiceTypeListener;
import javax.jmdns.impl.ListenerStatus.ServiceListenerStatus;
import javax.jmdns.impl.ListenerStatus.ServiceTypeListenerStatus;
import javax.jmdns.impl.constants.DNSConstants;
import javax.jmdns.impl.constants.DNSRecordClass;
import javax.jmdns.impl.constants.DNSRecordType;
import javax.jmdns.impl.constants.DNSState;
import javax.jmdns.impl.tasks.DNSTask;
import javax.jmdns.impl.tasks.RecordReaper;
import javax.jmdns.impl.util.NamedThreadFactory;

// REMIND: multiple IP addresses

/**
 * mDNS implementation in Java.
 *
 * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Scott Lewis, Kai Kreuzer, Victor Toni
 */
public class JmDNSImpl extends JmDNS implements DNSStatefulObject, DNSTaskStarter {
    private static Logger logger = LoggerFactory.getLogger(JmDNSImpl.class.getName());

    public enum Operation {
        Remove, Update, Add, RegisterServiceType, Noop
    }

    /**
     * This is the multicast group, we are listening to for multicast DNS messages.
     */
    private volatile InetAddress     _group;
    /**
     * This is our multicast socket.
     */
    private volatile MulticastSocket _socket;

    /**
     * Holds instances of JmDNS.DNSListener. Must by a synchronized collection, because it is updated from concurrent threads.
     */
    private final List<DNSListener> _listeners;

    /**
     * Holds instances of ServiceListener's. Keys are Strings holding a fully qualified service type. Values are LinkedList's of ServiceListener's.
     */
    /* default */ final ConcurrentMap<String, List<ServiceListenerStatus>> _serviceListeners;

    /**
     * Holds instances of ServiceTypeListener's.
     */
    private final Set<ServiceTypeListenerStatus> _typeListeners;

    /**
     * Cache for DNSEntry's.
     */
    private final DNSCache _cache;

    /**
     * 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.
     */
    private final ConcurrentMap<String, ServiceInfo> _services;

    /**
     * This hashtable holds the service types that have been registered or that have been received in an incoming datagram.<br/>
     * Keys are instances of String which hold an all lower-case version of the fully qualified service type.<br/>
     * Values hold the fully qualified service type.
     */
    private final ConcurrentMap<String, ServiceTypeEntry> _serviceTypes;

    private volatile Delegate _delegate;

    /**
     * This is used to store type entries. The type is stored as a call variable and the map support the subtypes.
     * <p>
     * The key is the lowercase version as the value is the case preserved version.
     * </p>
     */
    public static class ServiceTypeEntry extends AbstractMap<String, String>implements Cloneable {

        private final Set<Map.Entry<String, String>> _entrySet;

        private final String _type;

        private static class SubTypeEntry implements Entry<String, String>, java.io.Serializable, Cloneable {

            private static final long serialVersionUID = 9188503522395855322L;

            private final String _key;
            private final String _value;

            public SubTypeEntry(String subtype) {
                super();
                _value = (subtype != null ? subtype : "");
                _key = _value.toLowerCase();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public String getKey() {
                return _key;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public String getValue() {
                return _value;
            }

            /**
             * 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.
             *
             * @param value
             *            new value to be stored in this entry
             * @return (Does not return)
             * @exception UnsupportedOperationException
             *                always
             */
            @Override
            public String setValue(String value) {
                throw new UnsupportedOperationException();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean equals(Object entry) {
                if (!(entry instanceof Map.Entry)) {
                    return false;
                }
                return this.getKey().equals(((Map.Entry<?, ?>) entry).getKey()) && this.getValue().equals(((Map.Entry<?, ?>) entry).getValue());
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public int hashCode() {
                return (_key == null ? 0 : _key.hashCode()) ^ (_value == null ? 0 : _value.hashCode());
            }

            /*
             * (non-Javadoc)
             * @see java.lang.Object#clone()
             */
            @Override
            public SubTypeEntry clone() {
                // Immutable object
                return this;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public String toString() {
                return _key + "=" + _value;
            }

        }

        public ServiceTypeEntry(String type) {
            super();
            this._type = type;
            this._entrySet = new HashSet<Map.Entry<String, String>>();
        }

        /**
         * The type associated with this entry.
         *
         * @return the type
         */
        public String getType() {
            return _type;
        }

        /*
         * (non-Javadoc)
         * @see java.util.AbstractMap#entrySet()
         */
        @Override
        public Set<Map.Entry<String, String>> entrySet() {
            return _entrySet;
        }

        /**
         * 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
         * <code>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</code>.
         *
         * @param subtype
         *            element whose presence in this set is to be tested
         * @return <code>true</code> if this set contains the specified element
         */
        public boolean contains(String subtype) {
            return subtype != null && this.containsKey(subtype.toLowerCase());
        }

        /**
         * 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
         * <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>.
         *
         * @param subtype
         *            element to be added to this set
         * @return <code>true</code> if this set did not already contain the specified element
         */
        public boolean add(String subtype) {
            if (subtype == null || this.contains(subtype)) {
                return false;
            }
            _entrySet.add(new SubTypeEntry(subtype));
            return true;
        }

        /**
         * 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).
         *
         * @return an iterator over the elements in this set
         */
        public Iterator<String> iterator() {
            return this.keySet().iterator();
        }

        /*
         * (non-Javadoc)
         * @see java.util.AbstractMap#clone()
         */
        @Override
        public ServiceTypeEntry clone() {
            ServiceTypeEntry entry = new ServiceTypeEntry(this.getType());
            for (Map.Entry<String, String> subTypeEntry : this.entrySet()) {
                entry.add(subTypeEntry.getValue());
            }
            return entry;
        }

        /*
         * (non-Javadoc)
         * @see java.util.AbstractMap#toString()
         */
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder(200);
            if (this.isEmpty()) {
                sb.append("empty");
            } else {
                for (String value : this.values()) {
                    sb.append(value);
                    sb.append(", ");
                }
                sb.setLength(sb.length() - 2);
            }
            return sb.toString();
        }

    }

    /**
     * This is the shutdown hook, we registered with the java runtime.
     */
    protected Thread _shutdown;

    /**
     * Handle on the local host
     */
    private HostInfo _localHost;

    private Thread _incomingListener;

    /**
     * Throttle count. This is used to count the overall number of probes sent by JmDNS. When the last throttle increment happened .
     */
    private int _throttle;

    /**
     * Last throttle increment.
     */
    private long _lastThrottleIncrement;

    private final ExecutorService _executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("JmDNS"));

    //
    // 2009-09-16 ldeck: adding docbug patch with slight ammendments
    // 'Fixes two deadlock conditions involving JmDNS.close() - ID: 1473279'
    //
    // ---------------------------------------------------
    /**
     * 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
     * the timer thread so we can announce. (Patch from docbug in 2006-04-19 still wasn't patched .. so I'm doing it!)
     */
    // private final Timer _cancelerTimer;
    // ---------------------------------------------------

    /**
     * The source for random values. This is used to introduce random delays in responses. This reduces the potential for collisions on the network.
     */
    private final static Random _random = new Random();

    /**
     * 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.
     */
    private final ReentrantLock _ioLock = new ReentrantLock();

    /**
     * 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/>
     * FIXME [PJYF June 8 2010]: This does not work well with multiple planned answers for packages that came in from different clients.
     */
    private DNSIncoming _plannedAnswer;

    // State machine

    /**
     * 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.
     *
     * @see #list
     */
    private final ConcurrentMap<String, ServiceCollector> _serviceCollectors;

    private final String _name;

    /**
     * Main method to display API information if run from java -jar
     *
     * @param argv
     *            the command line arguments
     */
    public static void main(String[] argv) {
        String version = null;
        try {
            final Properties pomProperties = new Properties();
            pomProperties.load(JmDNSImpl.class.getResourceAsStream("/META-INF/maven/javax.jmdns/jmdns/pom.properties"));
            version = pomProperties.getProperty("version");
        } catch (Exception e) {
            version = "RUNNING.IN.IDE.FULL";
        }
        System.out.println("JmDNS version \"" + version + "\"");
        System.out.println(" ");

        System.out.println("Running on java version \"" + System.getProperty("java.version") + "\"" + " (build " + System.getProperty("java.runtime.version") + ")" + " from " + System.getProperty("java.vendor"));

        System.out.println("Operating environment \"" + System.getProperty("os.name") + "\"" + " version " + System.getProperty("os.version") + " on " + System.getProperty("os.arch"));

        System.out.println("For more information on JmDNS please visit http://jmdns.org");
    }

    /**
     * Create an instance of JmDNS and bind it to a specific network interface given its IP-address.
     *
     * @param address
     *            IP address to bind to.
     * @param name
     *            name of the newly created JmDNS
     * @exception IOException
     */
    public JmDNSImpl(InetAddress address, String name) throws IOException {
        super();
        logger.debug("JmDNS instance created");

        _cache = new DNSCache(100);

        _listeners = Collections.synchronizedList(new ArrayList<DNSListener>());
        _serviceListeners = new ConcurrentHashMap<String, List<ServiceListenerStatus>>();
        _typeListeners = Collections.synchronizedSet(new HashSet<ServiceTypeListenerStatus>());
        _serviceCollectors = new ConcurrentHashMap<String, ServiceCollector>();

        _services = new ConcurrentHashMap<String, ServiceInfo>(20);
        _serviceTypes = new ConcurrentHashMap<String, ServiceTypeEntry>(20);

        _localHost = HostInfo.newHostInfo(address, this, name);
        _name = (name != null ? name : _localHost.getName());

        // _cancelerTimer = new Timer("JmDNS.cancelerTimer");

        // (ldeck 2.1.1) preventing shutdown blocking thread
        // -------------------------------------------------
        // _shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
        // Runtime.getRuntime().addShutdownHook(_shutdown);

        // -------------------------------------------------

        // Bind to multicast socket
        this.openMulticastSocket(this.getLocalHost());
        this.start(this.getServices().values());

        this.startReaper();
    }

    private void start(Collection<? extends ServiceInfo> serviceInfos) {
        if (_incomingListener == null) {
            _incomingListener = new SocketListener(this);
            _incomingListener.start();
        }
        this.startProber();
        for (ServiceInfo info : serviceInfos) {
            try {
                this.registerService(new ServiceInfoImpl(info));
            } catch (final Exception exception) {
                logger.warn("start() Registration exception ", exception);
            }
        }
    }

    private void openMulticastSocket(HostInfo hostInfo) throws IOException {
        if (_group == null) {
            if (hostInfo.getInetAddress() instanceof Inet6Address) {
                _group = InetAddress.getByName(DNSConstants.MDNS_GROUP_IPV6);
            } else {
                _group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
            }
        }
        if (_socket != null) {
            this.closeMulticastSocket();
        }
        // SocketAddress address = new InetSocketAddress((hostInfo != null ? hostInfo.getInetAddress() : null), DNSConstants.MDNS_PORT);
        // System.out.println("Socket Address: " + address);
        // try {
        // _socket = new MulticastSocket(address);
        // } catch (Exception exception) {
        // logger.warn("openMulticastSocket() Open socket exception Address: " + address + ", ", exception);
        // // The most likely cause is a duplicate address lets open without specifying the address
        // _socket = new MulticastSocket(DNSConstants.MDNS_PORT);
        // }
        _socket = new MulticastSocket(DNSConstants.MDNS_PORT);
        if ((hostInfo != null) && (hostInfo.getInterface() != null)) {
            final SocketAddress multicastAddr = new InetSocketAddress(_group, DNSConstants.MDNS_PORT);
            _socket.setNetworkInterface(hostInfo.getInterface());

            logger.trace("Trying to joinGroup({}, {})", multicastAddr, hostInfo.getInterface());

            // this joinGroup() might be less surprisingly so this is the default
            _socket.joinGroup(multicastAddr, hostInfo.getInterface());
        } else {
            logger.trace("Trying to joinGroup({})", _group);
            _socket.joinGroup(_group);
        }

        _socket.setTimeToLive(255);
    }

    private void closeMulticastSocket() {
        // jP: 20010-01-18. See below. We'll need this monitor...
        // assert (Thread.holdsLock(this));
        logger.debug("closeMulticastSocket()");
        if (_socket != null) {
            // close socket
            try {
                try {
                    _socket.leaveGroup(_group);
                } catch (SocketException exception) {
                    //
                }
                _socket.close();
                // jP: 20010-01-18. It isn't safe to join() on the listener
                // thread - it attempts to lock the IoLock object, and deadlock
                // ensues. Per issue #2933183, changed this to wait on the JmDNS
                // monitor, checking on each notify (or timeout) that the
                // listener thread has stopped.
                //
                while (_incomingListener != null && _incomingListener.isAlive()) {
                    synchronized (this) {
                        try {
                            if (_incomingListener != null && _incomingListener.isAlive()) {
                                // wait time is arbitrary, we're really expecting notification.
                                logger.debug("closeMulticastSocket(): waiting for jmDNS monitor");
                                this.wait(1000);
                            }
                        } catch (InterruptedException ignored) {
                            // Ignored
                        }
                    }
                }
                _incomingListener = null;
            } catch (final Exception exception) {
                logger.warn("closeMulticastSocket() Close socket exception ", exception);
            }
            _socket = null;
        }
    }

    // State machine
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean advanceState(DNSTask task) {
        return this._localHost.advanceState(task);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean revertState() {
        return this._localHost.revertState();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean cancelState() {
        return this._localHost.cancelState();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean closeState() {
        return this._localHost.closeState();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean recoverState() {
        return this._localHost.recoverState();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JmDNSImpl getDns() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void associateWithTask(DNSTask task, DNSState state) {
        this._localHost.associateWithTask(task, state);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAssociationWithTask(DNSTask task) {
        this._localHost.removeAssociationWithTask(task);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
        return this._localHost.isAssociatedWithTask(task, state);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isProbing() {
        return this._localHost.isProbing();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAnnouncing() {
        return this._localHost.isAnnouncing();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAnnounced() {
        return this._localHost.isAnnounced();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCanceling() {
        return this._localHost.isCanceling();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCanceled() {
        return this._localHost.isCanceled();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isClosing() {
        return this._localHost.isClosing();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isClosed() {
        return this._localHost.isClosed();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean waitForAnnounced(long timeout) {
        return this._localHost.waitForAnnounced(timeout);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean waitForCanceled(long timeout) {
        return this._localHost.waitForCanceled(timeout);
    }

    /**
     * Return the DNSCache associated with the cache variable
     *
     * @return DNS cache
     */
    public DNSCache getCache() {
        return _cache;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getName() {
        return _name;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getHostName() {
        return _localHost.getName();
    }

    /**
     * Returns the local host info
     *
     * @return local host info
     */
    public HostInfo getLocalHost() {
        return _localHost;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public InetAddress getInetAddress() throws IOException {
        return _localHost.getInetAddress();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Deprecated
    public InetAddress getInterface() throws IOException {
        return _socket.getInterface();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo getServiceInfo(String type, String name) {
        return this.getServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo getServiceInfo(String type, String name, long timeout) {
        return this.getServiceInfo(type, name, false, timeout);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo getServiceInfo(String type, String name, boolean persistent) {
        return this.getServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo getServiceInfo(String type, String name, boolean persistent, long timeout) {
        final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
        this.waitForInfoData(info, timeout);
        return (info.hasData() ? info : null);
    }

    ServiceInfoImpl resolveServiceInfo(String type, String name, String subtype, boolean persistent) {
        this.cleanCache();
        String loType = type.toLowerCase();
        this.registerServiceType(type);
        if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
            this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHRONOUS);
        }

        // Check if the answer is in the cache.
        final ServiceInfoImpl info = this.getServiceInfoFromCache(type, name, subtype, persistent);
        // We still run the resolver to do the dispatch but if the info is already there it will quit immediately
        this.startServiceInfoResolver(info);

        return info;
    }

    ServiceInfoImpl getServiceInfoFromCache(String type, String name, String subtype, boolean persistent) {
        // Check if the answer is in the cache.
        ServiceInfoImpl info = new ServiceInfoImpl(type, name, subtype, 0, 0, 0, persistent, (byte[]) null);
        DNSEntry pointerEntry = this.getCache().getDNSEntry(new DNSRecord.Pointer(type, DNSRecordClass.CLASS_ANY, false, 0, info.getQualifiedName()));
        if (pointerEntry instanceof DNSRecord) {
            ServiceInfoImpl cachedInfo = (ServiceInfoImpl) ((DNSRecord) pointerEntry).getServiceInfo(persistent);
            if (cachedInfo != null) {
                // To get a complete info record we need to retrieve the service, address and the text bytes.

                Map<Fields, String> map = cachedInfo.getQualifiedNameMap();
                byte[] srvBytes = null;
                String server = "";
                DNSEntry serviceEntry = this.getCache().getDNSEntry(info.getQualifiedName(), DNSRecordType.TYPE_SRV, DNSRecordClass.CLASS_ANY);
                if (serviceEntry instanceof DNSRecord) {
                    ServiceInfo cachedServiceEntryInfo = ((DNSRecord) serviceEntry).getServiceInfo(persistent);
                    if (cachedServiceEntryInfo != null) {
                        cachedInfo = new ServiceInfoImpl(map, cachedServiceEntryInfo.getPort(), cachedServiceEntryInfo.getWeight(), cachedServiceEntryInfo.getPriority(), persistent, (byte[]) null);
                        srvBytes = cachedServiceEntryInfo.getTextBytes();
                        server = cachedServiceEntryInfo.getServer();
                    }
                }
                for (DNSEntry addressEntry : this.getCache().getDNSEntryList(server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_ANY)) {
                    if (addressEntry instanceof DNSRecord) {
                        ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
                        if (cachedAddressInfo != null) {
                            for (Inet4Address address : cachedAddressInfo.getInet4Addresses()) {
                                cachedInfo.addAddress(address);
                            }
                            cachedInfo._setText(cachedAddressInfo.getTextBytes());
                        }
                    }
                }
                for (DNSEntry addressEntry : this.getCache().getDNSEntryList(server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_ANY)) {
                    if (addressEntry instanceof DNSRecord) {
                        ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent);
                        if (cachedAddressInfo != null) {
                            for (Inet6Address address : cachedAddressInfo.getInet6Addresses()) {
                                cachedInfo.addAddress(address);
                            }
                            cachedInfo._setText(cachedAddressInfo.getTextBytes());
                        }
                    }
                }
                DNSEntry textEntry = this.getCache().getDNSEntry(cachedInfo.getQualifiedName(), DNSRecordType.TYPE_TXT, DNSRecordClass.CLASS_ANY);
                if (textEntry instanceof DNSRecord) {
                    ServiceInfo cachedTextInfo = ((DNSRecord) textEntry).getServiceInfo(persistent);
                    if (cachedTextInfo != null) {
                        cachedInfo._setText(cachedTextInfo.getTextBytes());
                    }
                }
                if (cachedInfo.getTextBytes().length == 0) {
                    cachedInfo._setText(srvBytes);
                }
                if (cachedInfo.hasData()) {
                    info = cachedInfo;
                }
            }
        }
        return info;
    }

    private void waitForInfoData(ServiceInfo info, long timeout) {
        synchronized (info) {
            long loops = (timeout / 200L);
            if (loops < 1) {
                loops = 1;
            }
            for (int i = 0; i < loops; i++) {
                if (info.hasData()) {
                    break;
                }
                try {
                    info.wait(200);
                } catch (final InterruptedException e) {
                    /* Stub */
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void requestServiceInfo(String type, String name) {
        this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void requestServiceInfo(String type, String name, boolean persistent) {
        this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void requestServiceInfo(String type, String name, long timeout) {
        this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void requestServiceInfo(String type, String name, boolean persistent, long timeout) {
        final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent);
        this.waitForInfoData(info, timeout);
    }

    void handleServiceResolved(ServiceEvent event) {
        List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
        final List<ServiceListenerStatus> listCopy;
        if ((list != null) && (!list.isEmpty())) {
            if ((event.getInfo() != null) && event.getInfo().hasData()) {
                final ServiceEvent localEvent = event;
                synchronized (list) {
                    listCopy = new ArrayList<ServiceListenerStatus>(list);
                }
                for (final ServiceListenerStatus listener : listCopy) {
                    _executor.submit(new Runnable() {
                        /** {@inheritDoc} */
                        @Override
                        public void run() {
                            listener.serviceResolved(localEvent);
                        }
                    });
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
        ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHRONOUS);
        _typeListeners.add(status);

        // report cached service types
        for (String type : _serviceTypes.keySet()) {
            status.serviceTypeAdded(new ServiceEventImpl(this, type, "", null));
        }

        this.startTypeResolver();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeServiceTypeListener(ServiceTypeListener listener) {
        ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHRONOUS);
        _typeListeners.remove(status);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addServiceListener(String type, ServiceListener listener) {
        this.addServiceListener(type, listener, ListenerStatus.ASYNCHRONOUS);
    }

    private void addServiceListener(String type, ServiceListener listener, boolean synch) {
        ServiceListenerStatus status = new ServiceListenerStatus(listener, synch);
        final String loType = type.toLowerCase();
        List<ServiceListenerStatus> list = _serviceListeners.get(loType);
        if (list == null) {
            if (_serviceListeners.putIfAbsent(loType, new LinkedList<ServiceListenerStatus>()) == null) {
                if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) {
                    // We have a problem here. The service collectors must be called synchronously so that their cache get cleaned up immediately or we will report .
                    this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHRONOUS);
                }
            }
            list = _serviceListeners.get(loType);
        }
        if (list != null) {
            synchronized (list) {
                if (!list.contains(status)) {
                    list.add(status);
                }
            }
        }
        // report cached service types
        final List<ServiceEvent> serviceEvents = new ArrayList<ServiceEvent>();
        Collection<DNSEntry> dnsEntryLits = this.getCache().allValues();
        for (DNSEntry entry : dnsEntryLits) {
            final DNSRecord record = (DNSRecord) entry;
            if (record.getRecordType() == DNSRecordType.TYPE_SRV) {
                if (record.getKey().endsWith(loType)) {
                    // Do not used the record embedded method for generating event this will not work.
                    // serviceEvents.add(record.getServiceEvent(this));
                    serviceEvents.add(new ServiceEventImpl(this, record.getType(), toUnqualifiedName(record.getType(), record.getName()), record.getServiceInfo()));
                }
            }
        }
        // Actually call listener with all service events added above
        for (ServiceEvent serviceEvent : serviceEvents) {
            status.serviceAdded(serviceEvent);
        }
        // Create/start ServiceResolver
        this.startServiceResolver(type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeServiceListener(String type, ServiceListener listener) {
        String loType = type.toLowerCase();
        List<ServiceListenerStatus> list = _serviceListeners.get(loType);
        if (list != null) {
            synchronized (list) {
                ServiceListenerStatus status = new ServiceListenerStatus(listener, ListenerStatus.ASYNCHRONOUS);
                list.remove(status);
                if (list.isEmpty()) {
                    _serviceListeners.remove(loType, list);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerService(ServiceInfo infoAbstract) throws IOException {
        if (this.isClosing() || this.isClosed()) {
            throw new IllegalStateException("This DNS is closed.");
        }
        final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract;

        if (info.getDns() != null) {
            if (info.getDns() != this) {
                throw new IllegalStateException("A service information can only be registered with a single instamce of JmDNS.");
            } else if (_services.get(info.getKey()) != null) {
                throw new IllegalStateException("A service information can only be registered once.");
            }
        }
        info.setDns(this);

        this.registerServiceType(info.getTypeWithSubtype());

        // bind the service to this address
        info.recoverState();
        info.setServer(_localHost.getName());
        info.addAddress(_localHost.getInet4Address());
        info.addAddress(_localHost.getInet6Address());

        this.makeServiceNameUnique(info);
        while (_services.putIfAbsent(info.getKey(), info) != null) {
            this.makeServiceNameUnique(info);
        }

        this.startProber();

        logger.debug("registerService() JmDNS registered service as {}", info);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void unregisterService(ServiceInfo infoAbstract) {
        final ServiceInfoImpl info = (ServiceInfoImpl) _services.get(infoAbstract.getKey());

        if (info != null) {
            info.cancelState();
            this.startCanceler();
            info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);

            _services.remove(info.getKey(), info);
            logger.debug("unregisterService() JmDNS {} unregistered service as {}", this.getName(), info);
        } else {
            logger.warn("{} removing unregistered service info: {}", this.getName(), infoAbstract.getKey());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void unregisterAllServices() {
        logger.debug("unregisterAllServices()");

        for (final ServiceInfo info : _services.values()) {
            if (info != null) {
                final ServiceInfoImpl infoImpl = (ServiceInfoImpl) info;
                logger.debug("Cancelling service info: {}", info);
                infoImpl.cancelState();
            }
        }
        this.startCanceler();

        for (final Map.Entry<String, ServiceInfo> entry : _services.entrySet()) {
            final ServiceInfo info = entry.getValue();
            if (info != null) {
                final ServiceInfoImpl infoImpl = (ServiceInfoImpl) info;
                final String name = entry.getKey();

                logger.debug("Wait for service info cancel: {}", info);
                infoImpl.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);
                _services.remove(name, info);
            }
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean registerServiceType(String type) {
        boolean typeAdded = false;
        Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(type);
        String domain = map.get(Fields.Domain);
        String protocol = map.get(Fields.Protocol);
        String application = map.get(Fields.Application);
        String subtype = map.get(Fields.Subtype);

        final String name = (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
        final String loname = name.toLowerCase();
        logger.debug("{} registering service type: {} as: {}{}{}",
            this.getName(),
            type,
            name,
            (subtype.length() > 0 ? " subtype: " : ""),
            (subtype.length() > 0 ? subtype : "")
        );
        if (!_serviceTypes.containsKey(loname) && !application.toLowerCase().equals("dns-sd") && !domain.toLowerCase().endsWith("in-addr.arpa") && !domain.toLowerCase().endsWith("ip6.arpa")) {
            typeAdded = _serviceTypes.putIfAbsent(loname, new ServiceTypeEntry(name)) == null;
            if (typeAdded) {
                final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
                final ServiceEvent event = new ServiceEventImpl(this, name, "", null);
                for (final ServiceTypeListenerStatus status : list) {
                    _executor.submit(new Runnable() {
                        /** {@inheritDoc} */
                        @Override
                        public void run() {
                            status.serviceTypeAdded(event);
                        }
                    });
                }
            }
        }
        if (subtype.length() > 0) {
            ServiceTypeEntry subtypes = _serviceTypes.get(loname);
            if ((subtypes != null) && (!subtypes.contains(subtype))) {
                synchronized (subtypes) {
                    if (!subtypes.contains(subtype)) {
                        typeAdded = true;
                        subtypes.add(subtype);
                        final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]);
                        final ServiceEvent event = new ServiceEventImpl(this, "_" + subtype + "._sub." + name, "", null);
                        for (final ServiceTypeListenerStatus status : list) {
                            _executor.submit(new Runnable() {
                                /** {@inheritDoc} */
                                @Override
                                public void run() {
                                    status.subTypeForServiceTypeAdded(event);
                                }
                            });
                        }
                    }
                }
            }
        }
        return typeAdded;
    }

    /**
     * Generate a possibly unique name for a service using the information we have in the cache.
     *
     * @return returns true, if the name of the service info had to be changed.
     */
    private boolean makeServiceNameUnique(ServiceInfoImpl info) {
        final String originalQualifiedName = info.getKey();
        final long now = System.currentTimeMillis();

        boolean collision;
        do {
            collision = false;

            // Check for collision in cache
            for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(info.getKey())) {
                if (DNSRecordType.TYPE_SRV.equals(dnsEntry.getRecordType()) && !dnsEntry.isExpired(now)) {
                    final DNSRecord.Service s = (DNSRecord.Service) dnsEntry;
                    if (s.getPort() != info.getPort() || !s.getServer().equals(_localHost.getName())) {
                        logger.debug("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:{} s.server={} {} equals:{}",
                                dnsEntry,
                                s.getServer(),
                                _localHost.getName(),
                                s.getServer().equals(_localHost.getName())
                        );
                        info.setName(NameRegister.Factory.getRegistry().incrementName(_localHost.getInetAddress(), info.getName(), NameRegister.NameType.SERVICE));
                        collision = true;
                        break;
                    }
                }
            }

            // Check for collision with other service infos published by JmDNS
            final ServiceInfo selfService = _services.get(info.getKey());
            if (selfService != null && selfService != info) {
                info.setName(NameRegister.Factory.getRegistry().incrementName(_localHost.getInetAddress(), info.getName(), NameRegister.NameType.SERVICE));
                collision = true;
            }
        }
        while (collision);

        return !(originalQualifiedName.equals(info.getKey()));
    }

    /**
     * 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.
     *
     * @param listener
     *            DSN listener
     * @param question
     *            DNS query
     */
    public void addListener(DNSListener listener, DNSQuestion question) {
        final long now = System.currentTimeMillis();

        // add the new listener
        _listeners.add(listener);

        // report existing matched records

        if (question != null) {
            for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(question.getName().toLowerCase())) {
                if (question.answeredBy(dnsEntry) && !dnsEntry.isExpired(now)) {
                    listener.updateRecord(this.getCache(), now, dnsEntry);
                }
            }
        }
    }

    /**
     * Remove a listener from all outstanding questions. The listener will no longer receive any updates.
     *
     * @param listener
     *            DSN listener
     */
    public void removeListener(DNSListener listener) {
        _listeners.remove(listener);
    }

    /**
     * Renew a service when the record become stale. If there is no service collector for the type this method does nothing.
     *
     * @param type
     *            Service Type
     */
    public void renewServiceCollector(String type) {
        if (_serviceCollectors.containsKey(type.toLowerCase())) {
            // Create/start ServiceResolver
            this.startServiceResolver(type);
        }
    }

    // Remind: Method updateRecord should receive a better name.
    /**
     * Notify all listeners that a record was updated.
     *
     * @param now
     *            update date
     * @param rec
     *            DNS record
     * @param operation
     *            DNS cache operation
     */
    public void updateRecord(long now, DNSRecord rec, Operation operation) {
        // We do not want to block the entire DNS while we are updating the record for each listener (service info)
        {
            List<DNSListener> listenerList = null;
            synchronized (_listeners) {
                listenerList = new ArrayList<DNSListener>(_listeners);
            }
            for (DNSListener listener : listenerList) {
                listener.updateRecord(this.getCache(), now, rec);
            }
        }

        /**
         * This is the place where we actually add & remove services.
         *  - For adding we need a PTR record.
         *  - For removing we want also to consider expired SRV records because this means the service hasn't been updated
         *    and is probably not reachable anymore.
         *
         * For details see also RFC 6762 / Section 10.4:
         * @see https://tools.ietf.org/html/rfc6762#section-10.4
         */
        if (
                DNSRecordType.TYPE_PTR.equals(rec.getRecordType())
                || ( DNSRecordType.TYPE_SRV.equals(rec.getRecordType()) && Operation.Remove.equals(operation))
        )
        {
            ServiceEvent event = rec.getServiceEvent(this);
            if ((event.getInfo() == null) || !event.getInfo().hasData()) {
                // We do not care about the subtype because the info is only used if complete and the subtype will then be included.
                ServiceInfo info = this.getServiceInfoFromCache(event.getType(), event.getName(), "", false);
                if (info.hasData()) {
                    event = new ServiceEventImpl(this, event.getType(), event.getName(), info);
                }
            }

            List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase());
            final List<ServiceListenerStatus> serviceListenerList;
            if (list != null) {
                synchronized (list) {
                    serviceListenerList = new ArrayList<ServiceListenerStatus>(list);
                }
            } else {
                serviceListenerList = Collections.emptyList();
            }
            logger.trace("{}.updating record for event: {} list {} operation: {}",
                this.getName(),
                event,
                serviceListenerList,
                operation
            );
            if (!serviceListenerList.isEmpty()) {
                final ServiceEvent localEvent = event;

                switch (operation) {
                    case Add:
                        for (final ServiceListenerStatus listener : serviceListenerList) {
                            if (listener.isSynchronous()) {
                                listener.serviceAdded(localEvent);
                            } else {
                                _executor.submit(new Runnable() {
                                    /** {@inheritDoc} */
                                    @Override
                                    public void run() {
                                        listener.serviceAdded(localEvent);
                                    }
                                });
                            }
                        }
                        break;
                    case Remove:
                        for (final ServiceListenerStatus listener : serviceListenerList) {
                            if (listener.isSynchronous()) {
                                listener.serviceRemoved(localEvent);
                            } else {
                                _executor.submit(new Runnable() {
                                    /** {@inheritDoc} */
                                    @Override
                                    public void run() {
                                        listener.serviceRemoved(localEvent);
                                    }
                                });
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }

    void handleRecord(DNSRecord record, long now) {
        DNSRecord newRecord = record;

        Operation cacheOperation = Operation.Noop;
        final boolean expired = newRecord.isExpired(now);
        logger.debug("{} handle response: {}", this.getName(), newRecord);

        // update the cache
        if (!newRecord.isServicesDiscoveryMetaQuery() && !newRecord.isDomainDiscoveryQuery()) {
            final boolean unique = newRecord.isUnique();
            final DNSRecord cachedRecord = (DNSRecord) this.getCache().getDNSEntry(newRecord);
            logger.debug("{} handle response cached record: {}", this.getName(), cachedRecord);
            // RFC 6762, section 10.2 Announcements to Flush Outdated Cache Entries
            // https://tools.ietf.org/html/rfc6762#section-10.2
            // if (cache-flush a.k.a unique), remove all existing records matching these criterias :--
            //     1. same name
            //     2. same record type
            //     3. same record class
            //     4. record is older than 1 second.
            if (unique) {
                for (DNSEntry entry : this.getCache().getDNSEntryList(newRecord.getKey())) {
                    if (    newRecord.getRecordType().equals(entry.getRecordType()) &&
                            newRecord.getRecordClass().equals(entry.getRecordClass()) &&
                            isOlderThanOneSecond( (DNSRecord)entry, now )                            
                    ) {
                        logger.trace("setWillExpireSoon() on: {}", entry);
                        // this set ttl to 1 second,
                        ((DNSRecord) entry).setWillExpireSoon(now);  
                    }
                }
            }
            if (cachedRecord != null) {
                if (expired) {
                    // if the record has a 0 ttl that means we have a cancel record we need to delay the removal by 1s
                    if (newRecord.getTTL() == 0) {
                        cacheOperation = Operation.Noop;
                        logger.trace("Record is expired - setWillExpireSoon() on:\n\t{}", cachedRecord);
                        cachedRecord.setWillExpireSoon(now);
                        // the actual record will be disposed of by the record reaper.
                    } else {
                        cacheOperation = Operation.Remove;
                        logger.trace("Record is expired - removeDNSEntry() on:\n\t{}", cachedRecord);
                        this.getCache().removeDNSEntry(cachedRecord);
                    }
                } else {
                    // If the record content has changed we need to inform our listeners.
                    if (    !newRecord.sameValue(cachedRecord) ||
                            (
                                    !newRecord.sameSubtype(cachedRecord) &&
                                    (newRecord.getSubtype().length() > 0)
                            )
                    ) {
                        if (newRecord.isSingleValued()) {
                            cacheOperation = Operation.Update;
                            logger.trace("Record (singleValued) has changed - replaceDNSEntry() on:\n\t{}\n\t{}", newRecord, cachedRecord);
                            this.getCache().replaceDNSEntry(newRecord, cachedRecord);
                        } else {
                            // Address record can have more than one value on multi-homed machines
                            cacheOperation = Operation.Add;
                            logger.trace("Record (multiValue) has changed - addDNSEntry on:\n\t{}", newRecord);
                            this.getCache().addDNSEntry(newRecord);
                        }
                    } else {
                        cachedRecord.resetTTL(newRecord);
                        newRecord = cachedRecord;
                    }
                }
            } else {
                if (!expired) {
                    cacheOperation = Operation.Add;
                    logger.trace("Record not cached - addDNSEntry on:\n\t{}", newRecord);
                    this.getCache().addDNSEntry(newRecord);
                }
            }
        }

        // Register new service types
        if (newRecord.getRecordType() == DNSRecordType.TYPE_PTR) {
            // handle DNSConstants.DNS_META_QUERY records
            boolean typeAdded = false;
            if (newRecord.isServicesDiscoveryMetaQuery()) {
                // The service names are in the alias.
                if (!expired) {
                    typeAdded = this.registerServiceType(((DNSRecord.Pointer) newRecord).getAlias());
                }
                return;
            }
            typeAdded |= this.registerServiceType(newRecord.getName());
            if (typeAdded && (cacheOperation == Operation.Noop)) {
                cacheOperation = Operation.RegisterServiceType;
            }
        }

        // notify the listeners
        if (cacheOperation != Operation.Noop) {
            this.updateRecord(now, newRecord, cacheOperation);
        }

    }
    
    /**
     *  
     * @param dnsRecord 
     * @param timeToCompare a given times for comparison
     * @return true if dnsRecord create time is older than 1 second, relative to the given time; false otherwise 
     */
    private boolean isOlderThanOneSecond(DNSRecord dnsRecord, long timeToCompare) {
        return (dnsRecord.getCreated() < (timeToCompare - DNSConstants.FLUSH_RECORD_OLDER_THAN_1_SECOND*1000));
    }

    /**
     * Handle an incoming response. Cache answers, and pass them on to the appropriate questions.
     *
     * @exception IOException
     */
    void handleResponse(DNSIncoming msg) throws IOException {
        final long now = System.currentTimeMillis();

        boolean hostConflictDetected = false;
        boolean serviceConflictDetected = false;

        List<DNSRecord> allAnswers = msg.getAllAnswers();
        allAnswers = aRecordsLast(allAnswers);
        for (DNSRecord newRecord : allAnswers) {
            this.handleRecord(newRecord, now);

            if (DNSRecordType.TYPE_A.equals(newRecord.getRecordType()) || DNSRecordType.TYPE_AAAA.equals(newRecord.getRecordType())) {
                hostConflictDetected |= newRecord.handleResponse(this);
            } else {
                serviceConflictDetected |= newRecord.handleResponse(this);
            }

        }

        if (hostConflictDetected || serviceConflictDetected) {
            this.startProber();
        }
    }

    /**
     * In case the a record is received before the srv record the ip address would not be set.
     * <p>
     * Wireshark record: see also file a_record_before_srv.pcapng and {@link ServiceInfoImplTest#test_ip_address_is_set()}
     * <p>
     * Multicast Domain Name System (response)
     * Transaction ID: 0x0000
     * Flags: 0x8400 Standard query response, No error
     * Questions: 0
     * Answer RRs: 2
     * Authority RRs: 0
     * Additional RRs: 8
     * Answers
     * _ibisip_http._tcp.local: type PTR, class IN, DeviceManagementService._ibisip_http._tcp.local
     * _ibisip_http._tcp.local: type PTR, class IN, PassengerCountingService._ibisip_http._tcp.local
     * Additional records
     * DeviceManagementService._ibisip_http._tcp.local: type TXT, class IN, cache flush
     * PassengerCountingService._ibisip_http._tcp.local: type TXT, class IN, cache flush
     * DIST500_7-F07_OC030_05_03941.local: type A, class IN, cache flush, addr 192.168.88.236
     * DeviceManagementService._ibisip_http._tcp.local: type SRV, class IN, cache flush, priority 0, weight 0, port 5000, target DIST500_7-F07_OC030_05_03941.local
     * PassengerCountingService._ibisip_http._tcp.local: type SRV, class IN, cache flush, priority 0, weight 0, port 5001, target DIST500_7-F07_OC030_05_03941.local
     * DeviceManagementService._ibisip_http._tcp.local: type NSEC, class IN, cache flush, next domain name DeviceManagementService._ibisip_http._tcp.local
     * PassengerCountingService._ibisip_http._tcp.local: type NSEC, class IN, cache flush, next domain name PassengerCountingService._ibisip_http._tcp.local
     * DIST500_7-F07_OC030_05_03941.local: type NSEC, class IN, cache flush, next domain name DIST500_7-F07_OC030_05_03941.local
     */
    private List<DNSRecord> aRecordsLast(List<DNSRecord> allAnswers) {
        ArrayList<DNSRecord> ret = new ArrayList<DNSRecord>(allAnswers.size());
        ArrayList<DNSRecord> arecords = new ArrayList<DNSRecord>();
        // filter all a records and move them to the end of the list
        // we do not change der order of the order records
        for (DNSRecord answer : allAnswers) {
            if (answer.getRecordType().equals(DNSRecordType.TYPE_A) || answer.getRecordType().equals(DNSRecordType.TYPE_AAAA)) {
                arecords.add(answer);
            } else {
                ret.add(answer);
            }
        }
        ret.addAll(arecords);
        return ret;
    }


    /**
     * Handle an incoming query. See if we can answer any part of it given our service infos.
     *
     * @param in
     * @param addr
     * @param port
     * @exception IOException
     */
    void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException {
        logger.debug("{} handle query: {}", this.getName(), in);
        // Track known answers
        boolean conflictDetected = false;
        final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL;
        for (DNSRecord answer : in.getAllAnswers()) {
            conflictDetected |= answer.handleQuery(this, expirationTime);
        }

        this.ioLock();
        try {

            if (_plannedAnswer != null) {
                _plannedAnswer.append(in);
            } else {
                DNSIncoming plannedAnswer = in.clone();
                if (in.isTruncated()) {
                    _plannedAnswer = plannedAnswer;
                }
                this.startResponder(plannedAnswer, addr, port);
            }

        } finally {
            this.ioUnlock();
        }

        final long now = System.currentTimeMillis();
        for (DNSRecord answer : in.getAnswers()) {
            this.handleRecord(answer, now);
        }

        if (conflictDetected) {
            this.startProber();
        }
    }

    public void respondToQuery(DNSIncoming in) {
        this.ioLock();
        try {
            if (_plannedAnswer == in) {
                _plannedAnswer = null;
            }
        } finally {
            this.ioUnlock();
        }
    }

    /**
     * Add an answer to a question. Deal with the case when the outgoing packet overflows
     *
     * @param in
     * @param addr
     * @param port
     * @param out
     * @param rec
     * @return outgoing answer
     * @exception IOException
     */
    public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException {
        DNSOutgoing newOut = out;
        if (newOut == null) {
            newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
        }
        try {
            newOut.addAnswer(in, rec);
        } catch (final IOException e) {
            newOut.setFlags(newOut.getFlags() | DNSConstants.FLAGS_TC);
            newOut.setId(in.getId());
            send(newOut);

            newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload());
            newOut.addAnswer(in, rec);
        }
        return newOut;
    }

    /**
     * Send an outgoing multicast DNS message.
     *
     * @param out
     * @exception IOException
     */
    public void send(DNSOutgoing out) throws IOException {
        if (!out.isEmpty()) {
            final InetAddress addr;
            final int port;

            if (out.getDestination() != null) {
                addr = out.getDestination().getAddress();
                port = out.getDestination().getPort();
            } else {
                addr = _group;
                port = DNSConstants.MDNS_PORT;
            }

            byte[] message = out.data();
            final DatagramPacket packet = new DatagramPacket(message, message.length, addr, port);

            if (logger.isTraceEnabled()) {
                try {
                    final DNSIncoming msg = new DNSIncoming(packet);
                    if (logger.isTraceEnabled()) {
                        logger.trace("send({}) JmDNS out:{}", this.getName(), msg.print(true));
                    }
                } catch (final IOException e) {
                    logger.debug(getClass().toString(), ".send(" + this.getName() + ") - JmDNS can not parse what it sends!!!", e);
                }
            }
            final MulticastSocket ms = _socket;
            if (ms != null && !ms.isClosed()) {
                ms.send(packet);
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#purgeTimer()
     */
    @Override
    public void purgeTimer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeTimer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#purgeStateTimer()
     */
    @Override
    public void purgeStateTimer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeStateTimer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#cancelTimer()
     */
    @Override
    public void cancelTimer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelTimer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#cancelStateTimer()
     */
    @Override
    public void cancelStateTimer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelStateTimer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startProber()
     */
    @Override
    public void startProber() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startProber();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startAnnouncer()
     */
    @Override
    public void startAnnouncer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startAnnouncer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startRenewer()
     */
    @Override
    public void startRenewer() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startRenewer();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startCanceler()
     */
    @Override
    public void startCanceler() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startCanceler();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startReaper()
     */
    @Override
    public void startReaper() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startReaper();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startServiceInfoResolver(javax.jmdns.impl.ServiceInfoImpl)
     */
    @Override
    public void startServiceInfoResolver(ServiceInfoImpl info) {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceInfoResolver(info);
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startTypeResolver()
     */
    @Override
    public void startTypeResolver() {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startTypeResolver();
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startServiceResolver(java.lang.String)
     */
    @Override
    public void startServiceResolver(String type) {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceResolver(type);
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSTaskStarter#startResponder(javax.jmdns.impl.DNSIncoming, int)
     */
    @Override
    public void startResponder(DNSIncoming in, InetAddress addr, int port) {
        DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startResponder(in, addr, port);
    }

    // REMIND: Why is this not an anonymous inner class?
    /**
     * Shutdown operations.
     */
    protected class Shutdown implements Runnable {
        /** {@inheritDoc} */
        @Override
        public void run() {
            try {
                _shutdown = null;
                close();
            } catch (Throwable exception) {
                System.err.println("Error while shuting down. " + exception);
            }
        }
    }

    private final Object _recoverLock = new Object();

    /**
     * Recover jmDNS when there is an error.
     */
    public void recover() {
        logger.debug("{}.recover()", this.getName());
        // We have an IO error so lets try to recover if anything happens lets close it.
        // This should cover the case of the IP address changing under our feet
        if (this.isClosing() || this.isClosed() || this.isCanceling() || this.isCanceled()) {
            return;
        }

        // 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
        // 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.
        synchronized (_recoverLock) {
            // Stop JmDNS
            // This protects against recursive calls
            if (this.cancelState()) {
                final String newThreadName = this.getName() + ".recover()";
                logger.debug("{} thread {}", newThreadName, Thread.currentThread().getName());
                Thread recover = new Thread(newThreadName) {
                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public void run() {
                        __recover();
                    }
                };
                recover.start();
            }
        }
    }

    void __recover() {
        // Synchronize only if we are not already in process to prevent dead locks
        //
        logger.debug("{}.recover() Cleanning up", this.getName());

        logger.warn("RECOVERING");
        // Purge the timer
        this.purgeTimer();

        // We need to keep a copy for reregistration
        final Collection<ServiceInfo> oldServiceInfos = new ArrayList<ServiceInfo>(getServices().values());

        // Cancel all services
        this.unregisterAllServices();
        this.disposeServiceCollectors();

        this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);

        // Purge the canceler timer
        this.purgeStateTimer();

        //
        // close multicast socket
        this.closeMulticastSocket();

        //
        this.getCache().clear();
        logger.debug("{}.recover() All is clean", this.getName());

        if (this.isCanceled()) {
            //
            // All is clear now start the services
            //
            for (ServiceInfo info : oldServiceInfos) {
                ((ServiceInfoImpl) info).recoverState();
            }
            this.recoverState();

            try {
                this.openMulticastSocket(this.getLocalHost());
                this.start(oldServiceInfos);
            } catch (final Exception exception) {
                logger.warn(this.getName() + ".recover() Start services exception ", exception);
            }
            logger.warn("{}.recover() We are back!", this.getName());
        } else {
            // We have a problem. We could not clear the state.
            logger.warn("{}.recover() Could not recover we are Down!", this.getName());
            if (this.getDelegate() != null) {
                this.getDelegate().cannotRecoverFromIOError(this.getDns(), oldServiceInfos);
            }
        }

    }

    /**
     * Checks the cache of expired records and removes them.
     * If any records are about to expire it tries to get them refreshed.
     *
     * <p>
     * Implementation note:<br />
     * This method is called by the {@link RecordReaper} every {@link DNSConstants#RECORD_REAPER_INTERVAL} milliseconds.
     * </p>
     * @see DNSRecord
     * @see RecordReaper
     */
    public void cleanCache() {
        this.getCache().logCachedContent();

        final long now = System.currentTimeMillis();
        final Set<String> staleServiceTypesForRefresh = new HashSet<String>();
        for (final DNSEntry entry : this.getCache().allValues()) {
            try {
                final DNSRecord record = (DNSRecord) entry;
                if (record.isExpired(now)) {
                    this.updateRecord(now, record, Operation.Remove);
                    logger.trace("Removing DNSEntry from cache: {}", entry);
                    this.getCache().removeDNSEntry(record);
                } else if (record.isStaleAndShouldBeRefreshed(now)) {
                    record.incrementRefreshPercentage();
                    String type = record.getServiceInfo().getType().toLowerCase();
                    // only query every service type once per refresh
                    if (staleServiceTypesForRefresh.add(type)) {
                        // we should query for the record we care about i.e. those in the service collectors
                        this.renewServiceCollector(type);
                    }
                }
            } catch (Exception exception) {
                logger.warn(this.getName() + ".Error while reaping records: " + entry, exception);
                logger.warn(this.toString());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        if (this.isClosing()) {
            return;
        }

        logger.debug("Cancelling JmDNS: {}", this);

        // Stop JmDNS
        // This protects against recursive calls
        if (this.cancelState()) {
            // We got the tie break now clean up

            // Stop the timer
            logger.debug("Canceling the timer");
            this.cancelTimer();

            // Cancel all services
            this.unregisterAllServices();
            this.disposeServiceCollectors();

            logger.debug("Wait for JmDNS cancel: {}", this);

            this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT);

            // Stop the canceler timer
            logger.debug("Canceling the state timer");
            this.cancelStateTimer();

            // Stop the executor
            _executor.shutdown();

            // close socket
            this.closeMulticastSocket();

            // remove the shutdown hook
            if (_shutdown != null) {
                Runtime.getRuntime().removeShutdownHook(_shutdown);
            }

            // earlier we did a DNSTaskStarter.Factory.getInstance().getStarter(this.getDns())
            // now we must release the resources associated with the starter for this JmDNS instance
            DNSTaskStarter.Factory.getInstance().disposeStarter(this.getDns());

            logger.debug("JmDNS closed.");
        }
        advanceState(null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Deprecated
    public void printServices() {
        System.err.println(toString());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(2048);
        sb.append("\n");
        sb.append("\t---- Local Host -----");
        sb.append("\n\t");
        sb.append(_localHost);
        sb.append("\n\t---- Services -----");
        for (final Map.Entry<String, ServiceInfo> entry : _services.entrySet()) {
            sb.append("\n\t\tService: ");
            sb.append(entry.getKey());
            sb.append(": ");
            sb.append(entry.getValue());
        }
        sb.append("\n");
        sb.append("\t---- Types ----");
        for (final ServiceTypeEntry subtypes : _serviceTypes.values()) {
            sb.append("\n\t\tType: ");
            sb.append(subtypes.getType());
            sb.append(": ");
            sb.append(subtypes.isEmpty() ? "no subtypes" : subtypes);
        }
        sb.append("\n");
        sb.append(_cache.toString());
        sb.append("\n");
        sb.append("\t---- Service Collectors ----");
        for (final Map.Entry<String, ServiceCollector> entry : _serviceCollectors.entrySet()) {
            sb.append("\n\t\tService Collector: ");
            sb.append(entry.getKey());
            sb.append(": ");
            sb.append(entry.getValue());
        }
        sb.append("\n");
        sb.append("\t---- Service Listeners ----");
        for (final Map.Entry<String, List<ListenerStatus.ServiceListenerStatus>> entry : _serviceListeners.entrySet()) {
            sb.append("\n\t\tService Listener: ");
            sb.append(entry.getKey());
            sb.append(": ");
            sb.append(entry.getValue());
        }
        return sb.toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo[] list(String type) {
        return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ServiceInfo[] list(String type, long timeout) {
        this.cleanCache();
        // Implementation note: The first time a list for a given type is
        // requested, a ServiceCollector is created which collects service
        // infos. This greatly speeds up the performance of subsequent calls
        // to this method. The caveats are, that 1) the first call to this
        // method for a given type is slow, and 2) we spawn a ServiceCollector
        // instance for each service type which increases network traffic a
        // little.

        String loType = type.toLowerCase();

        boolean newCollectorCreated = false;
        if (this.isCanceling() || this.isCanceled()) {
            return new ServiceInfo[0];
        }

        ServiceCollector collector = _serviceCollectors.get(loType);
        if (collector == null) {
            newCollectorCreated = _serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null;
            collector = _serviceCollectors.get(loType);
            if (newCollectorCreated) {
                this.addServiceListener(type, collector, ListenerStatus.SYNCHRONOUS);
            }
        }
        logger.debug("{}-collector: {}", this.getName(), collector);

        // At this stage the collector should never be null but it keeps findbugs happy.
        return (collector != null ? collector.list(timeout) : new ServiceInfo[0]);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, ServiceInfo[]> listBySubtype(String type) {
        return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, ServiceInfo[]> listBySubtype(String type, long timeout) {
        Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5);
        for (ServiceInfo info : this.list(type, timeout)) {
            String subtype = info.getSubtype().toLowerCase();
            if (!map.containsKey(subtype)) {
                map.put(subtype, new ArrayList<ServiceInfo>(10));
            }
            map.get(subtype).add(info);
        }

        Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size());
        for (final Map.Entry<String, List<ServiceInfo>> entry : map.entrySet()) {
            final String subtype = entry.getKey();
            final List<ServiceInfo> infoForSubType = entry.getValue();
            result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()]));
        }

        return result;
    }

    /**
     * This method disposes all ServiceCollector instances which have been created by calls to method <code>list(type)</code>.
     *
     * @see #list
     */
    private void disposeServiceCollectors() {
        logger.debug("disposeServiceCollectors()");
        for (final Map.Entry<String, ServiceCollector> entry : _serviceCollectors.entrySet()) {
            final ServiceCollector collector = entry.getValue();
            if (collector != null) {
                final String type = entry.getKey();
                this.removeServiceListener(type, collector);
                _serviceCollectors.remove(type, collector);
            }
        }
    }

    /**
     * Instances of ServiceCollector are used internally to speed up the performance of method <code>list(type)</code>.
     *
     * @see #list
     */
    private static class ServiceCollector implements ServiceListener {
        // private static Logger logger = LoggerFactory.getLogger(ServiceCollector.class.getName());

        /**
         * A set of collected service instance names.
         */
        private final ConcurrentMap<String, ServiceInfo> _infos;

        /**
         * A set of collected service event waiting to be resolved.
         */
        private final ConcurrentMap<String, ServiceEvent> _events;

        /**
         * This is the type we are listening for (only used for debugging).
         */
        private final String _type;

        /**
         * This is used to force a wait on the first invocation of list.
         */
        private volatile boolean _needToWaitForInfos;

        public ServiceCollector(String type) {
            super();
            _infos = new ConcurrentHashMap<String, ServiceInfo>();
            _events = new ConcurrentHashMap<String, ServiceEvent>();
            _type = type;
            _needToWaitForInfos = true;
        }

        /**
         * A service has been added.
         *
         * @param event
         *            service event
         */
        @Override
        public void serviceAdded(ServiceEvent event) {
            synchronized (this) {
                ServiceInfo info = event.getInfo();
                if ((info != null) && (info.hasData())) {
                    _infos.put(event.getName(), info);
                } else {
                    String subtype = (info != null ? info.getSubtype() : "");
                    info = ((JmDNSImpl) event.getDNS()).resolveServiceInfo(event.getType(), event.getName(), subtype, true);
                    if (info != null) {
                        _infos.put(event.getName(), info);
                    } else {
                        _events.put(event.getName(), event);
                    }
                }
            }
        }

        /**
         * A service has been removed.
         *
         * @param event
         *            service event
         */
        @Override
        public void serviceRemoved(ServiceEvent event) {
            synchronized (this) {
                _infos.remove(event.getName());
                _events.remove(event.getName());
            }
        }

        /**
         * A service has been resolved. Its details are now available in the ServiceInfo record.
         *
         * @param event
         *            service event
         */
        @Override
        public void serviceResolved(ServiceEvent event) {
            synchronized (this) {
                _infos.put(event.getName(), event.getInfo());
                _events.remove(event.getName());
            }
        }

        /**
         * Returns an array of all service infos which have been collected by this ServiceCollector.
         *
         * @param timeout
         *            timeout if the info list is empty.
         * @return Service Info array
         */
        public ServiceInfo[] list(long timeout) {
            if (_infos.isEmpty() || !_events.isEmpty() || _needToWaitForInfos) {
                long loops = (timeout / 200L);
                if (loops < 1) {
                    loops = 1;
                }
                for (int i = 0; i < loops; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (final InterruptedException e) {
                        /* Stub */
                    }
                    if (_events.isEmpty() && !_infos.isEmpty() && !_needToWaitForInfos) {
                        break;
                    }
                }
            }
            _needToWaitForInfos = false;
            return _infos.values().toArray(new ServiceInfo[_infos.size()]);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("\n\tType: ");
            sb.append(_type);
            if (_infos.isEmpty()) {
                sb.append("\n\tNo services collected.");
            } else {
                sb.append("\n\tServices");
                for (final Map.Entry<String, ServiceInfo> entry : _infos.entrySet()) {
                    sb.append("\n\t\tService: ");
                    sb.append(entry.getKey());
                    sb.append(": ");
                    sb.append(entry.getValue());
                }
            }
            if (_events.isEmpty()) {
                sb.append("\n\tNo event queued.");
            } else {
                sb.append("\n\tEvents");
                for (final Map.Entry<String, ServiceEvent> entry : _events.entrySet()) {
                    sb.append("\n\t\tEvent: ");
                    sb.append(entry.getKey());
                    sb.append(": ");
                    sb.append(entry.getValue());
                }
            }
            return sb.toString();
        }

    }

    static String toUnqualifiedName(String type, String qualifiedName) {
        String loType = type.toLowerCase();
        String loQualifiedName = qualifiedName.toLowerCase();
        if (loQualifiedName.endsWith(loType) && !(loQualifiedName.equals(loType))) {
            return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1);
        }
        return qualifiedName;
    }

    public Map<String, ServiceInfo> getServices() {
        return _services;
    }

    public void setLastThrottleIncrement(long lastThrottleIncrement) {
        this._lastThrottleIncrement = lastThrottleIncrement;
    }

    public long getLastThrottleIncrement() {
        return _lastThrottleIncrement;
    }

    public void setThrottle(int throttle) {
        this._throttle = throttle;
    }

    public int getThrottle() {
        return _throttle;
    }

    public static Random getRandom() {
        return _random;
    }

    public void ioLock() {
        _ioLock.lock();
    }

    public void ioUnlock() {
        _ioLock.unlock();
    }

    public void setPlannedAnswer(DNSIncoming plannedAnswer) {
        this._plannedAnswer = plannedAnswer;
    }

    public DNSIncoming getPlannedAnswer() {
        return _plannedAnswer;
    }

    void setLocalHost(HostInfo localHost) {
        this._localHost = localHost;
    }

    public Map<String, ServiceTypeEntry> getServiceTypes() {
        return _serviceTypes;
    }

    public MulticastSocket getSocket() {
        return _socket;
    }

    public InetAddress getGroup() {
        return _group;
    }

    @Override
    public Delegate getDelegate() {
        return this._delegate;
    }

    @Override
    public Delegate setDelegate(Delegate delegate) {
        Delegate previous = this._delegate;
        this._delegate = delegate;
        return previous;
    }

}
