Author: nextgens
Date: 2007-03-04 15:23:01 +0000 (Sun, 04 Mar 2007)
New Revision: 11954

Added:
   trunk/plugins/MDNSDiscovery/
   trunk/plugins/MDNSDiscovery/MDNSDiscovery.java
   trunk/plugins/MDNSDiscovery/com/
   trunk/plugins/MDNSDiscovery/com/strangeberry/
   trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/
   trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/
   trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java
   trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java
   trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java
   trunk/plugins/MDNSDiscovery/gpl.txt
   trunk/plugins/MDNSDiscovery/javax/
   trunk/plugins/MDNSDiscovery/javax/jmdns/
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java
   trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java
Log:
plugins: new plugin called MDNSDiscovery ; it implements Zeroconf (called 
Bonjour/RendezVous by apple) support on a freenet node.

That will allow FCP clients to discover "automagicaly" FCP servers and might 
have other applications (we will discuss that on @darknet-tools soon)

it works fine using latest trunk (r11953) no backward compatibility is planned.

Added: trunk/plugins/MDNSDiscovery/MDNSDiscovery.java
===================================================================
--- trunk/plugins/MDNSDiscovery/MDNSDiscovery.java                              
(rev 0)
+++ trunk/plugins/MDNSDiscovery/MDNSDiscovery.java      2007-03-04 15:23:01 UTC 
(rev 11954)
@@ -0,0 +1,178 @@
+/* This code is part of Freenet. It is distributed under the GNU General
+ * Public License, version 2 (or at your option any later version). See
+ * http://www.gnu.org/ for further details of the GPL. */
+
+package plugins.MDNSDiscovery;
+
+import java.io.IOException;
+
+import plugins.MDNSDiscovery.javax.jmdns.JmDNS;
+import plugins.MDNSDiscovery.javax.jmdns.ServiceEvent;
+import plugins.MDNSDiscovery.javax.jmdns.ServiceInfo;
+import plugins.MDNSDiscovery.javax.jmdns.ServiceListener;
+import freenet.clients.http.PageMaker;
+import freenet.config.Config;
+import freenet.pluginmanager.*;
+import freenet.support.HTMLNode;
+import freenet.support.api.HTTPRequest;
+
+/**
+ * This plugin implements Zeroconf (called Bonjour/RendezVous by apple) 
support on a freenet node.
+ * 
+ * @author Florent Daignière <nextgens at freenetproject.org>
+ *
+ * @see http://www.dns-sd.org/ServiceTypes.html
+ * @see http://www.multicastdns.org/
+ * @see http://jmdns.sourceforge.net/
+ * 
+ * TODO: We shouldn't start a thread at all ... but they are issues on startup 
(the configuration framework isn't available yet)
+ * TODO: We shouldn't advertise services if they aren't reachable (ie: bound 
to localhost)
+ * TODO: We will need to manage the list on our own insteed of requesting it 
for each http request
+ * TODO: Plug into config. callbacks to reflect changes
+ */
+public class MDNSDiscovery implements FredPlugin, FredPluginHTTP{
+       public static String freenetServiceType = "_freenet._udp.local.";
+       private boolean goon = true;
+       private JmDNS jmdns;
+       private ServiceInfo fproxyInfo, tcmiInfo, fcpInfo, nodeInfo;
+       private Config nodeConfig;
+       private PageMaker pageMaker;
+       
+       /**
+        * Called upon plugin unloading : we unregister advertised services
+        */
+       public void terminate() {
+               jmdns.unregisterAllServices();
+               goon = false;
+       }
+
+       public void runPlugin(PluginRespirator pr) {
+               // wait until the node is initialised.
+               do{
+                       try{
+                               Thread.sleep(1000);     
+                       }catch (InterruptedException e) {}                      
+               }while(pr.getNode() == null || !pr.getNode().isHasStarted());
+                       
+               nodeConfig = pr.getNode().config;
+               pageMaker = new PageMaker("clean");
+               
+               try{
+                       // Create the multicast listener
+                       jmdns = new JmDNS();
+                       final String address = "server on " + 
jmdns.getLocalHost() + " (" + pr.getNode().getMyName();
+                       
+                       // Advertise Fproxy
+                       if(nodeConfig.get("fproxy").getBoolean("enabled")){
+                               fproxyInfo = new 
ServiceInfo("_http._tcp.local.", "Freenet 0.7 Fproxy " + address,
+                                               
nodeConfig.get("fproxy").getInt("port"), 0, 0, "path=/");
+                               jmdns.registerService(fproxyInfo);
+                       }
+
+                       // Advertise FCP
+                       if(nodeConfig.get("fcp").getBoolean("enabled")){
+                               fcpInfo = new ServiceInfo("_fcp._tcp.local.", 
"Freenet 0.7 FCP " + address,
+                                               
nodeConfig.get("fcp").getInt("port"), 0, 0, "");
+                               jmdns.registerService(fcpInfo);
+                       }
+                       
+                       // Advertise TCMI
+                       if(nodeConfig.get("console").getBoolean("enabled")){
+                               tcmiInfo = new 
ServiceInfo("_telnet._tcp.local.", "Freenet 0.7 TCMI " + address,
+                                               
nodeConfig.get("console").getInt("port"), 0, 0, "");
+                               jmdns.registerService(tcmiInfo);
+                       }
+                               
+                       // Advertise the node
+                       nodeInfo = new 
ServiceInfo(MDNSDiscovery.freenetServiceType, "Freenet 0.7 Node " + address,
+                                       
nodeConfig.get("node").getInt("listenPort"), 0, 0, "");
+                       jmdns.registerService(nodeInfo);
+                       
+                       // Watch out for other nodes
+                       
jmdns.addServiceListener(MDNSDiscovery.freenetServiceType, new 
NodeMDNSListener(this));
+                       
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+
+               while(goon){
+                       synchronized (this) {
+                               try{
+                                       wait(300000);
+                               }catch (InterruptedException e) {}      
+                       }
+               }
+       }
+       
+       static class NodeMDNSListener implements ServiceListener {
+               final MDNSDiscovery plugin;
+               
+               public NodeMDNSListener(MDNSDiscovery plugin) {
+                       this.plugin = plugin;
+               }
+               
+        public synchronized void serviceAdded(ServiceEvent event) {
+            System.out.println("Service added   : " + 
event.getName()+"."+event.getType());
+            synchronized (plugin) {
+                plugin.notify();                               
+                       }
+        }
+        
+        public synchronized void serviceRemoved(ServiceEvent event) {
+            System.out.println("Service removed : " + 
event.getName()+"."+event.getType());
+            synchronized (plugin) {
+                plugin.notify();                               
+                       }
+        }
+        
+        public synchronized void serviceResolved(ServiceEvent event) {
+            System.out.println("Service resolved: " + event.getInfo());
+            synchronized (plugin) {
+                plugin.notify();                               
+                       }
+        }
+    }
+       
+       public String handleHTTPGet(HTTPRequest request) throws 
PluginHTTPException {
+               HTMLNode pageNode = pageMaker.getPageNode("MDNSDiscovery plugin 
configuration page", false);
+               HTMLNode contentNode = pageMaker.getContentNode(pageNode);
+               
+               ServiceInfo[] foundNodes = 
jmdns.list(MDNSDiscovery.freenetServiceType);
+               if(foundNodes != null && foundNodes.length > 0){
+                       HTMLNode peerTableInfobox = contentNode.addChild("div", 
"class", "infobox infobox-normal");
+                       HTMLNode peerTableInfoboxHeader = 
peerTableInfobox.addChild("div", "class", "infobox-header");
+                       peerTableInfoboxHeader.addChild("#", "The following 
nodes have been found on the local subnet :");
+                       HTMLNode peerTableInfoboxContent = 
peerTableInfobox.addChild("div", "class", "infobox-content");
+                       
+                       HTMLNode peerTable = 
peerTableInfoboxContent.addChild("table", "class", "darknet_connections");
+                       HTMLNode peerTableHeaderRow = peerTable.addChild("tr");
+                       peerTableHeaderRow.addChild("th").addChild("span", new 
String[] { "title", "style" }, new String[] { "The node's name.", 
"border-bottom: 1px dotted; cursor: help;" }, "Name");
+                       peerTableHeaderRow.addChild("th").addChild("span", new 
String[] { "title", "style" }, new String[] { "The node's network address as 
IP:Port", "border-bottom: 1px dotted; cursor: help;" }, "Address");
+                       peerTableHeaderRow.addChild("th").addChild("span", new 
String[] { "title", "style" }, new String[] { "A description of the service.", 
"border-bottom: 1px dotted; cursor: help;" }, "Description");
+                       
+                       HTMLNode peerRow = peerTable.addChild("tr");
+                       
+                       for(int i=0; i<foundNodes.length; i++){
+                               peerRow.addChild("td", "class", 
"peer-name").addChild("#", foundNodes[i].getServer());
+                               peerRow.addChild("td", "class", 
"peer-address").addChild("#", 
foundNodes[i].getHostAddress()+':'+foundNodes[i].getPort());
+                               peerRow.addChild("td", "class", 
"peer-private-darknet-comment-note").addChild("#", foundNodes[i].getName());
+                       }
+               }else{
+                       HTMLNode peerTableInfobox = contentNode.addChild("div", 
"class", "infobox infobox-warning");
+                       HTMLNode peerTableInfoboxHeader = 
peerTableInfobox.addChild("div", "class", "infobox-header");
+                       peerTableInfoboxHeader.addChild("#", "Nothing found!");
+                       HTMLNode peerTableInfoboxContent = 
peerTableInfobox.addChild("div", "class", "infobox-content");
+                       peerTableInfoboxContent.addChild("#", "No freenet node 
found on the local subnet, sorry!");
+               }
+               
+               return pageNode.generate();
+       }
+       
+       public String handleHTTPPut(HTTPRequest request) throws 
PluginHTTPException {
+               throw new PluginHTTPException();
+       }
+       
+       public String handleHTTPPost(HTTPRequest request) throws 
PluginHTTPException {
+               throw new PluginHTTPException();
+       }
+}

Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java
===================================================================
--- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java       
                        (rev 0)
+++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java       
2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,272 @@
+// %Z%%M%, %I%, %G%
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.table.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import plugins.MDNSDiscovery.javax.jmdns.*;
+
+/**
+ * User Interface for browsing JmDNS services.
+ *
+ * @author     Arthur van Hoff, Werner Randelshofer
+ * @version    %I%, %G%
+ */
+public class Browser extends JFrame implements ServiceListener, 
ServiceTypeListener, ListSelectionListener {
+    JmDNS jmdns;
+    Vector headers;
+    String type;
+    DefaultListModel types;
+    JList typeList;
+    DefaultListModel services;
+    JList serviceList;
+    JTextArea info;
+    
+    Browser(JmDNS jmdns) throws IOException {
+        super("JmDNS Browser");
+        this.jmdns = jmdns;
+        
+        Color bg = new Color(230, 230, 230);
+        EmptyBorder border = new EmptyBorder(5, 5, 5, 5);
+        Container content = getContentPane();
+        content.setLayout(new GridLayout(1, 3));
+        
+        types = new DefaultListModel();
+        typeList = new JList(types);
+        typeList.setBorder(border);
+        typeList.setBackground(bg);
+        typeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        typeList.addListSelectionListener(this);
+        
+        JPanel typePanel = new JPanel();
+        typePanel.setLayout(new BorderLayout());
+        typePanel.add("North", new JLabel("Types"));
+        typePanel.add("Center", new JScrollPane(typeList, 
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
+        content.add(typePanel);
+        
+        services = new DefaultListModel();
+        serviceList = new JList(services);
+        serviceList.setBorder(border);
+        serviceList.setBackground(bg);
+        serviceList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        serviceList.addListSelectionListener(this);
+        
+        JPanel servicePanel = new JPanel();
+        servicePanel.setLayout(new BorderLayout());
+        servicePanel.add("North", new JLabel("Services"));
+        servicePanel.add("Center", new JScrollPane(serviceList, 
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
+        content.add(servicePanel);
+        
+        info = new JTextArea();
+        info.setBorder(border);
+        info.setBackground(bg);
+        info.setEditable(false);
+        info.setLineWrap(true);
+        
+        JPanel infoPanel = new JPanel();
+        infoPanel.setLayout(new BorderLayout());
+        infoPanel.add("North", new JLabel("Details"));
+        infoPanel.add("Center", new JScrollPane(info, 
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
+        content.add(infoPanel);
+        
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setLocation(100, 100);
+        setSize(600, 400);
+        
+        jmdns.addServiceTypeListener(this);
+        
+        // register some well known types
+        String list[] = new String[] {
+            "_http._tcp.local.",
+            "_ftp._tcp.local.",
+            "_tftp._tcp.local.",
+            "_ssh._tcp.local.",
+            "_smb._tcp.local.",
+            "_printer._tcp.local.",
+            "_airport._tcp.local.",
+            "_afpovertcp._tcp.local.",
+            "_ichat._tcp.local.",
+            "_eppc._tcp.local.",
+            "_presence._tcp.local."
+        };
+        
+        for (int i = 0 ; i < list.length ; i++) {
+            jmdns.registerServiceType(list[i]);
+        }
+        
+        show();
+    }
+    
+    /**
+     * Add a service.
+     */
+    public void serviceAdded(ServiceEvent event) {
+        final String name = event.getName();
+        
+        System.out.println("ADD: " + name);
+        SwingUtilities.invokeLater(new Runnable() {
+        public void run() { insertSorted(services, name); }
+        });
+    }
+    
+    /**
+     * Remove a service.
+     */
+    public void serviceRemoved(ServiceEvent event) {
+        final String name = event.getName();
+
+        System.out.println("REMOVE: " + name);
+        SwingUtilities.invokeLater(new Runnable() {
+        public void run() { services.removeElement(name); }
+        });
+    }
+    
+    /**
+     * A new service type was <discovered.
+     */
+    public void serviceTypeAdded(ServiceEvent event) {
+        final String type = event.getType();
+
+        System.out.println("TYPE: " + type);
+        SwingUtilities.invokeLater(new Runnable() {
+        public void run() { insertSorted(types, type); }
+        });
+    }
+    
+    
+    void insertSorted(DefaultListModel model, String value) {
+        for (int i = 0, n = model.getSize() ; i < n ; i++) {
+            if (value.compareToIgnoreCase((String)model.elementAt(i)) < 0) {
+                model.insertElementAt(value, i);
+                return;
+            }
+        }
+        model.addElement(value);
+    }
+    
+    /**
+     * Resolve a service.
+     */
+    public void serviceResolved(ServiceEvent event) {
+        String name = event.getName();
+        String type = event.getType();
+        ServiceInfo info = event.getInfo();
+
+        if (name.equals(serviceList.getSelectedValue())) {
+            if (info == null) {
+                this.info.setText("service not found");
+            } else {
+                
+                StringBuffer buf = new StringBuffer();
+                buf.append(name);
+                buf.append('.');
+                buf.append(type);
+                buf.append('\n');
+                buf.append(info.getServer());
+                buf.append(':');
+                buf.append(info.getPort());
+                buf.append('\n');
+                buf.append(info.getAddress());
+                buf.append(':');
+                buf.append(info.getPort());
+                buf.append('\n');
+                for (Enumeration names = info.getPropertyNames() ; 
names.hasMoreElements() ; ) {
+                    String prop = (String)names.nextElement();
+                    buf.append(prop);
+                    buf.append('=');
+                    buf.append(info.getPropertyString(prop));
+                    buf.append('\n');
+                }
+                
+                this.info.setText(buf.toString());
+            }
+        }
+    }
+    
+    /**
+     * List selection changed.
+     */
+    public void valueChanged(ListSelectionEvent e) {
+        if (!e.getValueIsAdjusting()) {
+            if (e.getSource() == typeList) {
+                type = (String)typeList.getSelectedValue();
+                jmdns.removeServiceListener(type, this);
+                services.setSize(0);
+                info.setText("");
+                if (type != null) {
+                jmdns.addServiceListener(type, this);
+                }
+            } else if (e.getSource() == serviceList) {
+                String name = (String)serviceList.getSelectedValue();
+                if (name == null) {
+                    info.setText("");
+                } else {
+                    System.out.println(this+" valueChanged() type:"+type+" 
name:"+name);
+                    System.out.flush();
+                    ServiceInfo service = jmdns.getServiceInfo(type, name);
+                    if (service == null) {
+                        info.setText("service not found");
+                    } else {
+                        jmdns.requestServiceInfo(type, name);
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Table data.
+     */
+    class ServiceTableModel extends AbstractTableModel {
+        public String getColumnName(int column) {
+            switch (column) {
+                case 0: return "service";
+                case 1: return "address";
+                case 2: return "port";
+                case 3: return "text";
+            }
+            return null;
+        }
+        public int getColumnCount() {
+            return 1;
+        }
+        public int getRowCount() {
+            return services.size();
+        }
+        public Object getValueAt(int row, int col) {
+            return services.elementAt(row);
+        }
+    }
+    
+    public String toString() {
+        return "RVBROWSER";
+    }
+    
+    /**
+     * Main program.
+     */
+    public static void main(String argv[]) throws IOException {
+        new Browser(new JmDNS());
+    }
+}

Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java
===================================================================
--- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java          
                (rev 0)
+++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java  
2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,115 @@
+// %Z%%M%, %I%, %G%
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import plugins.MDNSDiscovery.javax.jmdns.*;
+
+/**
+ * Main sample program for JmDNS.
+ *
+ * @author     Arthur van Hoff, Werner Randelshofer
+ * @version    %I%, %G%
+ */
+public class Main {
+    static class SampleListener implements ServiceListener, 
ServiceTypeListener {
+        public void serviceAdded(ServiceEvent event) {
+            System.out.println("ADD: " + 
event.getDNS().getServiceInfo(event.getType(), event.getName(), 3*1000));
+        }
+        public void serviceRemoved(ServiceEvent event) {
+            System.out.println("REMOVE: " + event.getName());
+        }
+        public void serviceResolved(ServiceEvent event) {
+            System.out.println("RESOLVED: " + event.getInfo());
+        }
+        public void serviceTypeAdded(ServiceEvent event) {
+            System.out.println("TYPE: " + event.getType());
+        }
+    }
+    
+    public static void main(String argv[]) throws IOException {
+        int argc = argv.length;
+        boolean debug = false;
+        InetAddress intf = null;
+        
+        if ((argc > 0) && "-d".equals(argv[0])) {
+            System.arraycopy(argv, 1, argv, 0, --argc);
+            System.getProperties().put("jmdns.debug", "1");
+            debug = true;
+        }
+        if ((argc > 1) && "-i".equals(argv[0])) {
+            intf = InetAddress.getByName(argv[1]);
+            System.arraycopy(argv, 2, argv, 0, argc -= 2);
+        }
+        if (intf == null) {
+            intf = InetAddress.getLocalHost();
+        }
+        
+        JmDNS jmdns = new JmDNS(intf);
+        
+        if ((argc == 0) || ((argc >= 1) && "-browse".equals(argv[0]))) {
+            new Browser(jmdns);
+            for (int i = 2 ; i < argc ; i++) {
+                jmdns.registerServiceType(argv[i]);
+            }
+        } else if ((argc == 1) && "-bt".equals(argv[0])) {
+            jmdns.addServiceTypeListener(new SampleListener());
+        } else if ((argc == 3) && "-bs".equals(argv[0])) {
+            jmdns.addServiceListener(argv[1] + "." + argv[2], new 
SampleListener());
+        } else if ((argc > 4) && "-rs".equals(argv[0])) {
+            String type = argv[2] + "." + argv[3];
+            String name = argv[1];
+            Hashtable props = null;
+            for (int i = 5 ; i < argc ; i++) {
+                int j = argv[i].indexOf('=');
+                if (j < 0) {
+                    throw new RuntimeException("not key=val: " + argv[i]);
+                }
+                if (props == null) {
+                    props = new Hashtable();
+                }
+                props.put(argv[i].substring(0, j), argv[i].substring(j+1));
+            }
+            jmdns.registerService(new ServiceInfo(type, name, 
Integer.parseInt(argv[4]), 0, 0, props));
+            
+            // This while loop keeps the main thread alive
+            while (true) {
+                try {
+                    Thread.sleep(Integer.MAX_VALUE);
+                } catch (InterruptedException e) {
+                    break;
+                }
+            }
+        } else if ((argc == 2) && "-f".equals(argv[0])) {
+            new Responder(jmdns, argv[1]);
+        } else if (!debug) {
+            System.out.println();
+            System.out.println("jmdns:");
+            System.out.println("     -d                                       
- output debugging info");
+            System.out.println("     -i <addr>                                
- specify the interface address");
+            System.out.println("     -browse [<type>...]                      
- GUI browser (default)");
+            System.out.println("     -bt                                      
- browse service types");
+            System.out.println("     -bs <type> <domain>                      
- browse services by type");
+            System.out.println("     -rs <name> <type> <domain> <port> <txt>  
- register service");
+            System.out.println("     -f <file>                                
- rendezvous responder");
+            System.out.println();
+            System.exit(1);
+        }
+    }
+}

Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java
===================================================================
--- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java     
                        (rev 0)
+++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java     
2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,79 @@
+// %Z%%M%, %I%, %G%
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+// 
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+// 
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools;
+
+import java.io.*;
+import plugins.MDNSDiscovery.javax.jmdns.*;
+
+/**
+ * A sample JmDNS responder that reads a set of rendezvous service
+ * definitions from a file and registers them with rendezvous. It uses
+ * the same file format as Apple's responder.  Each record consists of
+ * 4 lines: name, type, text, port. Empty lines and lines starting with #
+ * between records are ignored.
+ *
+ * @author     Arthur van Hoff
+ * @version    %I%, %G%
+ */
+public class Responder
+{
+    /**
+     * Constructor.
+     */
+    public Responder(JmDNS jmdns, String file) throws IOException
+    {
+       BufferedReader in = new BufferedReader(new FileReader(file));
+       try {
+           while (true) {
+               String ln = in.readLine();
+               while ((ln != null) && (ln.startsWith("#") || 
ln.trim().length() == 0)) {
+                   ln = in.readLine();
+               }
+               if (ln == null) {
+                   break;
+               }
+               String name = ln;
+               String type = in.readLine();
+               String text = in.readLine();
+               int port = Integer.parseInt(in.readLine());
+
+               // make sure the type is fully qualified and in the local. 
domain
+               if (!type.endsWith(".")) {
+                   type += ".";
+               }
+               if  (!type.endsWith(".local.")) {
+                   type += "local.";
+               }
+
+               jmdns.registerService(
+                   new ServiceInfo(type, name, port, text));
+           }
+       } finally {
+           in.close();
+       }
+    }
+    
+    /**
+     * Create a responder.
+     */
+    public static void main(String argv[]) throws IOException
+    {
+       new Responder(new JmDNS(), (argv.length > 0) ? argv[0] : 
"services.txt");
+    }
+}
+
+

Added: trunk/plugins/MDNSDiscovery/gpl.txt
===================================================================
--- trunk/plugins/MDNSDiscovery/gpl.txt                         (rev 0)
+++ trunk/plugins/MDNSDiscovery/gpl.txt 2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java                       
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java       2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,257 @@
+//Copyright 2003-2005 Arthur van Hoff Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.logging.Logger;
+
+/**
+ * A table of DNS entries. This is a hash table which
+ * can handle multiple entries with the same name.
+ * <p/>
+ * Storing multiple entries with the same name is implemented using a
+ * linked list of <code>CacheNode</code>'s.
+ * <p/>
+ * The current implementation of the API of DNSCache does expose the
+ * cache nodes to clients. Clients must explicitly deal with the nodes
+ * when iterating over entries in the cache. Here's how to iterate over
+ * all entries in the cache:
+ * <pre>
+ * for (Iterator i=dnscache.iterator(); i.hasNext(); ) {
+ *    for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; 
n.next()) {
+ *       DNSEntry entry = n.getValue();
+ *       ...do something with entry...
+ *    }
+ * }
+ * </pre>
+ * <p/>
+ * And here's how to iterate over all entries having a given name:
+ * <pre>
+ * for (DNSCache.CacheNode n = (DNSCache.CacheNode) dnscache.find(name); n != 
null; n.next()) {
+ *     DNSEntry entry = n.getValue();
+ *     ...do something with entry...
+ * }
+ * </pre>
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Werner Randelshofer, Rick Blair
+ */
+class DNSCache
+{
+    private static Logger logger = Logger.getLogger(DNSCache.class.toString());
+    // Implementation note:
+    // We might completely hide the existence of CacheNode's in a future 
version
+    // of DNSCache. But this will require to implement two (inner) classes for
+    // the  iterators that will be returned by method <code>iterator()</code> 
and
+    // method <code>find(name)</code>.
+    // Since DNSCache is not a public class, it does not seem worth the effort
+    // to clean its API up that much.
+
+    // [PJYF Oct 15 2004] This should implements Collections that would be 
amuch cleaner implementation
+    
+    /**
+     * The number of DNSEntry's in the cache.
+     */
+    private int size;
+
+    /**
+     * The hashtable used internally to store the entries of the cache.
+     * Keys are instances of String. The String contains an unqualified service
+     * name.
+     * Values are linked lists of CacheNode instances.
+     */
+    private HashMap hashtable;
+
+    /**
+     * Cache nodes are used to implement storage of multiple DNSEntry's of the
+     * same name in the cache.
+     */
+    public static class CacheNode
+    {
+        private static Logger logger = 
Logger.getLogger(CacheNode.class.toString());
+        private DNSEntry value;
+        private CacheNode next;
+
+        public CacheNode(DNSEntry value)
+        {
+            this.value = value;
+        }
+
+        public CacheNode next()
+        {
+            return next;
+        }
+
+        public DNSEntry getValue()
+        {
+            return value;
+        }
+    }
+
+
+    /**
+     * Create a table with a given initial size.
+     */
+    public DNSCache(final int size)
+    {
+        hashtable = new HashMap(size);
+    }
+
+    /**
+     * Clears the cache.
+     */
+    public synchronized void clear()
+    {
+        hashtable.clear();
+        size = 0;
+    }
+
+    /**
+     * Adds an entry to the table.
+     */
+    public synchronized void add(final DNSEntry entry)
+    {
+        //logger.log("DNSCache.add("+entry.getName()+")");
+        CacheNode newValue = new CacheNode(entry);
+        CacheNode node = (CacheNode) hashtable.get(entry.getName());
+        if (node == null)
+        {
+            hashtable.put(entry.getName(), newValue);
+        }
+        else
+        {
+            newValue.next = node.next;
+            node.next = newValue;
+        }
+        size++;
+    }
+
+    /**
+     * Remove a specific entry from the table. Returns true if the
+     * entry was found.
+     */
+    public synchronized boolean remove(DNSEntry entry)
+    {
+        CacheNode node = (CacheNode) hashtable.get(entry.getName());
+        if (node != null)
+        {
+            if (node.value == entry)
+            {
+                if (node.next == null)
+                {
+                    hashtable.remove(entry.getName());
+                }
+                else
+                {
+                    hashtable.put(entry.getName(), node.next);
+                }
+                size--;
+                return true;
+            }
+
+            CacheNode previous = node;
+            node = node.next;
+            while (node != null)
+            {
+                if (node.value == entry)
+                {
+                    previous.next = node.next;
+                    size--;
+                    return true;
+                }
+                previous = node;
+                node = node.next;
+            }
+            ;
+        }
+        return false;
+    }
+
+    /**
+     * Get a matching DNS entry from the table (using equals).
+     * Returns the entry that was found.
+     */
+    public synchronized DNSEntry get(DNSEntry entry)
+    {
+        for (CacheNode node = find(entry.getName()); node != null; node = 
node.next)
+        {
+            if (node.value.equals(entry))
+            {
+                return node.value;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get a matching DNS entry from the table.
+     */
+    public synchronized DNSEntry get(String name, int type, int clazz)
+    {
+        for (CacheNode node = find(name); node != null; node = node.next)
+        {
+            if (node.value.type == type && node.value.clazz == clazz)
+            {
+                return node.value;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Iterates over all cache nodes.
+     * The iterator returns instances of DNSCache.CacheNode.
+     * Each instance returned is the first node of a linked list.
+     * To retrieve all entries, one must iterate over this linked list. See
+     * code snippets in the header of the class.
+     */
+    public Iterator iterator()
+    {
+        return 
Collections.unmodifiableCollection(hashtable.values()).iterator();
+    }
+
+    /**
+     * Iterate only over items with matching name.
+     * Returns an instance of DNSCache.CacheNode or null.
+     * If an instance is returned, it is the first node of a linked list.
+     * To retrieve all entries, one must iterate over this linked list.
+     */
+    public synchronized CacheNode find(String name)
+    {
+        return (CacheNode) hashtable.get(name);
+    }
+
+    /**
+     * List all entries for debugging.
+     */
+    public synchronized void print()
+    {
+        for (Iterator i = iterator(); i.hasNext();)
+        {
+            for (CacheNode n = (CacheNode) i.next(); n != null; n = n.next)
+            {
+                System.out.println(n.value);
+            }
+        }
+    }
+
+    public synchronized String toString()
+    {
+        StringBuffer aLog = new StringBuffer();
+        aLog.append("\t---- cache ----");
+        for (Iterator i = iterator(); i.hasNext();)
+        {
+            for (CacheNode n = (CacheNode) i.next(); n != null; n = n.next)
+            {
+                aLog.append("\n\t\t" + n.value);
+            }
+        }
+        return aLog.toString();
+    }
+
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java                   
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java   2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,125 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+/**
+ * DNS constants.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Jeff Sonstein, Werner Randelshofer, Pierre 
Frisch, Rick Blair
+ */
+final class DNSConstants
+{
+
+    // changed to final class - jeffs
+    final static String MDNS_GROUP = "224.0.0.251";
+    final static String MDNS_GROUP_IPV6 = "FF02::FB";
+    final static int MDNS_PORT = 5353;
+    final static int DNS_PORT = 53;
+    final static int DNS_TTL = 60 * 60;        // default one hour TTL
+    // final static int DNS_TTL                    = 120 * 60; // two hour TTL 
(draft-cheshire-dnsext-multicastdns.txt ch 13)
+    
+    final static int MAX_MSG_TYPICAL = 1460;
+    final static int MAX_MSG_ABSOLUTE = 8972;
+
+    final static int FLAGS_QR_MASK = 0x8000;   // Query response mask
+    final static int FLAGS_QR_QUERY = 0x0000;  // Query
+    final static int FLAGS_QR_RESPONSE = 0x8000;       // Response
+
+    final static int FLAGS_AA = 0x0400;        // Authorative answer
+    final static int FLAGS_TC = 0x0200;        // Truncated
+    final static int FLAGS_RD = 0x0100;        // Recursion desired
+    final static int FLAGS_RA = 0x8000;        // Recursion available
+
+    final static int FLAGS_Z = 0x0040; // Zero
+    final static int FLAGS_AD = 0x0020;        // Authentic data
+    final static int FLAGS_CD = 0x0010;        // Checking disabled
+
+    final static int CLASS_IN = 1;             // Final Static Internet
+    final static int CLASS_CS = 2;             // CSNET
+    final static int CLASS_CH = 3;             // CHAOS
+    final static int CLASS_HS = 4;             // Hesiod
+    final static int CLASS_NONE = 254;         // Used in DNS UPDATE [RFC 2136]
+    final static int CLASS_ANY = 255;          // Not a DNS class, but a DNS 
query class, meaning "all classes"
+    final static int CLASS_MASK = 0x7FFF;      // Multicast DNS uses the 
bottom 15 bits to identify the record class...
+    final static int CLASS_UNIQUE = 0x8000;    // ... and the top bit 
indicates that all other cached records are now invalid
+
+    final static int TYPE_IGNORE = 0;          // This is a hack to stop 
further processing
+    final static int TYPE_A = 1;               // Address
+    final static int TYPE_NS = 2;              // Name Server
+    final static int TYPE_MD = 3;              // Mail Destination
+    final static int TYPE_MF = 4;              // Mail Forwarder
+    final static int TYPE_CNAME = 5;           // Canonical Name
+    final static int TYPE_SOA = 6;             // Start of Authority
+    final static int TYPE_MB = 7;              // Mailbox
+    final static int TYPE_MG = 8;              // Mail Group
+    final static int TYPE_MR = 9;              // Mail Rename
+    final static int TYPE_NULL = 10;           // NULL RR
+    final static int TYPE_WKS = 11;            // Well-known-service
+    final static int TYPE_PTR = 12;            // Domain Name pofinal static 
inter
+    final static int TYPE_HINFO = 13;          // Host information
+    final static int TYPE_MINFO = 14;          // Mailbox information
+    final static int TYPE_MX = 15;             // Mail exchanger
+    final static int TYPE_TXT = 16;            // Arbitrary text string
+    final static int TYPE_RP = 17;             // for Responsible Person       
          [RFC1183]
+    final static int TYPE_AFSDB = 18;          // for AFS Data Base location   
          [RFC1183]
+    final static int TYPE_X25 = 19;            // for X.25 PSDN address        
          [RFC1183]
+    final static int TYPE_ISDN = 20;           // for ISDN address             
          [RFC1183]
+    final static int TYPE_RT = 21;             // for Route Through            
          [RFC1183]
+    final static int TYPE_NSAP = 22;           // for NSAP address, NSAP style 
A record  [RFC1706]
+    final static int TYPE_NSAP_PTR = 23;               //
+    final static int TYPE_SIG = 24;            // for security signature       
          [RFC2931]
+    final static int TYPE_KEY = 25;            // for security key             
          [RFC2535]
+    final static int TYPE_PX = 26;             // X.400 mail mapping 
information         [RFC2163]
+    final static int TYPE_GPOS = 27;           // Geographical Position        
          [RFC1712]
+    final static int TYPE_AAAA = 28;           // IP6 Address                  
          [Thomson]
+    final static int TYPE_LOC = 29;            // Location Information         
          [Vixie]
+    final static int TYPE_NXT = 30;            // Next Domain - OBSOLETE       
          [RFC2535, RFC3755]
+    final static int TYPE_EID = 31;            // Endpoint Identifier          
          [Patton]
+    final static int TYPE_NIMLOC = 32;                 // Nimrod Locator       
                  [Patton]
+    final static int TYPE_SRV = 33;            // Server Selection             
          [RFC2782]
+    final static int TYPE_ATMA = 34;           // ATM Address                  
          [Dobrowski]
+    final static int TYPE_NAPTR = 35;          // Naming Authority Pointer     
          [RFC2168, RFC2915]
+    final static int TYPE_KX = 36;             // Key Exchanger                
          [RFC2230]
+    final static int TYPE_CERT = 37;           // CERT                         
          [RFC2538]
+    final static int TYPE_A6 = 38;             // A6                           
          [RFC2874]
+    final static int TYPE_DNAME = 39;          // DNAME                        
          [RFC2672]
+    final static int TYPE_SINK = 40;           // SINK                         
          [Eastlake]
+    final static int TYPE_OPT = 41;            // OPT                          
          [RFC2671]
+    final static int TYPE_APL = 42;            // APL                          
          [RFC3123]
+    final static int TYPE_DS = 43;             // Delegation Signer            
          [RFC3658]
+    final static int TYPE_SSHFP = 44;          // SSH Key Fingerprint          
          [RFC-ietf-secsh-dns-05.txt]
+    final static int TYPE_RRSIG = 46;          // RRSIG                        
          [RFC3755]
+    final static int TYPE_NSEC = 47;           // NSEC                         
          [RFC3755]
+    final static int TYPE_DNSKEY = 48;         // DNSKEY                       
          [RFC3755]
+    final static int TYPE_UINFO = 100;      //                                 
                                      [IANA-Reserved]
+    final static int TYPE_UID = 101;      //                                   
     [IANA-Reserved]
+    final static int TYPE_GID = 102;      //                                   
     [IANA-Reserved]
+    final static int TYPE_UNSPEC = 103;      //                                
        [IANA-Reserved]
+    final static int TYPE_TKEY = 249;          // Transaction Key              
          [RFC2930]
+    final static int TYPE_TSIG = 250;          // Transaction Signature        
          [RFC2845]
+    final static int TYPE_IXFR = 251;          // Incremental transfer         
          [RFC1995]
+    final static int TYPE_AXFR = 252;          // Transfer of an entire zone   
          [RFC1035]
+    final static int TYPE_MAILA = 253;         // Mailbox-related records (MB, 
MG or MR) [RFC1035]
+    final static int TYPE_MAILB = 254;         // Mail agent RRs (Obsolete - 
see MX)     [RFC1035]
+    final static int TYPE_ANY = 255;           // Request for all records      
                  [RFC1035]
+    
+    //Time Intervals for various functions
+    
+    final static int SHARED_QUERY_TIME = 20;            //milliseconds before 
send shared query
+    final static int QUERY_WAIT_INTERVAL = 225;           //milliseconds 
between query loops.
+    final static int PROBE_WAIT_INTERVAL = 250;           //milliseconds 
between probe loops.
+    final static int RESPONSE_MIN_WAIT_INTERVAL = 20;            //minimal 
wait interval for response.
+    final static int RESPONSE_MAX_WAIT_INTERVAL = 115;           //maximal 
wait interval for response
+    final static int PROBE_CONFLICT_INTERVAL = 1000;          //milliseconds 
to wait after conflict.
+    final static int PROBE_THROTTLE_COUNT = 10;            //After x tries go 
1 time a sec. on probes.
+    final static int PROBE_THROTTLE_COUNT_INTERVAL = 5000;          //We only 
increment the throttle count, if
+    // the previous increment is inside this interval.
+    final static int ANNOUNCE_WAIT_INTERVAL = 1000;          //milliseconds 
between Announce loops.
+    final static int RECORD_REAPER_INTERVAL = 10000;         //milliseconds 
between cache cleanups.
+    final static int KNOWN_ANSWER_TTL = 120;
+    final static int ANNOUNCED_RENEWAL_TTL_INTERVAL = DNS_TTL * 500; // 50% of 
the TTL in milliseconds
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java                       
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java       2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,148 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.logging.Logger;
+
+/**
+ * DNS entry with a name, type, and class. This is the base
+ * class for questions and records.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Pierre Frisch, Rick Blair
+ */
+class DNSEntry
+{
+    private static Logger logger = Logger.getLogger(DNSEntry.class.toString());
+    String key;
+    String name;
+    int type;
+    int clazz;
+    boolean unique;
+
+    /**
+     * Create an entry.
+     */
+    DNSEntry(String name, int type, int clazz)
+    {
+        this.key = name.toLowerCase();
+        this.name = name;
+        this.type = type;
+        this.clazz = clazz & DNSConstants.CLASS_MASK;
+        this.unique = (clazz & DNSConstants.CLASS_UNIQUE) != 0;
+    }
+
+    /**
+     * Check if two entries have exactly the same name, type, and class.
+     */
+    public boolean equals(Object obj)
+    {
+        if (obj instanceof DNSEntry)
+        {
+            DNSEntry other = (DNSEntry) obj;
+            return name.equals(other.name) && type == other.type && clazz == 
other.clazz;
+        }
+        return false;
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public int getType()
+    {
+        return type;
+    }
+
+    /**
+     * Overriden, to return a value which is consistent with the value returned
+     * by equals(Object).
+     */
+    public int hashCode()
+    {
+        return name.hashCode() + type + clazz;
+    }
+
+    /**
+     * Get a string given a clazz.
+     */
+    static String getClazz(int clazz)
+    {
+        switch (clazz & DNSConstants.CLASS_MASK)
+        {
+            case DNSConstants.CLASS_IN:
+                return "in";
+            case DNSConstants.CLASS_CS:
+                return "cs";
+            case DNSConstants.CLASS_CH:
+                return "ch";
+            case DNSConstants.CLASS_HS:
+                return "hs";
+            case DNSConstants.CLASS_NONE:
+                return "none";
+            case DNSConstants.CLASS_ANY:
+                return "any";
+            default:
+                return "?";
+        }
+    }
+
+    /**
+     * Get a string given a type.
+     */
+    static String getType(int type)
+    {
+        switch (type)
+        {
+            case DNSConstants.TYPE_A:
+                return "a";
+            case DNSConstants.TYPE_AAAA:
+                return "aaaa";
+            case DNSConstants.TYPE_NS:
+                return "ns";
+            case DNSConstants.TYPE_MD:
+                return "md";
+            case DNSConstants.TYPE_MF:
+                return "mf";
+            case DNSConstants.TYPE_CNAME:
+                return "cname";
+            case DNSConstants.TYPE_SOA:
+                return "soa";
+            case DNSConstants.TYPE_MB:
+                return "mb";
+            case DNSConstants.TYPE_MG:
+                return "mg";
+            case DNSConstants.TYPE_MR:
+                return "mr";
+            case DNSConstants.TYPE_NULL:
+                return "null";
+            case DNSConstants.TYPE_WKS:
+                return "wks";
+            case DNSConstants.TYPE_PTR:
+                return "ptr";
+            case DNSConstants.TYPE_HINFO:
+                return "hinfo";
+            case DNSConstants.TYPE_MINFO:
+                return "minfo";
+            case DNSConstants.TYPE_MX:
+                return "mx";
+            case DNSConstants.TYPE_TXT:
+                return "txt";
+            case DNSConstants.TYPE_SRV:
+                return "srv";
+            case DNSConstants.TYPE_ANY:
+                return "any";
+            default:
+                return "?";
+        }
+    }
+
+    public String toString(String hdr, String other)
+    {
+        return hdr + "[" + getType(type) + "," + getClazz(clazz) + (unique ? 
"-unique," : ",") + name + ((other != null) ? "," + other + "]" : "]");
+    }
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java                    
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java    2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,466 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Parse an incoming DNS message into its components.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Werner Randelshofer, Pierre Frisch
+ */
+final class DNSIncoming
+{
+    private static Logger logger = 
Logger.getLogger(DNSIncoming.class.toString());
+    // Implementation note: This vector should be immutable.
+    // If a client of DNSIncoming changes the contents of this vector,
+    // we get undesired results. To fix this, we have to migrate to
+    // the Collections API of Java 1.2. i.e we replace Vector by List.
+    // final static Vector EMPTY = new Vector();
+    
+    private DatagramPacket packet;
+    private int off;
+    private int len;
+    private byte data[];
+
+    int id;
+    private int flags;
+    private int numQuestions;
+    int numAnswers;
+    private int numAuthorities;
+    private int numAdditionals;
+    private long receivedTime;
+
+    List questions;
+    List answers;
+
+    /**
+     * Parse a message from a datagram packet.
+     */
+    DNSIncoming(DatagramPacket packet) throws IOException
+    {
+        this.packet = packet;
+        this.data = packet.getData();
+        this.len = packet.getLength();
+        this.off = packet.getOffset();
+        this.questions = Collections.EMPTY_LIST;
+        this.answers = Collections.EMPTY_LIST;
+        this.receivedTime = System.currentTimeMillis();
+
+        try
+        {
+            id = readUnsignedShort();
+            flags = readUnsignedShort();
+            numQuestions = readUnsignedShort();
+            numAnswers = readUnsignedShort();
+            numAuthorities = readUnsignedShort();
+            numAdditionals = readUnsignedShort();
+
+            // parse questions
+            if (numQuestions > 0)
+            {
+                questions = Collections.synchronizedList(new 
ArrayList(numQuestions));
+                for (int i = 0; i < numQuestions; i++)
+                {
+                    DNSQuestion question = new DNSQuestion(readName(), 
readUnsignedShort(), readUnsignedShort());
+                    questions.add(question);
+                }
+            }
+
+            // parse answers
+            int n = numAnswers + numAuthorities + numAdditionals;
+            if (n > 0)
+            {
+                answers = Collections.synchronizedList(new ArrayList(n));
+                for (int i = 0; i < n; i++)
+                {
+                    String domain = readName();
+                    int type = readUnsignedShort();
+                    int clazz = readUnsignedShort();
+                    int ttl = readInt();
+                    int len = readUnsignedShort();
+                    int end = off + len;
+                    DNSRecord rec = null;
+
+                    switch (type)
+                    {
+                        case DNSConstants.TYPE_A:              // IPv4
+                        case DNSConstants.TYPE_AAAA:   // IPv6 FIXME [PJYF Oct 
14 2004] This has not been tested
+                            rec = new DNSRecord.Address(domain, type, clazz, 
ttl, readBytes(off, len));
+                            break;
+                        case DNSConstants.TYPE_CNAME:
+                        case DNSConstants.TYPE_PTR:
+                            rec = new DNSRecord.Pointer(domain, type, clazz, 
ttl, readName());
+                            break;
+                        case DNSConstants.TYPE_TXT:
+                            rec = new DNSRecord.Text(domain, type, clazz, ttl, 
readBytes(off, len));
+                            break;
+                        case DNSConstants.TYPE_SRV:
+                            rec = new DNSRecord.Service(domain, type, clazz, 
ttl,
+                                readUnsignedShort(), readUnsignedShort(), 
readUnsignedShort(), readName());
+                            break;
+                        case DNSConstants.TYPE_HINFO:
+                            // Maybe we should do something with those
+                            break;
+                        default :
+                            logger.finer("DNSIncoming() unknown type:" + type);
+                            break;
+                    }
+
+                    if (rec != null)
+                    {
+                        // Add a record, if we were able to create one.
+                        answers.add(rec);
+                    }
+                    else
+                    {
+                        // Addjust the numbers for the skipped record
+                        if (answers.size() < numAnswers)
+                        {
+                            numAnswers--;
+                        }
+                        else
+                        {
+                            if (answers.size() < numAnswers + numAuthorities)
+                            {
+                                numAuthorities--;
+                            }
+                            else
+                            {
+                                if (answers.size() < numAnswers + 
numAuthorities + numAdditionals)
+                                {
+                                    numAdditionals--;
+                                }
+                            }
+                        }
+                    }
+                    off = end;
+                }
+            }
+        }
+        catch (IOException e)
+        {
+            logger.log(Level.WARNING, "DNSIncoming() dump " + print(true) + 
"\n exception ", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Check if the message is a query.
+     */
+    boolean isQuery()
+    {
+        return (flags & DNSConstants.FLAGS_QR_MASK) == 
DNSConstants.FLAGS_QR_QUERY;
+    }
+
+    /**
+     * Check if the message is truncated.
+     */
+    boolean isTruncated()
+    {
+        return (flags & DNSConstants.FLAGS_TC) != 0;
+    }
+
+    /**
+     * Check if the message is a response.
+     */
+    boolean isResponse()
+    {
+        return (flags & DNSConstants.FLAGS_QR_MASK) == 
DNSConstants.FLAGS_QR_RESPONSE;
+    }
+
+    private int get(int off) throws IOException
+    {
+        if ((off < 0) || (off >= len))
+        {
+            throw new IOException("parser error: offset=" + off);
+        }
+        return data[off] & 0xFF;
+    }
+
+    private int readUnsignedShort() throws IOException
+    {
+        return (get(off++) << 8) + get(off++);
+    }
+
+    private int readInt() throws IOException
+    {
+        return (readUnsignedShort() << 16) + readUnsignedShort();
+    }
+
+    private byte[] readBytes(int off, int len) throws IOException
+    {
+        byte bytes[] = new byte[len];
+        System.arraycopy(data, off, bytes, 0, len);
+        return bytes;
+    }
+
+    private void readUTF(StringBuffer buf, int off, int len) throws IOException
+    {
+        for (int end = off + len; off < end;)
+        {
+            int ch = get(off++);
+            switch (ch >> 4)
+            {
+                case 0:
+                case 1:
+                case 2:
+                case 3:
+                case 4:
+                case 5:
+                case 6:
+                case 7:
+                    // 0xxxxxxx
+                    break;
+                case 12:
+                case 13:
+                    // 110x xxxx   10xx xxxx
+                    ch = ((ch & 0x1F) << 6) | (get(off++) & 0x3F);
+                    break;
+                case 14:
+                    // 1110 xxxx  10xx xxxx  10xx xxxx
+                    ch = ((ch & 0x0f) << 12) | ((get(off++) & 0x3F) << 6) | 
(get(off++) & 0x3F);
+                    break;
+                default:
+                    // 10xx xxxx,  1111 xxxx
+                    ch = ((ch & 0x3F) << 4) | (get(off++) & 0x0f);
+                    break;
+            }
+            buf.append((char) ch);
+        }
+    }
+
+    private String readName() throws IOException
+    {
+        StringBuffer buf = new StringBuffer();
+        int off = this.off;
+        int next = -1;
+        int first = off;
+
+        while (true)
+        {
+            int len = get(off++);
+            if (len == 0)
+            {
+                break;
+            }
+            switch (len & 0xC0)
+            {
+                case 0x00:
+                    //buf.append("[" + off + "]");
+                    readUTF(buf, off, len);
+                    off += len;
+                    buf.append('.');
+                    break;
+                case 0xC0:
+                    //buf.append("<" + (off - 1) + ">");
+                    if (next < 0)
+                    {
+                        next = off + 1;
+                    }
+                    off = ((len & 0x3F) << 8) | get(off++);
+                    if (off >= first)
+                    {
+                        throw new IOException("bad domain name: possible 
circular name detected");
+                    }
+                    first = off;
+                    break;
+                default:
+                    throw new IOException("bad domain name: '" + buf + "' at " 
+ off);
+            }
+        }
+        this.off = (next >= 0) ? next : off;
+        return buf.toString();
+    }
+
+    /**
+     * Debugging.
+     */
+    String print(boolean dump)
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append(toString() + "\n");
+        for (Iterator iterator = questions.iterator(); iterator.hasNext();)
+        {
+            buf.append("    ques:" + iterator.next() + "\n");
+        }
+        int count = 0;
+        for (Iterator iterator = answers.iterator(); iterator.hasNext(); 
count++)
+        {
+            if (count < numAnswers)
+            {
+                buf.append("    answ:");
+            }
+            else
+            {
+                if (count < numAnswers + numAuthorities)
+                {
+                    buf.append("    auth:");
+                }
+                else
+                {
+                    buf.append("    addi:");
+                }
+            }
+            buf.append(iterator.next() + "\n");
+        }
+        if (dump)
+        {
+            for (int off = 0, len = packet.getLength(); off < len; off += 32)
+            {
+                int n = Math.min(32, len - off);
+                if (off < 10)
+                {
+                    buf.append(' ');
+                }
+                if (off < 100)
+                {
+                    buf.append(' ');
+                }
+                buf.append(off);
+                buf.append(':');
+                for (int i = 0; i < n; i++)
+                {
+                    if ((i % 8) == 0)
+                    {
+                        buf.append(' ');
+                    }
+                    buf.append(Integer.toHexString((data[off + i] & 0xF0) >> 
4));
+                    buf.append(Integer.toHexString((data[off + i] & 0x0F) >> 
0));
+                }
+                buf.append("\n");
+                buf.append("    ");
+                for (int i = 0; i < n; i++)
+                {
+                    if ((i % 8) == 0)
+                    {
+                        buf.append(' ');
+                    }
+                    buf.append(' ');
+                    int ch = data[off + i] & 0xFF;
+                    buf.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.');
+                }
+                buf.append("\n");
+
+                // limit message size
+                if (off + 32 >= 256)
+                {
+                    buf.append("....\n");
+                    break;
+                }
+            }
+        }
+        return buf.toString();
+    }
+
+    public String toString()
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append(isQuery() ? "dns[query," : "dns[response,");
+        if (packet.getAddress() != null)
+        {
+            buf.append(packet.getAddress().getHostAddress());
+        }
+        buf.append(':');
+        buf.append(packet.getPort());
+        buf.append(",len=");
+        buf.append(packet.getLength());
+        buf.append(",id=0x");
+        buf.append(Integer.toHexString(id));
+        if (flags != 0)
+        {
+            buf.append(",flags=0x");
+            buf.append(Integer.toHexString(flags));
+            if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0)
+            {
+                buf.append(":r");
+            }
+            if ((flags & DNSConstants.FLAGS_AA) != 0)
+            {
+                buf.append(":aa");
+            }
+            if ((flags & DNSConstants.FLAGS_TC) != 0)
+            {
+                buf.append(":tc");
+            }
+        }
+        if (numQuestions > 0)
+        {
+            buf.append(",questions=");
+            buf.append(numQuestions);
+        }
+        if (numAnswers > 0)
+        {
+            buf.append(",answers=");
+            buf.append(numAnswers);
+        }
+        if (numAuthorities > 0)
+        {
+            buf.append(",authorities=");
+            buf.append(numAuthorities);
+        }
+        if (numAdditionals > 0)
+        {
+            buf.append(",additionals=");
+            buf.append(numAdditionals);
+        }
+        buf.append("]");
+        return buf.toString();
+    }
+
+    /**
+     * Appends answers to this Incoming.
+     *
+     * @throws IllegalArgumentException If not a query or if Truncated.
+     */
+    void append(DNSIncoming that)
+    {
+        if (this.isQuery() && this.isTruncated() && that.isQuery())
+        {
+            this.questions.addAll(that.questions);
+            this.numQuestions += that.numQuestions;
+
+            if (Collections.EMPTY_LIST.equals(answers))
+            {
+                answers = Collections.synchronizedList(new ArrayList());
+            }
+
+            if (that.numAnswers > 0)
+            {
+                this.answers.addAll(this.numAnswers, that.answers.subList(0, 
that.numAnswers));
+                this.numAnswers += that.numAnswers;
+            }
+            if (that.numAuthorities > 0)
+            {
+                this.answers.addAll(this.numAnswers + this.numAuthorities, 
that.answers.subList(that.numAnswers, that.numAnswers + that.numAuthorities));
+                this.numAuthorities += that.numAuthorities;
+            }
+            if (that.numAdditionals > 0)
+            {
+                this.answers.addAll(that.answers.subList(that.numAnswers + 
that.numAuthorities, that.numAnswers + that.numAuthorities + 
that.numAdditionals));
+                this.numAdditionals += that.numAdditionals;
+            }
+        }
+        else
+        {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    int elapseSinceArrival()
+    {
+        return (int) (System.currentTimeMillis() - receivedTime);
+    }
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java                    
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java    2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,24 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+// REMIND: Listener should follow Java idiom for listener or have a different
+//         name.
+
+/**
+ * DNSListener.
+ * Listener for record updates.
+ *
+ * @author Werner Randelshofer, Rick Blair
+ * @version 1.0  May 22, 2004  Created.
+ */
+interface DNSListener
+{
+    /**
+     * Update a DNS record.
+     */
+    void updateRecord(JmDNS jmdns, long now, DNSRecord record);
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java                    
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java    2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,381 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.io.IOException;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An outgoing DNS message.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Rick Blair, Werner Randelshofer
+ */
+final class DNSOutgoing
+{
+    private static Logger logger = 
Logger.getLogger(DNSOutgoing.class.toString());
+    int id;
+    int flags;
+    private boolean multicast;
+    private int numQuestions;
+    private int numAnswers;
+    private int numAuthorities;
+    private int numAdditionals;
+    private Hashtable names;
+
+    byte data[];
+    int off;
+    int len;
+
+    /**
+     * Create an outgoing multicast query or response.
+     */
+    DNSOutgoing(int flags)
+    {
+        this(flags, true);
+    }
+
+    /**
+     * Create an outgoing query or response.
+     */
+    DNSOutgoing(int flags, boolean multicast)
+    {
+        this.flags = flags;
+        this.multicast = multicast;
+        names = new Hashtable();
+        data = new byte[DNSConstants.MAX_MSG_TYPICAL];
+        off = 12;
+    }
+
+    /**
+     * Add a question to the message.
+     */
+    void addQuestion(DNSQuestion rec) throws IOException
+    {
+        if (numAnswers > 0 || numAuthorities > 0 || numAdditionals > 0)
+        {
+            throw new IllegalStateException("Questions must be added before 
answers");
+        }
+        numQuestions++;
+        writeQuestion(rec);
+    }
+
+    /**
+     * Add an answer if it is not suppressed.
+     */
+    void addAnswer(DNSIncoming in, DNSRecord rec) throws IOException
+    {
+        if (numAuthorities > 0 || numAdditionals > 0)
+        {
+            throw new IllegalStateException("Answers must be added before 
authorities and additionals");
+        }
+        if (!rec.suppressedBy(in))
+        {
+            addAnswer(rec, 0);
+        }
+    }
+
+    /**
+     * Add an additional answer to the record. Omit if there is no room.
+     */
+    void addAdditionalAnswer(DNSIncoming in, DNSRecord rec) throws IOException
+    {
+        if ((off < DNSConstants.MAX_MSG_TYPICAL - 200) && 
!rec.suppressedBy(in))
+        {
+            writeRecord(rec, 0);
+            numAdditionals++;
+        }
+    }
+
+    /**
+     * Add an answer to the message.
+     */
+    void addAnswer(DNSRecord rec, long now) throws IOException
+    {
+        if (numAuthorities > 0 || numAdditionals > 0)
+        {
+            throw new IllegalStateException("Questions must be added before 
answers");
+        }
+        if (rec != null)
+        {
+            if ((now == 0) || !rec.isExpired(now))
+            {
+                writeRecord(rec, now);
+                numAnswers++;
+            }
+        }
+    }
+
+    private LinkedList authorativeAnswers = new LinkedList();
+
+    /**
+     * Add an authorative answer to the message.
+     */
+    void addAuthorativeAnswer(DNSRecord rec) throws IOException
+    {
+        if (numAdditionals > 0)
+        {
+            throw new IllegalStateException("Authorative answers must be added 
before additional answers");
+        }
+        authorativeAnswers.add(rec);
+        writeRecord(rec, 0);
+        numAuthorities++;
+
+        // VERIFY:
+
+    }
+
+    void writeByte(int value) throws IOException
+    {
+        if (off >= data.length)
+        {
+            throw new IOException("buffer full");
+        }
+        data[off++] = (byte) value;
+    }
+
+    void writeBytes(String str, int off, int len) throws IOException
+    {
+        for (int i = 0; i < len; i++)
+        {
+            writeByte(str.charAt(off + i));
+        }
+    }
+
+    void writeBytes(byte data[]) throws IOException
+    {
+        if (data != null)
+        {
+            writeBytes(data, 0, data.length);
+        }
+    }
+
+    void writeBytes(byte data[], int off, int len) throws IOException
+    {
+        for (int i = 0; i < len; i++)
+        {
+            writeByte(data[off + i]);
+        }
+    }
+
+    void writeShort(int value) throws IOException
+    {
+        writeByte(value >> 8);
+        writeByte(value);
+    }
+
+    void writeInt(int value) throws IOException
+    {
+        writeShort(value >> 16);
+        writeShort(value);
+    }
+
+    void writeUTF(String str, int off, int len) throws IOException
+    {
+        // compute utf length
+        int utflen = 0;
+        for (int i = 0; i < len; i++)
+        {
+            int ch = str.charAt(off + i);
+            if ((ch >= 0x0001) && (ch <= 0x007F))
+            {
+                utflen += 1;
+            }
+            else
+            {
+                if (ch > 0x07FF)
+                {
+                    utflen += 3;
+                }
+                else
+                {
+                    utflen += 2;
+                }
+            }
+        }
+        // write utf length
+        writeByte(utflen);
+        // write utf data
+        for (int i = 0; i < len; i++)
+        {
+            int ch = str.charAt(off + i);
+            if ((ch >= 0x0001) && (ch <= 0x007F))
+            {
+                writeByte(ch);
+            }
+            else
+            {
+                if (ch > 0x07FF)
+                {
+                    writeByte(0xE0 | ((ch >> 12) & 0x0F));
+                    writeByte(0x80 | ((ch >> 6) & 0x3F));
+                    writeByte(0x80 | ((ch >> 0) & 0x3F));
+                }
+                else
+                {
+                    writeByte(0xC0 | ((ch >> 6) & 0x1F));
+                    writeByte(0x80 | ((ch >> 0) & 0x3F));
+                }
+            }
+        }
+    }
+
+    void writeName(String name) throws IOException
+    {
+        while (true)
+        {
+            int n = name.indexOf('.');
+            if (n < 0)
+            {
+                n = name.length();
+            }
+            if (n <= 0)
+            {
+                writeByte(0);
+                return;
+            }
+            Integer offset = (Integer) names.get(name);
+            if (offset != null)
+            {
+                int val = offset.intValue();
+
+                if (val > off)
+                {
+                    logger.log(Level.WARNING, "DNSOutgoing writeName failed 
val=" + val + " name=" + name);
+                }
+
+                writeByte((val >> 8) | 0xC0);
+                writeByte(val);
+                return;
+            }
+            names.put(name, new Integer(off));
+            writeUTF(name, 0, n);
+            name = name.substring(n);
+            if (name.startsWith("."))
+            {
+                name = name.substring(1);
+            }
+        }
+    }
+
+    void writeQuestion(DNSQuestion question) throws IOException
+    {
+        writeName(question.name);
+        writeShort(question.type);
+        writeShort(question.clazz);
+    }
+
+    void writeRecord(DNSRecord rec, long now) throws IOException
+    {
+        int save = off;
+        try
+        {
+            writeName(rec.name);
+            writeShort(rec.type);
+            writeShort(rec.clazz | ((rec.unique && multicast) ? 
DNSConstants.CLASS_UNIQUE : 0));
+            writeInt((now == 0) ? rec.ttl : rec.getRemainingTTL(now));
+            writeShort(0);
+            int start = off;
+            rec.write(this);
+            int len = off - start;
+            data[start - 2] = (byte) (len >> 8);
+            data[start - 1] = (byte) (len & 0xFF);
+        }
+        catch (IOException e)
+        {
+            off = save;
+            throw e;
+        }
+    }
+
+    /**
+     * Finish the message before sending it off.
+     */
+    void finish() throws IOException
+    {
+        int save = off;
+        off = 0;
+
+        writeShort(multicast ? 0 : id);
+        writeShort(flags);
+        writeShort(numQuestions);
+        writeShort(numAnswers);
+        writeShort(numAuthorities);
+        writeShort(numAdditionals);
+        off = save;
+    }
+
+    boolean isQuery()
+    {
+        return (flags & DNSConstants.FLAGS_QR_MASK) == 
DNSConstants.FLAGS_QR_QUERY;
+    }
+
+    public boolean isEmpty()
+    {
+        return numQuestions == 0 && numAuthorities == 0
+            && numAdditionals == 0 && numAnswers == 0;
+    }
+
+
+    public String toString()
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append(isQuery() ? "dns[query," : "dns[response,");
+        //buf.append(packet.getAddress().getHostAddress());
+        buf.append(':');
+        //buf.append(packet.getPort());
+        //buf.append(",len=");
+        //buf.append(packet.getLength());
+        buf.append(",id=0x");
+        buf.append(Integer.toHexString(id));
+        if (flags != 0)
+        {
+            buf.append(",flags=0x");
+            buf.append(Integer.toHexString(flags));
+            if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0)
+            {
+                buf.append(":r");
+            }
+            if ((flags & DNSConstants.FLAGS_AA) != 0)
+            {
+                buf.append(":aa");
+            }
+            if ((flags & DNSConstants.FLAGS_TC) != 0)
+            {
+                buf.append(":tc");
+            }
+        }
+        if (numQuestions > 0)
+        {
+            buf.append(",questions=");
+            buf.append(numQuestions);
+        }
+        if (numAnswers > 0)
+        {
+            buf.append(",answers=");
+            buf.append(numAnswers);
+        }
+        if (numAuthorities > 0)
+        {
+            buf.append(",authorities=");
+            buf.append(numAuthorities);
+        }
+        if (numAdditionals > 0)
+        {
+            buf.append(",additionals=");
+            buf.append(numAdditionals);
+        }
+        buf.append(",\nnames=" + names);
+        buf.append(",\nauthorativeAnswers=" + authorativeAnswers);
+
+        buf.append("]");
+        return buf.toString();
+    }
+
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java                    
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java    2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,44 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.logging.Logger;
+
+/**
+ * A DNS question.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff
+ */
+final class DNSQuestion extends DNSEntry
+{
+    private static Logger logger = 
Logger.getLogger(DNSQuestion.class.toString());
+
+    /**
+     * Create a question.
+     */
+    DNSQuestion(String name, int type, int clazz)
+    {
+        super(name, type, clazz);
+    }
+
+    /**
+     * Check if this question is answered by a given DNS record.
+     */
+    boolean answeredBy(DNSRecord rec)
+    {
+        return (clazz == rec.clazz) && ((type == rec.type) || (type == 
DNSConstants.TYPE_ANY)) &&
+            name.equals(rec.name);
+    }
+
+    /**
+     * For debugging only.
+     */
+    public String toString()
+    {
+        return toString("question", null);
+    }
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java                      
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java      2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,673 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * DNS record
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch
+ */
+abstract class DNSRecord extends DNSEntry
+{
+    private static Logger logger = 
Logger.getLogger(DNSRecord.class.toString());
+    int ttl;
+    private long created;
+
+    /**
+     * Create a DNSRecord with a name, type, clazz, and ttl.
+     */
+    DNSRecord(String name, int type, int clazz, int ttl)
+    {
+        super(name, type, clazz);
+        this.ttl = ttl;
+        this.created = System.currentTimeMillis();
+    }
+
+    /**
+     * True if this record is the same as some other record.
+     */
+    public boolean equals(Object other)
+    {
+        return (other instanceof DNSRecord) && sameAs((DNSRecord) other);
+    }
+
+    /**
+     * True if this record is the same as some other record.
+     */
+    boolean sameAs(DNSRecord other)
+    {
+        return super.equals(other) && sameValue((DNSRecord) other);
+    }
+
+    /**
+     * True if this record has the same value as some other record.
+     */
+    abstract boolean sameValue(DNSRecord other);
+
+    /**
+     * True if this record has the same type as some other record.
+     */
+    boolean sameType(DNSRecord other)
+    {
+        return type == other.type;
+    }
+
+    /**
+     * Handles a query represented by this record.
+     *
+     * @return Returns true if a conflict with one of the services registered
+     *         with JmDNS or with the hostname occured.
+     */
+    abstract boolean handleQuery(JmDNS dns, long expirationTime);
+
+    /**
+     * Handles a responserepresented by this record.
+     *
+     * @return Returns true if a conflict with one of the services registered
+     *         with JmDNS or with the hostname occured.
+     */
+    abstract boolean handleResponse(JmDNS dns);
+
+    /**
+     * Adds this as an answer to the provided outgoing datagram.
+     */
+    abstract DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress 
addr, int port, DNSOutgoing out) throws IOException;
+
+    /**
+     * True if this record is suppressed by the answers in a message.
+     */
+    boolean suppressedBy(DNSIncoming msg)
+    {
+        try
+        {
+            for (int i = msg.numAnswers; i-- > 0;)
+            {
+                if (suppressedBy((DNSRecord) msg.answers.get(i)))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+        catch (ArrayIndexOutOfBoundsException e)
+        {
+            logger.log(Level.WARNING, "suppressedBy() message " + msg + " 
exception ", e);
+            // msg.print(true);
+            return false;
+        }
+    }
+
+    /**
+     * True if this record would be supressed by an answer.
+     * This is the case if this record would not have a
+     * significantly longer TTL.
+     */
+    boolean suppressedBy(DNSRecord other)
+    {
+        if (sameAs(other) && (other.ttl > ttl / 2))
+        {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get the expiration time of this record.
+     */
+    long getExpirationTime(int percent)
+    {
+        return created + (percent * ttl * 10L);
+    }
+
+    /**
+     * Get the remaining TTL for this record.
+     */
+    int getRemainingTTL(long now)
+    {
+        return (int) Math.max(0, (getExpirationTime(100) - now) / 1000);
+    }
+
+    /**
+     * Check if the record is expired.
+     */
+    boolean isExpired(long now)
+    {
+        return getExpirationTime(100) <= now;
+    }
+
+    /**
+     * Check if the record is stale, ie it has outlived
+     * more than half of its TTL.
+     */
+    boolean isStale(long now)
+    {
+        return getExpirationTime(50) <= now;
+    }
+
+    /**
+     * Reset the TTL of a record. This avoids having to
+     * update the entire record in the cache.
+     */
+    void resetTTL(DNSRecord other)
+    {
+        created = other.created;
+        ttl = other.ttl;
+    }
+
+    /**
+     * Write this record into an outgoing message.
+     */
+    abstract void write(DNSOutgoing out) throws IOException;
+
+    /**
+     * Address record.
+     */
+    static class Address extends DNSRecord
+    {
+        private static Logger logger = 
Logger.getLogger(Address.class.toString());
+        InetAddress addr;
+
+        Address(String name, int type, int clazz, int ttl, InetAddress addr)
+        {
+            super(name, type, clazz, ttl);
+            this.addr = addr;
+        }
+
+        Address(String name, int type, int clazz, int ttl, byte[] rawAddress)
+        {
+            super(name, type, clazz, ttl);
+            try
+            {
+                this.addr = InetAddress.getByAddress(rawAddress);
+            }
+            catch (UnknownHostException exception)
+            {
+                logger.log(Level.WARNING, "Address() exception ", exception);
+            }
+        }
+
+        void write(DNSOutgoing out) throws IOException
+        {
+            if (addr != null)
+            {
+                byte[] buffer = addr.getAddress();
+                if (DNSConstants.TYPE_A == type)
+                {
+                    // If we have a type A records we should answer with a 
IPv4 address
+                    if (addr instanceof Inet4Address)
+                    {
+                        // All is good
+                    }
+                    else
+                    {
+                        // Get the last four bytes
+                        byte[] tempbuffer = buffer;
+                        buffer = new byte[4];
+                        System.arraycopy(tempbuffer, 12, buffer, 0, 4);
+                    }
+                }
+                else
+                {
+                    // If we have a type AAAA records we should answer with a 
IPv6 address
+                    if (addr instanceof Inet4Address)
+                    {
+                        byte[] tempbuffer = buffer;
+                        buffer = new byte[16];
+                        for (int i = 0; i < 16; i++)
+                        {
+                            if (i < 11)
+                            {
+                                buffer[i] = tempbuffer[i - 12];
+                            }
+                            else
+                            {
+                                buffer[i] = 0;
+                            }
+                        }
+                    }
+                }
+                int length = buffer.length;
+                out.writeBytes(buffer, 0, length);
+            }
+        }
+
+        boolean same(DNSRecord other)
+        {
+            return ((sameName(other)) && ((sameValue(other))));
+        }
+
+        boolean sameName(DNSRecord other)
+        {
+            return name.equalsIgnoreCase(((Address) other).name);
+        }
+
+        boolean sameValue(DNSRecord other)
+        {
+            return addr.equals(((Address) other).getAddress());
+        }
+
+        InetAddress getAddress()
+        {
+            return addr;
+        }
+
+        /**
+         * Creates a byte array representation of this record.
+         * This is needed for tie-break tests according to
+         * draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
+         */
+        private byte[] toByteArray()
+        {
+            try
+            {
+                ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                DataOutputStream dout = new DataOutputStream(bout);
+                dout.write(name.getBytes("UTF8"));
+                dout.writeShort(type);
+                dout.writeShort(clazz);
+                //dout.writeInt(len);
+                byte[] buffer = addr.getAddress();
+                for (int i = 0; i < buffer.length; i++)
+                {
+                    dout.writeByte(buffer[i]);
+                }
+                dout.close();
+                return bout.toByteArray();
+            }
+            catch (IOException e)
+            {
+                throw new InternalError();
+            }
+        }
+
+        /**
+         * Does a lexicographic comparison of the byte array representation
+         * of this record and that record.
+         * This is needed for tie-break tests according to
+         * draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
+         */
+        private int lexCompare(DNSRecord.Address that)
+        {
+            byte[] thisBytes = this.toByteArray();
+            byte[] thatBytes = that.toByteArray();
+            for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); 
i < n; i++)
+            {
+                if (thisBytes[i] > thatBytes[i])
+                {
+                    return 1;
+                }
+                else
+                {
+                    if (thisBytes[i] < thatBytes[i])
+                    {
+                        return -1;
+                    }
+                }
+            }
+            return thisBytes.length - thatBytes.length;
+        }
+
+        /**
+         * Does the necessary actions, when this as a query.
+         */
+        boolean handleQuery(JmDNS dns, long expirationTime)
+        {
+            DNSRecord.Address dnsAddress = 
dns.getLocalHost().getDNSAddressRecord(this);
+            if (dnsAddress != null)
+            {
+                if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && 
(!dnsAddress.sameValue(this)))
+                {
+                    logger.finer("handleQuery() Conflicting probe detected. 
dns state " + dns.getState() + " lex compare " + lexCompare(dnsAddress));
+                    // Tie-breaker test
+                    if (dns.getState().isProbing() && lexCompare(dnsAddress) 
>= 0)
+                    {
+                        // We lost the tie-break. We have to choose a 
different name.
+                        dns.getLocalHost().incrementHostName();
+                        dns.getCache().clear();
+                        for (Iterator i = dns.services.values().iterator(); 
i.hasNext();)
+                        {
+                            ServiceInfo info = (ServiceInfo) i.next();
+                            info.revertState();
+                        }
+                    }
+                    dns.revertState();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Does the necessary actions, when this as a response.
+         */
+        boolean handleResponse(JmDNS dns)
+        {
+            DNSRecord.Address dnsAddress = 
dns.getLocalHost().getDNSAddressRecord(this);
+            if (dnsAddress != null)
+            {
+                if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && 
(!dnsAddress.sameValue(this)))
+                {
+                    logger.finer("handleResponse() Denial detected");
+
+                    if (dns.getState().isProbing())
+                    {
+                        dns.getLocalHost().incrementHostName();
+                        dns.getCache().clear();
+                        for (Iterator i = dns.services.values().iterator(); 
i.hasNext();)
+                        {
+                            ServiceInfo info = (ServiceInfo) i.next();
+                            info.revertState();
+                        }
+                    }
+                    dns.revertState();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int 
port, DNSOutgoing out) throws IOException
+        {
+            return out;
+        }
+
+        public String toString()
+        {
+            return toString(" address '" + (addr != null ? 
addr.getHostAddress() : "null") + "'");
+        }
+
+    }
+
+    /**
+     * Pointer record.
+     */
+    static class Pointer extends DNSRecord
+    {
+        private static Logger logger = 
Logger.getLogger(Pointer.class.toString());
+        String alias;
+
+        Pointer(String name, int type, int clazz, int ttl, String alias)
+        {
+            super(name, type, clazz, ttl);
+            this.alias = alias;
+        }
+
+        void write(DNSOutgoing out) throws IOException
+        {
+            out.writeName(alias);
+        }
+
+        boolean sameValue(DNSRecord other)
+        {
+            return alias.equals(((Pointer) other).alias);
+        }
+
+        boolean handleQuery(JmDNS dns, long expirationTime)
+        {
+            // Nothing to do (?)
+            // I think there is no possibility for conflicts for this record 
type?
+            return false;
+        }
+
+        boolean handleResponse(JmDNS dns)
+        {
+            // Nothing to do (?)
+            // I think there is no possibility for conflicts for this record 
type?
+            return false;
+        }
+
+        String getAlias()
+        {
+            return alias;
+        }
+
+        DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int 
port, DNSOutgoing out) throws IOException
+        {
+            return out;
+        }
+
+        public String toString()
+        {
+            return toString(alias);
+        }
+    }
+
+    static class Text extends DNSRecord
+    {
+        private static Logger logger = Logger.getLogger(Text.class.toString());
+        byte text[];
+
+        Text(String name, int type, int clazz, int ttl, byte text[])
+        {
+            super(name, type, clazz, ttl);
+            this.text = text;
+        }
+
+        void write(DNSOutgoing out) throws IOException
+        {
+            out.writeBytes(text, 0, text.length);
+        }
+
+        boolean sameValue(DNSRecord other)
+        {
+            Text txt = (Text) other;
+            if (txt.text.length != text.length)
+            {
+                return false;
+            }
+            for (int i = text.length; i-- > 0;)
+            {
+                if (txt.text[i] != text[i])
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        boolean handleQuery(JmDNS dns, long expirationTime)
+        {
+            // Nothing to do (?)
+            // I think there is no possibility for conflicts for this record 
type?
+            return false;
+        }
+
+        boolean handleResponse(JmDNS dns)
+        {
+            // Nothing to do (?)
+            // Shouldn't we care if we get a conflict at this level?
+            /*
+                         ServiceInfo info = (ServiceInfo) 
dns.services.get(name.toLowerCase());
+                         if (info != null) {
+                                 if (! Arrays.equals(text,info.text)) {
+                                         info.revertState();
+                                         return true;
+                                 }
+                         }*/
+            return false;
+        }
+
+        DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int 
port, DNSOutgoing out) throws IOException
+        {
+            return out;
+        }
+
+        public String toString()
+        {
+            return toString((text.length > 10) ? new String(text, 0, 7) + 
"..." : new String(text));
+        }
+    }
+
+    /**
+     * Service record.
+     */
+    static class Service extends DNSRecord
+    {
+        private static Logger logger = 
Logger.getLogger(Service.class.toString());
+        int priority;
+        int weight;
+        int port;
+        String server;
+
+        Service(String name, int type, int clazz, int ttl, int priority, int 
weight, int port, String server)
+        {
+            super(name, type, clazz, ttl);
+            this.priority = priority;
+            this.weight = weight;
+            this.port = port;
+            this.server = server;
+        }
+
+        void write(DNSOutgoing out) throws IOException
+        {
+            out.writeShort(priority);
+            out.writeShort(weight);
+            out.writeShort(port);
+            out.writeName(server);
+        }
+
+        private byte[] toByteArray()
+        {
+            try
+            {
+                ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                DataOutputStream dout = new DataOutputStream(bout);
+                dout.write(name.getBytes("UTF8"));
+                dout.writeShort(type);
+                dout.writeShort(clazz);
+                //dout.writeInt(len);
+                dout.writeShort(priority);
+                dout.writeShort(weight);
+                dout.writeShort(port);
+                dout.write(server.getBytes("UTF8"));
+                dout.close();
+                return bout.toByteArray();
+            }
+            catch (IOException e)
+            {
+                throw new InternalError();
+            }
+        }
+
+        private int lexCompare(DNSRecord.Service that)
+        {
+            byte[] thisBytes = this.toByteArray();
+            byte[] thatBytes = that.toByteArray();
+            for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); 
i < n; i++)
+            {
+                if (thisBytes[i] > thatBytes[i])
+                {
+                    return 1;
+                }
+                else
+                {
+                    if (thisBytes[i] < thatBytes[i])
+                    {
+                        return -1;
+                    }
+                }
+            }
+            return thisBytes.length - thatBytes.length;
+        }
+
+        boolean sameValue(DNSRecord other)
+        {
+            Service s = (Service) other;
+            return (priority == s.priority) && (weight == s.weight) && (port 
== s.port) && server.equals(s.server);
+        }
+
+        boolean handleQuery(JmDNS dns, long expirationTime)
+        {
+            ServiceInfo info = (ServiceInfo) 
dns.services.get(name.toLowerCase());
+            if (info != null
+                && (port != info.port || 
!server.equalsIgnoreCase(dns.getLocalHost().getName())))
+            {
+                logger.finer("handleQuery() Conflicting probe detected");
+
+                // Tie breaker test
+                if (info.getState().isProbing() && lexCompare(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
+                    DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
+                    DNSConstants.DNS_TTL, info.priority,
+                    info.weight, info.port, dns.getLocalHost().getName())) >= 
0)
+                {
+                    // We lost the tie break
+                    String oldName = info.getQualifiedName().toLowerCase();
+                    info.setName(dns.incrementName(info.getName()));
+                    dns.services.remove(oldName);
+                    dns.services.put(info.getQualifiedName().toLowerCase(), 
info);
+                    logger.finer("handleQuery() Lost tie break: new unique 
name chosen:" + info.getName());
+
+                }
+                info.revertState();
+                return true;
+
+            }
+            return false;
+        }
+
+        boolean handleResponse(JmDNS dns)
+        {
+            ServiceInfo info = (ServiceInfo) 
dns.services.get(name.toLowerCase());
+            if (info != null
+                && (port != info.port || 
!server.equalsIgnoreCase(dns.getLocalHost().getName())))
+            {
+                logger.finer("handleResponse() Denial detected");
+
+                if (info.getState().isProbing())
+                {
+                    String oldName = info.getQualifiedName().toLowerCase();
+                    info.setName(dns.incrementName(info.getName()));
+                    dns.services.remove(oldName);
+                    dns.services.put(info.getQualifiedName().toLowerCase(), 
info);
+                    logger.finer("handleResponse() New unique name chose:" + 
info.getName());
+
+                }
+                info.revertState();
+                return true;
+            }
+            return false;
+        }
+
+        DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int 
port, DNSOutgoing out) throws IOException
+        {
+            ServiceInfo info = (ServiceInfo) 
dns.services.get(name.toLowerCase());
+            if (info != null)
+            {
+                if (this.port == info.port != 
server.equals(dns.getLocalHost().getName()))
+                {
+                    return dns.addAnswer(in, addr, port, out,
+                        new DNSRecord.Service(info.getQualifiedName(), 
DNSConstants.TYPE_SRV,
+                            DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
+                            DNSConstants.DNS_TTL, info.priority,
+                            info.weight, info.port, 
dns.getLocalHost().getName()));
+                }
+            }
+            return out;
+        }
+
+        public String toString()
+        {
+            return toString(server + ":" + port);
+        }
+    }
+
+    public String toString(String other)
+    {
+        return toString("record", ttl + "/" + 
getRemainingTTL(System.currentTimeMillis()) + "," + other);
+    }
+}
+

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java                       
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java       2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,111 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+/**
+ * DNSState defines the possible states for services registered with JmDNS.
+ *
+ * @author Werner Randelshofer, Rick Blair
+ * @version 1.0  May 23, 2004  Created.
+ */
+public class DNSState implements Comparable
+{
+    private static Logger logger = Logger.getLogger(DNSState.class.toString());
+
+    private final String name;
+
+    /**
+     * Ordinal of next state to be created.
+     */
+    private static int nextOrdinal = 0;
+    /**
+     * Assign an ordinal to this state.
+     */
+    private final int ordinal = nextOrdinal++;
+    /**
+     * Logical sequence of states.
+     * The sequence is consistent with the ordinal of a state.
+     * This is used for advancing through states.
+     */
+    private final static ArrayList sequence = new ArrayList();
+
+    private DNSState(String name)
+    {
+        this.name = name;
+        sequence.add(this);
+    }
+
+    public final String toString()
+    {
+        return name;
+    }
+
+    public static final DNSState PROBING_1 = new DNSState("probing 1");
+    public static final DNSState PROBING_2 = new DNSState("probing 2");
+    public static final DNSState PROBING_3 = new DNSState("probing 3");
+    public static final DNSState ANNOUNCING_1 = new DNSState("announcing 1");
+    public static final DNSState ANNOUNCING_2 = new DNSState("announcing 2");
+    public static final DNSState ANNOUNCED = new DNSState("announced");
+    public static final DNSState CANCELED = new DNSState("canceled");
+
+    /**
+     * Returns the next advanced state.
+     * In general, this advances one step in the following sequence: PROBING_1,
+     * PROBING_2, PROBING_3, ANNOUNCING_1, ANNOUNCING_2, ANNOUNCED.
+     * Does not advance for ANNOUNCED and CANCELED state.
+     */
+    public final DNSState advance()
+    {
+        return (isProbing() || isAnnouncing()) ? (DNSState) 
sequence.get(ordinal + 1) : this;
+    }
+
+    /**
+     * Returns to the next reverted state.
+     * All states except CANCELED revert to PROBING_1.
+     * Status CANCELED does not revert.
+     */
+    public final DNSState revert()
+    {
+        return (this == CANCELED) ? this : PROBING_1;
+    }
+
+    /**
+     * Returns true, if this is a probing state.
+     */
+    public boolean isProbing()
+    {
+        return compareTo(PROBING_1) >= 0 && compareTo(PROBING_3) <= 0;
+    }
+
+    /**
+     * Returns true, if this is an announcing state.
+     */
+    public boolean isAnnouncing()
+    {
+        return compareTo(ANNOUNCING_1) >= 0 && compareTo(ANNOUNCING_2) <= 0;
+    }
+
+    /**
+     * Returns true, if this is an announced state.
+     */
+    public boolean isAnnounced()
+    {
+        return compareTo(ANNOUNCED) == 0;
+    }
+
+    /**
+     * Compares two states.
+     * The states compare as follows:
+     * PROBING_1 &lt; PROBING_2 &lt; PROBING_3 &lt; ANNOUNCING_1 &lt;
+     * ANNOUNCING_2 &lt; RESPONDING &lt; ANNOUNCED &lt; CANCELED.
+     */
+    public int compareTo(Object o)
+    {
+        return ordinal - ((DNSState) o).ordinal;
+    }
+}
\ No newline at end of file

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java                       
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java       2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,138 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.net.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * HostInfo information on the local host to be able to cope with change of 
addresses.
+ *
+ * @version %I%, %G%
+ * @author     Pierre Frisch, Werner Randelshofer
+ */
+class HostInfo
+{
+    private static Logger logger = Logger.getLogger(HostInfo.class.toString());
+    protected String name;
+    protected InetAddress address;
+    protected NetworkInterface interfaze;
+    /**
+     * This is used to create a unique name for the host name.
+     */
+    private int hostNameCount;
+
+    public HostInfo(InetAddress address, String name)
+    {
+        super();
+        this.address = address;
+        this.name = name;
+        if (address != null)
+        {
+            try
+            {
+                interfaze = NetworkInterface.getByInetAddress(address);
+            }
+            catch (Exception exception)
+            {
+                // FIXME Shouldn't we take an action here?
+                logger.log(Level.WARNING, "LocalHostInfo() exception ", 
exception);
+            }
+        }
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public InetAddress getAddress()
+    {
+        return address;
+    }
+
+    public NetworkInterface getInterface()
+    {
+        return interfaze;
+    }
+
+    synchronized String incrementHostName()
+    {
+        hostNameCount++;
+        int plocal = name.indexOf(".local.");
+        int punder = name.lastIndexOf("-");
+        name = name.substring(0, (punder == -1 ? plocal : punder)) + "-" + 
hostNameCount + ".local.";
+        return name;
+    }
+
+    boolean shouldIgnorePacket(DatagramPacket packet)
+    {
+        boolean result = false;
+        if (getAddress() != null)
+        {
+            InetAddress from = packet.getAddress();
+            if (from != null)
+            {
+                if (from.isLinkLocalAddress() && 
(!getAddress().isLinkLocalAddress()))
+                {
+                    // Ignore linklocal packets on regular interfaces, unless 
this is
+                    // also a linklocal interface. This is to avoid 
duplicates. This is
+                    // a terrible hack caused by the lack of an API to get the 
address
+                    // of the interface on which the packet was received.
+                    result = true;
+                }
+                if (from.isLoopbackAddress() && 
(!getAddress().isLoopbackAddress()))
+                {
+                    // Ignore loopback packets on a regular interface unless 
this is
+                    // also a loopback interface.
+                    result = true;
+                }
+            }
+        }
+        return result;
+    }
+
+    DNSRecord.Address getDNSAddressRecord(DNSRecord.Address address)
+    {
+        return (DNSConstants.TYPE_AAAA == address.type ? 
getDNS6AddressRecord() : getDNS4AddressRecord());
+    }
+
+    DNSRecord.Address getDNS4AddressRecord()
+    {
+        if ((getAddress() != null) &&
+            ((getAddress() instanceof Inet4Address) ||
+            ((getAddress() instanceof Inet6Address) && (((Inet6Address) 
getAddress()).isIPv4CompatibleAddress()))))
+        {
+            return new DNSRecord.Address(getName(), DNSConstants.TYPE_A, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, getAddress());
+        }
+        return null;
+    }
+
+    DNSRecord.Address getDNS6AddressRecord()
+    {
+        if ((getAddress() != null) && (getAddress() instanceof Inet6Address))
+        {
+            return new DNSRecord.Address(getName(), DNSConstants.TYPE_AAAA, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, getAddress());
+        }
+        return null;
+    }
+
+    public String toString()
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append("local host info[");
+        buf.append(getName() != null ? getName() : "no name");
+        buf.append(", ");
+        buf.append(getInterface() != null ? getInterface().getDisplayName() : 
"???");
+        buf.append(":");
+        buf.append(getAddress() != null ? getAddress().getHostAddress() : "no 
address");
+        buf.append("]");
+        return buf.toString();
+    }
+
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java                          
(rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java  2007-03-04 15:23:01 UTC 
(rev 11954)
@@ -0,0 +1,2559 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+// REMIND: multiple IP addresses
+
+/**
+ * mDNS implementation in Java.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Rick Blair, Jeff Sonstein,
+ * Werner Randelshofer, Pierre Frisch, Scott Lewis
+ */
+public class JmDNS
+{
+    private static Logger logger = Logger.getLogger(JmDNS.class.toString());
+    /**
+     * The version of JmDNS.
+     */
+    public static String VERSION = "2.0";
+
+    /**
+     * This is the multicast group, we are listening to for multicast DNS 
messages.
+     */
+    private InetAddress group;
+    /**
+     * This is our multicast socket.
+     */
+    private MulticastSocket socket;
+
+    /**
+     * Used to fix live lock problem on unregester.
+     */
+
+     protected boolean closed = false;
+
+    /**
+     * Holds instances of JmDNS.DNSListener.
+     * Must by a synchronized collection, because it is updated from
+     * concurrent threads.
+     */
+    private List listeners;
+    /**
+     * Holds instances of ServiceListener's.
+     * Keys are Strings holding a fully qualified service type.
+     * Values are LinkedList's of ServiceListener's.
+     */
+    private Map serviceListeners;
+    /**
+     * Holds instances of ServiceTypeListener's.
+     */
+    private List typeListeners;
+
+
+    /**
+     * Cache for DNSEntry's.
+     */
+    private 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.
+     */
+    Map services;
+
+    /**
+     * This hashtable holds the service types that have been registered or
+     * that have been received in an incoming datagram.
+     * Keys are instances of String which hold an all lower-case version of the
+     * fully qualified service type.
+     * Values hold the fully qualified service type.
+     */
+    Map serviceTypes;
+    /**
+     * This is the shutdown hook, we registered with the java runtime.
+     */
+    private Thread shutdown;
+
+    /**
+     * Handle on the local host
+     */
+    HostInfo localHost;
+
+    private Thread incomingListener = null;
+
+    /**
+     * 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;
+
+    /**
+     * The timer is used to dispatch all outgoing messages of JmDNS.
+     * It is also used to dispatch maintenance tasks for the DNS cache.
+     */
+    private Timer timer;
+
+    /**
+     * 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 Object ioLock = new Object();
+
+    /**
+     * 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.
+     * Remind: This does not work well with multiple planned answers for 
packages
+     * that came in from different clients.
+     */
+    private DNSIncoming plannedAnswer;
+    
+    // State machine
+    /**
+     * The state of JmDNS.
+     * <p/>
+     * For proper handling of concurrency, this variable must be
+     * changed only using methods advanceState(), revertState() and cancel().
+     */
+    private DNSState state = DNSState.PROBING_1;
+
+    /**
+     * Timer task associated to the host name.
+     * This is used to prevent from having multiple tasks associated to the 
host
+     * name at the same time.
+     */
+    TimerTask task;
+
+    /**
+     * 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 HashMap serviceCollectors = new HashMap();
+
+    /**
+     * Create an instance of JmDNS.
+     */
+    public JmDNS() throws IOException
+    {
+        logger.finer("JmDNS instance created");
+        try
+        {
+            InetAddress addr = InetAddress.getLocalHost();
+            init(addr.isLoopbackAddress() ? null : addr, addr.getHostName()); 
// [PJYF Oct 14 2004] Why do we disallow the loopback address?
+        }
+        catch (IOException e)
+        {
+            init(null, "computer");
+        }
+    }
+
+    /**
+     * Create an instance of JmDNS and bind it to a
+     * specific network interface given its IP-address.
+     */
+    public JmDNS(InetAddress addr) throws IOException
+    {
+        try
+        {
+            init(addr, addr.getHostName());
+        }
+        catch (IOException e)
+        {
+            init(null, "computer");
+        }
+    }
+
+    /**
+     * Initialize everything.
+     *
+     * @param address The interface to which JmDNS binds to.
+     * @param name    The host name of the interface.
+     */
+    private void init(InetAddress address, String name) throws IOException
+    {
+        // A host name with "." is illegal. so strip off everything and append 
.local.
+        int idx = name.indexOf(".");
+        if (idx > 0)
+        {
+            name = name.substring(0, idx);
+        }
+        name += ".local.";
+        // localHost to IP address binding
+        localHost = new HostInfo(address, name);
+
+        cache = new DNSCache(100);
+
+        listeners = Collections.synchronizedList(new ArrayList());
+        serviceListeners = new HashMap();
+        typeListeners = new ArrayList();
+
+        services = new Hashtable(20);
+        serviceTypes = new Hashtable(20);
+
+        // REMIND: If I could pass in a name for the Timer thread,
+        //         I would pass 'JmDNS.Timer'.
+        timer = new Timer();
+        new RecordReaper().start();
+        shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown");
+        Runtime.getRuntime().addShutdownHook(shutdown);
+
+        incomingListener = new Thread(new SocketListener(), 
"JmDNS.SocketListener");
+
+        // Bind to multicast socket
+        openMulticastSocket(localHost);
+        start(services.values());
+    }
+
+    private void start(Collection serviceInfos)
+    {
+        state = DNSState.PROBING_1;
+        incomingListener.start();
+        new Prober().start();
+        for (Iterator iterator = serviceInfos.iterator(); iterator.hasNext();)
+        {
+            try
+            {
+                registerService(new ServiceInfo((ServiceInfo) 
iterator.next()));
+            }
+            catch (Exception exception)
+            {
+                logger.log(Level.WARNING, "start() Registration exception ", 
exception);
+            }
+        }
+    }
+
+    private void openMulticastSocket(HostInfo hostInfo) throws IOException
+    {
+        if (group == null)
+        {
+            group = InetAddress.getByName(DNSConstants.MDNS_GROUP);
+        }
+        if (socket != null)
+        {
+            this.closeMulticastSocket();
+        }
+        socket = new MulticastSocket(DNSConstants.MDNS_PORT);
+        if ((hostInfo != null) && (localHost.getInterface() != null))
+        {
+            socket.setNetworkInterface(hostInfo.getInterface());
+        }
+        socket.setTimeToLive(255);
+        socket.joinGroup(group);
+    }
+
+    private void closeMulticastSocket()
+    {
+        logger.finer("closeMulticastSocket()");
+        if (socket != null)
+        {
+            // close socket
+            try
+            {
+                socket.leaveGroup(group);
+                socket.close();
+                if (incomingListener != null)
+                {
+                    incomingListener.join();
+                }
+            }
+            catch (Exception exception)
+            {
+                logger.log(Level.WARNING, "closeMulticastSocket() Close socket 
exception ", exception);
+            }
+            socket = null;
+        }
+    }
+    
+    // State machine
+    /**
+     * Sets the state and notifies all objects that wait on JmDNS.
+     */
+    synchronized void advanceState()
+    {
+        state = state.advance();
+        notifyAll();
+    }
+
+    /**
+     * Sets the state and notifies all objects that wait on JmDNS.
+     */
+    synchronized void revertState()
+    {
+        state = state.revert();
+        notifyAll();
+    }
+
+    /**
+     * Sets the state and notifies all objects that wait on JmDNS.
+     */
+    synchronized void cancel()
+    {
+        state = DNSState.CANCELED;
+        notifyAll();
+    }
+
+    /**
+     * Returns the current state of this info.
+     */
+    DNSState getState()
+    {
+        return state;
+    }
+
+
+    /**
+     * Return the DNSCache associated with the cache variable
+     */
+    DNSCache getCache()
+    {
+        return cache;
+    }
+
+    /**
+     * Return the HostName associated with this JmDNS instance.
+     * Note: May not be the same as what started.  The host name is subject to
+     * negotiation.
+     */
+    public String getHostName()
+    {
+        return localHost.getName();
+    }
+
+    public HostInfo getLocalHost()
+    {
+        return localHost;
+    }
+
+    /**
+     * Return the address of the interface to which this instance of JmDNS is
+     * bound.
+     */
+    public InetAddress getInterface() throws IOException
+    {
+        return socket.getInterface();
+    }
+
+    /**
+     * Get service information. If the information is not cached, the method
+     * will block until updated information is received.
+     * <p/>
+     * Usage note: Do not call this method from the AWT event dispatcher 
thread.
+     * You will make the user interface unresponsive.
+     *
+     * @param type fully qualified service type, such as 
<code>_http._tcp.local.</code> .
+     * @param name unqualified service name, such as <code>foobar</code> .
+     * @return null if the service information cannot be obtained
+     */
+    public ServiceInfo getServiceInfo(String type, String name)
+    {
+        return getServiceInfo(type, name, 3 * 1000);
+    }
+
+    /**
+     * Get service information. If the information is not cached, the method
+     * will block for the given timeout until updated information is received.
+     * <p/>
+     * Usage note: If you call this method from the AWT event dispatcher 
thread,
+     * use a small timeout, or you will make the user interface unresponsive.
+     *
+     * @param type    full qualified service type, such as 
<code>_http._tcp.local.</code> .
+     * @param name    unqualified service name, such as <code>foobar</code> .
+     * @param timeout timeout in milliseconds
+     * @return null if the service information cannot be obtained
+     */
+    public ServiceInfo getServiceInfo(String type, String name, int timeout)
+    {
+        ServiceInfo info = new ServiceInfo(type, name);
+        new ServiceInfoResolver(info).start();
+
+        try
+        {
+            long end = System.currentTimeMillis() + timeout;
+            long delay;
+            synchronized (info)
+            {
+                while (!info.hasData() && (delay = end - 
System.currentTimeMillis()) > 0)
+                {
+                    info.wait(delay);
+                }
+            }
+        }
+        catch (InterruptedException e)
+        {
+            // empty
+        }
+
+        return (info.hasData()) ? info : null;
+    }
+
+    /**
+     * Request service information. The information about the service is
+     * requested and the ServiceListener.resolveService method is called as 
soon
+     * as it is available.
+     * <p/>
+     * Usage note: Do not call this method from the AWT event dispatcher 
thread.
+     * You will make the user interface unresponsive.
+     *
+     * @param type full qualified service type, such as 
<code>_http._tcp.local.</code> .
+     * @param name unqualified service name, such as <code>foobar</code> .
+     */
+    public void requestServiceInfo(String type, String name)
+    {
+        requestServiceInfo(type, name, 3 * 1000);
+    }
+
+    /**
+     * Request service information. The information about the service is 
requested
+     * and the ServiceListener.resolveService method is called as soon as it 
is available.
+     *
+     * @param type    full qualified service type, such as 
<code>_http._tcp.local.</code> .
+     * @param name    unqualified service name, such as <code>foobar</code> .
+     * @param timeout timeout in milliseconds
+     */
+    public void requestServiceInfo(String type, String name, int timeout)
+    {
+        registerServiceType(type);
+        ServiceInfo info = new ServiceInfo(type, name);
+        new ServiceInfoResolver(info).start();
+
+        try
+        {
+            long end = System.currentTimeMillis() + timeout;
+            long delay;
+            synchronized (info)
+            {
+                while (!info.hasData() && (delay = end - 
System.currentTimeMillis()) > 0)
+                {
+                    info.wait(delay);
+                }
+            }
+        }
+        catch (InterruptedException e)
+        {
+            // empty
+        }
+    }
+
+    void handleServiceResolved(ServiceInfo info)
+    {
+        List list = (List) serviceListeners.get(info.type.toLowerCase());
+        if (list != null)
+        {
+            ServiceEvent event = new ServiceEvent(this, info.type, 
info.getName(), info);
+            // Iterate on a copy in case listeners will modify it
+            final ArrayList listCopy = new ArrayList(list);
+            for (Iterator iterator = listCopy.iterator(); iterator.hasNext();)
+            {
+                ((ServiceListener) iterator.next()).serviceResolved(event);
+            }
+        }
+    }
+
+    /**
+     * Listen for service types.
+     *
+     * @param listener listener for service types
+     */
+    public void addServiceTypeListener(ServiceTypeListener listener) throws 
IOException
+    {
+        synchronized (this)
+        {
+            typeListeners.remove(listener);
+            typeListeners.add(listener);
+        }
+
+        // report cached service types
+        for (Iterator iterator = serviceTypes.values().iterator(); 
iterator.hasNext();)
+        {
+            listener.serviceTypeAdded(new ServiceEvent(this, (String) 
iterator.next(), null, null));
+        }
+
+        new TypeResolver().start();
+    }
+
+    /**
+     * Remove listener for service types.
+     *
+     * @param listener listener for service types
+     */
+    public void removeServiceTypeListener(ServiceTypeListener listener)
+    {
+        synchronized (this)
+        {
+            typeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Listen for services of a given type. The type has to be a fully 
qualified
+     * type name such as <code>_http._tcp.local.</code>.
+     *
+     * @param type     full qualified service type, such as 
<code>_http._tcp.local.</code>.
+     * @param listener listener for service updates
+     */
+    public void addServiceListener(String type, ServiceListener listener)
+    {
+        String lotype = type.toLowerCase();
+        removeServiceListener(lotype, listener);
+        List list = null;
+        synchronized (this)
+        {
+            list = (List) serviceListeners.get(lotype);
+            if (list == null)
+            {
+                list = Collections.synchronizedList(new LinkedList());
+                serviceListeners.put(lotype, list);
+            }
+            list.add(listener);
+        }
+
+        // report cached service types
+        for (Iterator i = cache.iterator(); i.hasNext();)
+        {
+            for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != 
null; n = n.next())
+            {
+                DNSRecord rec = (DNSRecord) n.getValue();
+                if (rec.type == DNSConstants.TYPE_SRV)
+                {
+                    if (rec.name.endsWith(type))
+                    {
+                        listener.serviceAdded(new ServiceEvent(this, type, 
toUnqualifiedName(type, rec.name), null));
+                    }
+                }
+            }
+        }
+        new ServiceResolver(type).start();
+    }
+
+    /**
+     * Remove listener for services of a given type.
+     *
+     * @param listener listener for service updates
+     */
+    public void removeServiceListener(String type, ServiceListener listener)
+    {
+        type = type.toLowerCase();
+        List list = (List) serviceListeners.get(type);
+        if (list != null)
+        {
+            synchronized (this)
+            {
+                list.remove(listener);
+                if (list.size() == 0)
+                {
+                    serviceListeners.remove(type);
+                }
+            }
+        }
+    }
+
+    /**
+     * Register a service. The service is registered for access by other jmdns 
clients.
+     * The name of the service may be changed to make it unique.
+     */
+    public void registerService(ServiceInfo info) throws IOException
+    {
+        registerServiceType(info.type);
+
+        // bind the service to this address
+        info.server = localHost.getName();
+        info.addr = localHost.getAddress();
+
+        synchronized (this)
+        {
+            makeServiceNameUnique(info);
+            services.put(info.getQualifiedName().toLowerCase(), info);
+        }
+
+        new /*Service*/Prober().start();
+        try
+        {
+            synchronized (info)
+            {
+                while (info.getState().compareTo(DNSState.ANNOUNCED) < 0)
+                {
+                    info.wait();
+                }
+            }
+        }
+        catch (InterruptedException e)
+        {
+            //empty
+        }
+        logger.fine("registerService() JmDNS registered service as " + info);
+    }
+
+    /**
+     * Unregister a service. The service should have been registered.
+     */
+    public void unregisterService(ServiceInfo info)
+    {
+        synchronized (this)
+        {
+            services.remove(info.getQualifiedName().toLowerCase());
+        }
+        info.cancel();
+
+        // Note: We use this lock object to synchronize on it.
+        //       Synchronizing on another object (e.g. the ServiceInfo) does
+        //       not make sense, because the sole purpose of the lock is to
+        //       wait until the canceler has finished. If we synchronized on
+        //       the ServiceInfo or on the Canceler, we would block all
+        //       accesses to synchronized methods on that object. This is not
+        //       what we want!
+        Object lock = new Object();
+        new Canceler(info, lock).start();
+
+        // Remind: We get a deadlock here, if the Canceler does not run!
+        try
+        {
+            synchronized (lock)
+            {
+                lock.wait();
+            }
+        }
+        catch (InterruptedException e)
+        {
+            // empty
+        }
+    }
+
+    /**
+     * Unregister all services.
+     */
+    public void unregisterAllServices()
+    {
+        logger.finer("unregisterAllServices()");
+        if (services.size() == 0)
+        {
+            return;
+        }
+
+        Collection list;
+        synchronized (this)
+        {
+            list = new LinkedList(services.values());
+            services.clear();
+        }
+        for (Iterator iterator = list.iterator(); iterator.hasNext();)
+        {
+            ((ServiceInfo) iterator.next()).cancel();
+        }
+
+
+        Object lock = new Object();
+        new Canceler(list, lock).start();
+              // Remind: We get a livelock here, if the Canceler does not run!
+        try {
+            synchronized (lock) {
+                if (!closed) {
+                    lock.wait();
+                }
+            }
+        } catch (InterruptedException e) {
+            // empty
+        }
+
+
+    }
+
+    /**
+     * Register a service type. If this service type was not already known,
+     * all service listeners will be notified of the new service type. Service 
types
+     * are automatically registered as they are discovered.
+     */
+    public void registerServiceType(String type)
+    {
+        String name = type.toLowerCase();
+        if (serviceTypes.get(name) == null)
+        {
+            if ((type.indexOf("._mdns._udp.") < 0) && 
!type.endsWith(".in-addr.arpa."))
+            {
+                Collection list;
+                synchronized (this)
+                {
+                    serviceTypes.put(name, type);
+                    list = new LinkedList(typeListeners);
+                }
+                for (Iterator iterator = list.iterator(); iterator.hasNext();)
+                {
+                    ((ServiceTypeListener) 
iterator.next()).serviceTypeAdded(new ServiceEvent(this, type, null, null));
+                }
+            }
+        }
+    }
+
+    /**
+     * Generate a possibly unique name for a host using the information we
+     * have in the cache.
+     *
+     * @return returns true, if the name of the host had to be changed.
+     */
+    private boolean makeHostNameUnique(DNSRecord.Address host)
+    {
+        String originalName = host.getName();
+        long now = System.currentTimeMillis();
+
+        boolean collision;
+        do
+        {
+            collision = false;
+
+            // Check for collision in cache
+            for (DNSCache.CacheNode j = 
cache.find(host.getName().toLowerCase()); j != null; j = j.next())
+            {
+                DNSRecord a = (DNSRecord) j.getValue();
+                if (false)
+                {
+                    host.name = incrementName(host.getName());
+                    collision = true;
+                    break;
+                }
+            }
+        }
+        while (collision);
+
+        if (originalName.equals(host.getName()))
+        {
+            return false;
+        }
+        else
+        {
+            return true;
+        }
+    }
+
+    /**
+     * 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(ServiceInfo info)
+    {
+        String originalQualifiedName = info.getQualifiedName();
+        long now = System.currentTimeMillis();
+
+        boolean collision;
+        do
+        {
+            collision = false;
+
+            // Check for collision in cache
+            for (DNSCache.CacheNode j = 
cache.find(info.getQualifiedName().toLowerCase()); j != null; j = j.next())
+            {
+                DNSRecord a = (DNSRecord) j.getValue();
+                if ((a.type == DNSConstants.TYPE_SRV) && !a.isExpired(now))
+                {
+                    DNSRecord.Service s = (DNSRecord.Service) a;
+                    if (s.port != info.port || 
!s.server.equals(localHost.getName()))
+                    {
+                        logger.finer("makeServiceNameUnique() 
JmDNS.makeServiceNameUnique srv collision:" + a + " s.server=" + s.server + " " 
+ localHost.getName() + " equals:" + (s.server.equals(localHost.getName())));
+                        info.setName(incrementName(info.getName()));
+                        collision = true;
+                        break;
+                    }
+                }
+            }
+
+            // Check for collision with other service infos published by JmDNS
+            Object selfService = 
services.get(info.getQualifiedName().toLowerCase());
+            if (selfService != null && selfService != info)
+            {
+                info.setName(incrementName(info.getName()));
+                collision = true;
+            }
+        }
+        while (collision);
+
+        return !(originalQualifiedName.equals(info.getQualifiedName()));
+    }
+
+    String incrementName(String name)
+    {
+        try
+        {
+            int l = name.lastIndexOf('(');
+            int r = name.lastIndexOf(')');
+            if ((l >= 0) && (l < r))
+            {
+                name = name.substring(0, l) + "(" + 
(Integer.parseInt(name.substring(l + 1, r)) + 1) + ")";
+            }
+            else
+            {
+                name += " (2)";
+            }
+        }
+        catch (NumberFormatException e)
+        {
+            name += " (2)";
+        }
+        return name;
+    }
+
+    /**
+     * 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.
+     */
+    void addListener(DNSListener listener, DNSQuestion question)
+    {
+        long now = System.currentTimeMillis();
+
+        // add the new listener
+        synchronized (this)
+        {
+            listeners.add(listener);
+        }
+
+        // report existing matched records
+        if (question != null)
+        {
+            for (DNSCache.CacheNode i = cache.find(question.name); i != null; 
i = i.next())
+            {
+                DNSRecord c = (DNSRecord) i.getValue();
+                if (question.answeredBy(c) && !c.isExpired(now))
+                {
+                    listener.updateRecord(this, now, c);
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove a listener from all outstanding questions. The listener will no 
longer
+     * receive any updates.
+     */
+    void removeListener(DNSListener listener)
+    {
+        synchronized (this)
+        {
+            listeners.remove(listener);
+        }
+    }
+    
+    
+    // Remind: Method updateRecord should receive a better name.
+    /**
+     * Notify all listeners that a record was updated.
+     */
+    void updateRecord(long now, DNSRecord rec)
+    {
+        // We do not want to block the entire DNS while we are updating the 
record for each listener (service info)
+        List listenerList = null;
+        synchronized (this)
+        {
+            listenerList = new ArrayList(listeners);
+        }
+        for (Iterator iterator = listenerList.iterator(); iterator.hasNext();)
+        {
+            DNSListener listener = (DNSListener) iterator.next();
+            listener.updateRecord(this, now, rec);
+        }
+        if (rec.type == DNSConstants.TYPE_PTR || rec.type == 
DNSConstants.TYPE_SRV)
+        {
+            List serviceListenerList = null;
+            synchronized (this)
+            {
+                serviceListenerList = (List) 
serviceListeners.get(rec.name.toLowerCase());
+                // Iterate on a copy in case listeners will modify it
+                if (serviceListenerList != null)
+                {
+                    serviceListenerList = new ArrayList(serviceListenerList);
+                }
+            }
+            if (serviceListenerList != null)
+            {
+                boolean expired = rec.isExpired(now);
+                String type = rec.getName();
+                String name = ((DNSRecord.Pointer) rec).getAlias();
+                // DNSRecord old = (DNSRecord)services.get(name.toLowerCase());
+                if (!expired)
+                {
+                    // new record
+                    ServiceEvent event = new ServiceEvent(this, type, 
toUnqualifiedName(type, name), null);
+                    for (Iterator iterator = serviceListenerList.iterator(); 
iterator.hasNext();)
+                    {
+                        ((ServiceListener) 
iterator.next()).serviceAdded(event);
+                    }
+                }
+                else
+                {
+                    // expire record
+                    ServiceEvent event = new ServiceEvent(this, type, 
toUnqualifiedName(type, name), null);
+                    for (Iterator iterator = serviceListenerList.iterator(); 
iterator.hasNext();)
+                    {
+                        ((ServiceListener) 
iterator.next()).serviceRemoved(event);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Handle an incoming response. Cache answers, and pass them on to
+     * the appropriate questions.
+     */
+    private void handleResponse(DNSIncoming msg) throws IOException
+    {
+        long now = System.currentTimeMillis();
+
+        boolean hostConflictDetected = false;
+        boolean serviceConflictDetected = false;
+
+        for (Iterator i = msg.answers.iterator(); i.hasNext();)
+        {
+            boolean isInformative = false;
+            DNSRecord rec = (DNSRecord) i.next();
+            boolean expired = rec.isExpired(now);
+
+            // update the cache
+            DNSRecord c = (DNSRecord) cache.get(rec);
+            if (c != null)
+            {
+                if (expired)
+                {
+                    isInformative = true;
+                    cache.remove(c);
+                }
+                else
+                {
+                    c.resetTTL(rec);
+                    rec = c;
+                }
+            }
+            else
+            {
+                if (!expired)
+                {
+                    isInformative = true;
+                    cache.add(rec);
+                }
+            }
+            switch (rec.type)
+            {
+                case DNSConstants.TYPE_PTR:
+                    // handle _mdns._udp records
+                    if (rec.getName().indexOf("._mdns._udp.") >= 0)
+                    {
+                        if (!expired && 
rec.name.startsWith("_services._mdns._udp."))
+                        {
+                            isInformative = true;
+                            registerServiceType(((DNSRecord.Pointer) 
rec).alias);
+                        }
+                        continue;
+                    }
+                    registerServiceType(rec.name);
+                    break;
+            }
+
+            if ((rec.getType() == DNSConstants.TYPE_A) || (rec.getType() == 
DNSConstants.TYPE_AAAA))
+            {
+                hostConflictDetected |= rec.handleResponse(this);
+            }
+            else
+            {
+                serviceConflictDetected |= rec.handleResponse(this);
+            }
+
+            // notify the listeners
+            if (isInformative)
+            {
+                updateRecord(now, rec);
+            }
+        }
+
+        if (hostConflictDetected || serviceConflictDetected)
+        {
+            new Prober().start();
+        }
+    }
+
+    /**
+     * Handle an incoming query. See if we can answer any part of it
+     * given our service infos.
+     */
+    private void handleQuery(DNSIncoming in, InetAddress addr, int port) 
throws IOException
+    {
+        // Track known answers
+        boolean hostConflictDetected = false;
+        boolean serviceConflictDetected = false;
+        long expirationTime = System.currentTimeMillis() + 
DNSConstants.KNOWN_ANSWER_TTL;
+        for (Iterator i = in.answers.iterator(); i.hasNext();)
+        {
+            DNSRecord answer = (DNSRecord) i.next();
+            if ((answer.getType() == DNSConstants.TYPE_A) || (answer.getType() 
== DNSConstants.TYPE_AAAA))
+            {
+                hostConflictDetected |= answer.handleQuery(this, 
expirationTime);
+            }
+            else
+            {
+                serviceConflictDetected |= answer.handleQuery(this, 
expirationTime);
+            }
+        }
+
+        if (plannedAnswer != null)
+        {
+            plannedAnswer.append(in);
+        }
+        else
+        {
+            if (in.isTruncated())
+            {
+                plannedAnswer = in;
+            }
+
+            new Responder(in, addr, port).start();
+        }
+
+        if (hostConflictDetected || serviceConflictDetected)
+        {
+            new Prober().start();
+        }
+    }
+
+    /**
+     * Add an answer to a question. Deal with the case when the
+     * outgoing packet overflows
+     */
+    DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, 
DNSOutgoing out, DNSRecord rec) throws IOException
+    {
+        if (out == null)
+        {
+            out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | 
DNSConstants.FLAGS_AA);
+        }
+        try
+        {
+            out.addAnswer(in, rec);
+        }
+        catch (IOException e)
+        {
+            out.flags |= DNSConstants.FLAGS_TC;
+            out.id = in.id;
+            out.finish();
+            send(out);
+
+            out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | 
DNSConstants.FLAGS_AA);
+            out.addAnswer(in, rec);
+        }
+        return out;
+    }
+
+
+    /**
+     * Send an outgoing multicast DNS message.
+     */
+    private void send(DNSOutgoing out) throws IOException
+    {
+        out.finish();
+        if (!out.isEmpty())
+        {
+            DatagramPacket packet = new DatagramPacket(out.data, out.off, 
group, DNSConstants.MDNS_PORT);
+
+            try
+            {
+                DNSIncoming msg = new DNSIncoming(packet);
+                logger.finest("send() JmDNS out:" + msg.print(true));
+            }
+            catch (IOException e)
+            {
+                logger.throwing(getClass().toString(), "send(DNSOutgoing) - 
JmDNS can not parse what it sends!!!", e);
+            }
+            socket.send(packet);
+        }
+    }
+
+    /**
+     * Listen for multicast packets.
+     */
+    class SocketListener implements Runnable
+    {
+        public void run()
+        {
+            try
+            {
+                byte buf[] = new byte[DNSConstants.MAX_MSG_ABSOLUTE];
+                DatagramPacket packet = new DatagramPacket(buf, buf.length);
+                while (state != DNSState.CANCELED)
+                {
+                    packet.setLength(buf.length);
+                    socket.receive(packet);
+                    if (state == DNSState.CANCELED)
+                    {
+                        break;
+                    }
+                    try
+                    {
+                        if (localHost.shouldIgnorePacket(packet))
+                        {
+                            continue;
+                        }
+
+                        DNSIncoming msg = new DNSIncoming(packet);
+                        logger.finest("SocketListener.run() JmDNS in:" + 
msg.print(true));
+
+                        synchronized (ioLock)
+                        {
+                            if (msg.isQuery())
+                            {
+                                if (packet.getPort() != DNSConstants.MDNS_PORT)
+                                {
+                                    handleQuery(msg, packet.getAddress(), 
packet.getPort());
+                                }
+                                handleQuery(msg, group, 
DNSConstants.MDNS_PORT);
+                            }
+                            else
+                            {
+                                handleResponse(msg);
+                            }
+                        }
+                    }
+                    catch (IOException e)
+                    {
+                        logger.log(Level.WARNING, "run() exception ", e);
+                    }
+                }
+            }
+            catch (IOException e)
+            {
+                if (state != DNSState.CANCELED)
+                {
+                    logger.log(Level.WARNING, "run() exception ", e);
+                    recover();
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Periodicaly removes expired entries from the cache.
+     */
+    private class RecordReaper extends TimerTask
+    {
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.RECORD_REAPER_INTERVAL, 
DNSConstants.RECORD_REAPER_INTERVAL);
+        }
+
+        public void run()
+        {
+            synchronized (JmDNS.this)
+            {
+                if (state == DNSState.CANCELED)
+                {
+                    return;
+                }
+                logger.finest("run() JmDNS reaping cache");
+
+                // Remove expired answers from the cache
+                // -------------------------------------
+                // To prevent race conditions, we defensively copy all cache
+                // entries into a list.
+                List list = new ArrayList();
+                synchronized (cache)
+                {
+                    for (Iterator i = cache.iterator(); i.hasNext();)
+                    {
+                        for (DNSCache.CacheNode n = (DNSCache.CacheNode) 
i.next(); n != null; n = n.next())
+                        {
+                            list.add(n.getValue());
+                        }
+                    }
+                }
+                // Now, we remove them.
+                long now = System.currentTimeMillis();
+                for (Iterator i = list.iterator(); i.hasNext();)
+                {
+                    DNSRecord c = (DNSRecord) i.next();
+                    if (c.isExpired(now))
+                    {
+                        updateRecord(now, c);
+                        cache.remove(c);
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * The Prober sends three consecutive probes for all service infos
+     * that needs probing as well as for the host name.
+     * The state of each service info of the host name is advanced, when a 
probe has
+     * been sent for it.
+     * When the prober has run three times, it launches an Announcer.
+     * <p/>
+     * If a conflict during probes occurs, the affected service infos (and 
affected
+     * host name) are taken away from the prober. This eventually causes the 
prober
+     * tho cancel itself.
+     */
+    private class Prober extends TimerTask
+    {
+        /**
+         * The state of the prober.
+         */
+        DNSState taskState = DNSState.PROBING_1;
+
+        public Prober()
+        {
+            // Associate the host name to this, if it needs probing
+            if (state == DNSState.PROBING_1)
+            {
+                task = this;
+            }
+            // Associate services to this, if they need probing
+            synchronized (JmDNS.this)
+            {
+                for (Iterator iterator = services.values().iterator(); 
iterator.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) iterator.next();
+                    if (info.getState() == DNSState.PROBING_1)
+                    {
+                        info.task = this;
+                    }
+                }
+            }
+        }
+
+
+        public void start()
+        {
+            long now = System.currentTimeMillis();
+            if (now - lastThrottleIncrement < 
DNSConstants.PROBE_THROTTLE_COUNT_INTERVAL)
+            {
+                throttle++;
+            }
+            else
+            {
+                throttle = 1;
+            }
+            lastThrottleIncrement = now;
+
+            if (state == DNSState.ANNOUNCED && throttle < 
DNSConstants.PROBE_THROTTLE_COUNT)
+            {
+                timer.schedule(this, random.nextInt(1 + 
DNSConstants.PROBE_WAIT_INTERVAL), DNSConstants.PROBE_WAIT_INTERVAL);
+            }
+            else
+            {
+                timer.schedule(this, DNSConstants.PROBE_CONFLICT_INTERVAL, 
DNSConstants.PROBE_CONFLICT_INTERVAL);
+            }
+        }
+
+        public boolean cancel()
+        {
+            // Remove association from host name to this
+            if (task == this)
+            {
+                task = null;
+            }
+
+            // Remove associations from services to this
+            synchronized (JmDNS.this)
+            {
+                for (Iterator i = services.values().iterator(); i.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) i.next();
+                    if (info.task == this)
+                    {
+                        info.task = null;
+                    }
+                }
+            }
+
+            return super.cancel();
+        }
+
+        public void run()
+        {
+            synchronized (ioLock)
+            {
+                DNSOutgoing out = null;
+                try
+                {
+                    // send probes for JmDNS itself
+                    if (state == taskState && task == this)
+                    {
+                        if (out == null)
+                        {
+                            out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
+                        }
+                        out.addQuestion(new DNSQuestion(localHost.getName(), 
DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
+                        DNSRecord answer = localHost.getDNS4AddressRecord();
+                        if (answer != null)
+                        {
+                            out.addAuthorativeAnswer(answer);
+                        }
+                        answer = localHost.getDNS6AddressRecord();
+                        if (answer != null)
+                        {
+                            out.addAuthorativeAnswer(answer);
+                        }
+                        advanceState();
+                    }
+                    // send probes for services
+                    // Defensively copy the services into a local list,
+                    // to prevent race conditions with methods registerService
+                    // and unregisterService.
+                    List list;
+                    synchronized (JmDNS.this)
+                    {
+                        list = new LinkedList(services.values());
+                    }
+                    for (Iterator i = list.iterator(); i.hasNext();)
+                    {
+                        ServiceInfo info = (ServiceInfo) i.next();
+
+                        synchronized (info)
+                        {
+                            if (info.getState() == taskState && info.task == 
this)
+                            {
+                                info.advanceState();
+                                logger.fine("run() JmDNS probing " + 
info.getQualifiedName() + " state " + info.getState());
+                                if (out == null)
+                                {
+                                    out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
+                                    out.addQuestion(new 
DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, 
DNSConstants.CLASS_IN));
+                                }
+                                out.addAuthorativeAnswer(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, 
info.port, localHost.getName()));
+                            }
+                        }
+                    }
+                    if (out != null)
+                    {
+                        logger.finer("run() JmDNS probing #" + taskState);
+                        send(out);
+                    }
+                    else
+                    {
+                        // If we have nothing to send, another timer taskState 
ahead
+                        // of us has done the job for us. We can cancel.
+                        cancel();
+                        return;
+                    }
+                }
+                catch (Throwable e)
+                {
+                    logger.log(Level.WARNING, "run() exception ", e);
+                    recover();
+                }
+
+                taskState = taskState.advance();
+                if (!taskState.isProbing())
+                {
+                    cancel();
+
+                    new Announcer().start();
+                }
+            }
+        }
+
+    }
+
+    /**
+     * The Announcer sends an accumulated query of all announces, and advances
+     * the state of all serviceInfos, for which it has sent an announce.
+     * The Announcer also sends announcements and advances the state of JmDNS 
itself.
+     * <p/>
+     * When the announcer has run two times, it finishes.
+     */
+    private class Announcer extends TimerTask
+    {
+        /**
+         * The state of the announcer.
+         */
+        DNSState taskState = DNSState.ANNOUNCING_1;
+
+        public Announcer()
+        {
+            // Associate host to this, if it needs announcing
+            if (state == DNSState.ANNOUNCING_1)
+            {
+                task = this;
+            }
+            // Associate services to this, if they need announcing
+            synchronized (JmDNS.this)
+            {
+                for (Iterator s = services.values().iterator(); s.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) s.next();
+                    if (info.getState() == DNSState.ANNOUNCING_1)
+                    {
+                        info.task = this;
+                    }
+                }
+            }
+        }
+
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.ANNOUNCE_WAIT_INTERVAL, 
DNSConstants.ANNOUNCE_WAIT_INTERVAL);
+        }
+
+        public boolean cancel()
+        {
+            // Remove association from host to this
+            if (task == this)
+            {
+                task = null;
+            }
+
+            // Remove associations from services to this
+            synchronized (JmDNS.this)
+            {
+                for (Iterator i = services.values().iterator(); i.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) i.next();
+                    if (info.task == this)
+                    {
+                        info.task = null;
+                    }
+                }
+            }
+
+            return super.cancel();
+        }
+
+        public void run()
+        {
+            DNSOutgoing out = null;
+            try
+            {
+                // send probes for JmDNS itself
+                if (state == taskState)
+                {
+                    if (out == null)
+                    {
+                        out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | 
DNSConstants.FLAGS_AA);
+                    }
+                    DNSRecord answer = localHost.getDNS4AddressRecord();
+                    if (answer != null)
+                    {
+                        out.addAnswer(answer, 0);
+                    }
+                    answer = localHost.getDNS6AddressRecord();
+                    if (answer != null)
+                    {
+                        out.addAnswer(answer, 0);
+                    }
+                    advanceState();
+                }
+                // send announces for services
+                // Defensively copy the services into a local list,
+                // to prevent race conditions with methods registerService
+                // and unregisterService.
+                List list;
+                synchronized (JmDNS.this)
+                {
+                    list = new ArrayList(services.values());
+                }
+                for (Iterator i = list.iterator(); i.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) i.next();
+                    synchronized (info)
+                    {
+                        if (info.getState() == taskState && info.task == this)
+                        {
+                            info.advanceState();
+                            logger.finer("run() JmDNS announcing " + 
info.getQualifiedName() + " state " + info.getState());
+                            if (out == null)
+                            {
+                                out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
+                            }
+                            out.addAnswer(new DNSRecord.Pointer(info.type, 
DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, 
info.getQualifiedName()), 0);
+                            out.addAnswer(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, 
info.port, localHost.getName()), 0);
+                            out.addAnswer(new 
DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0);
+                        }
+                    }
+                }
+                if (out != null)
+                {
+                    logger.finer("run() JmDNS announcing #" + taskState);
+                    send(out);
+                }
+                else
+                {
+                    // If we have nothing to send, another timer taskState 
ahead
+                    // of us has done the job for us. We can cancel.
+                    cancel();
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+
+            taskState = taskState.advance();
+            if (!taskState.isAnnouncing())
+            {
+                cancel();
+
+                new Renewer().start();
+            }
+        }
+    }
+
+    /**
+     * The Renewer is there to send renewal announcment when the record expire 
for ours infos.
+     */
+    private class Renewer extends TimerTask
+    {
+        /**
+         * The state of the announcer.
+         */
+        DNSState taskState = DNSState.ANNOUNCED;
+
+        public Renewer()
+        {
+            // Associate host to this, if it needs renewal
+            if (state == DNSState.ANNOUNCED)
+            {
+                task = this;
+            }
+            // Associate services to this, if they need renewal
+            synchronized (JmDNS.this)
+            {
+                for (Iterator s = services.values().iterator(); s.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) s.next();
+                    if (info.getState() == DNSState.ANNOUNCED)
+                    {
+                        info.task = this;
+                    }
+                }
+            }
+        }
+
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL, 
DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL);
+        }
+
+        public boolean cancel()
+        {
+            // Remove association from host to this
+            if (task == this)
+            {
+                task = null;
+            }
+
+            // Remove associations from services to this
+            synchronized (JmDNS.this)
+            {
+                for (Iterator i = services.values().iterator(); i.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) i.next();
+                    if (info.task == this)
+                    {
+                        info.task = null;
+                    }
+                }
+            }
+
+            return super.cancel();
+        }
+
+        public void run()
+        {
+            DNSOutgoing out = null;
+            try
+            {
+                // send probes for JmDNS itself
+                if (state == taskState)
+                {
+                    if (out == null)
+                    {
+                        out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | 
DNSConstants.FLAGS_AA);
+                    }
+                    DNSRecord answer = localHost.getDNS4AddressRecord();
+                    if (answer != null)
+                    {
+                        out.addAnswer(answer, 0);
+                    }
+                    answer = localHost.getDNS6AddressRecord();
+                    if (answer != null)
+                    {
+                        out.addAnswer(answer, 0);
+                    }
+                    advanceState();
+                }
+                // send announces for services
+                // Defensively copy the services into a local list,
+                // to prevent race conditions with methods registerService
+                // and unregisterService.
+                List list;
+                synchronized (JmDNS.this)
+                {
+                    list = new ArrayList(services.values());
+                }
+                for (Iterator i = list.iterator(); i.hasNext();)
+                {
+                    ServiceInfo info = (ServiceInfo) i.next();
+                    synchronized (info)
+                    {
+                        if (info.getState() == taskState && info.task == this)
+                        {
+                            info.advanceState();
+                            logger.finer("run() JmDNS announced " + 
info.getQualifiedName() + " state " + info.getState());
+                            if (out == null)
+                            {
+                                out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
+                            }
+                            out.addAnswer(new DNSRecord.Pointer(info.type, 
DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, 
info.getQualifiedName()), 0);
+                            out.addAnswer(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, 
info.port, localHost.getName()), 0);
+                            out.addAnswer(new 
DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0);
+                        }
+                    }
+                }
+                if (out != null)
+                {
+                    logger.finer("run() JmDNS announced");
+                    send(out);
+                }
+                else
+                {
+                    // If we have nothing to send, another timer taskState 
ahead
+                    // of us has done the job for us. We can cancel.
+                    cancel();
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+
+            taskState = taskState.advance();
+            if (!taskState.isAnnounced())
+            {
+                cancel();
+
+            }
+        }
+    }
+
+    /**
+     * The Responder sends a single answer for the specified service infos
+     * and for the host name.
+     */
+    private class Responder extends TimerTask
+    {
+        private DNSIncoming in;
+        private InetAddress addr;
+        private int port;
+
+        public Responder(DNSIncoming in, InetAddress addr, int port)
+        {
+            this.in = in;
+            this.addr = addr;
+            this.port = port;
+        }
+
+        public void start()
+        {
+            // According to draft-cheshire-dnsext-multicastdns.txt
+            // chapter "8 Responding":
+            // We respond immediately if we know for sure, that we are
+            // the only one who can respond to the query.
+            // In all other cases, we respond within 20-120 ms.
+            //
+            // According to draft-cheshire-dnsext-multicastdns.txt
+            // chapter "7.2 Multi-Packet Known Answer Suppression":
+            // We respond after 20-120 ms if the query is truncated.
+
+            boolean iAmTheOnlyOne = true;
+            for (Iterator i = in.questions.iterator(); i.hasNext();)
+            {
+                DNSEntry entry = (DNSEntry) i.next();
+                if (entry instanceof DNSQuestion)
+                {
+                    DNSQuestion q = (DNSQuestion) entry;
+                    logger.finest("start() question=" + q);
+                    iAmTheOnlyOne &= (q.type == DNSConstants.TYPE_SRV
+                        || q.type == DNSConstants.TYPE_TXT
+                        || q.type == DNSConstants.TYPE_A
+                        || q.type == DNSConstants.TYPE_AAAA
+                        || localHost.getName().equalsIgnoreCase(q.name)
+                        || services.containsKey(q.name.toLowerCase()));
+                    if (!iAmTheOnlyOne)
+                    {
+                        break;
+                    }
+                }
+            }
+            int delay = (iAmTheOnlyOne && !in.isTruncated()) ? 0 : 
DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + 
random.nextInt(DNSConstants.RESPONSE_MAX_WAIT_INTERVAL - 
DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + 1) - in.elapseSinceArrival();
+            if (delay < 0)
+            {
+                delay = 0;
+            }
+            logger.finest("start() Responder chosen delay=" + delay);
+            timer.schedule(this, delay);
+        }
+
+        public void run()
+        {
+            synchronized (ioLock)
+            {
+                if (plannedAnswer == in)
+                {
+                    plannedAnswer = null;
+                }
+
+                // We use these sets to prevent duplicate records
+                // FIXME - This should be moved into DNSOutgoing
+                HashSet questions = new HashSet();
+                HashSet answers = new HashSet();
+
+
+                if (state == DNSState.ANNOUNCED)
+                {
+                    try
+                    {
+                        long now = System.currentTimeMillis();
+                        long expirationTime = now + 1; 
//=now+DNSConstants.KNOWN_ANSWER_TTL;
+                        boolean isUnicast = (port != DNSConstants.MDNS_PORT);
+
+
+                        // Answer questions
+                        for (Iterator iterator = in.questions.iterator(); 
iterator.hasNext();)
+                        {
+                            DNSEntry entry = (DNSEntry) iterator.next();
+                            if (entry instanceof DNSQuestion)
+                            {
+                                DNSQuestion q = (DNSQuestion) entry;
+
+                                // for unicast responses the question must be 
included
+                                if (isUnicast)
+                                {
+                                    //out.addQuestion(q);
+                                    questions.add(q);
+                                }
+
+                                int type = q.type;
+                                if (type == DNSConstants.TYPE_ANY || type == 
DNSConstants.TYPE_SRV)
+                                { // I ama not sure of why there is a special 
case here [PJYF Oct 15 2004]
+                                    if 
(localHost.getName().equalsIgnoreCase(q.getName()))
+                                    {
+                                        // type = DNSConstants.TYPE_A;
+                                        DNSRecord answer = 
localHost.getDNS4AddressRecord();
+                                        if (answer != null)
+                                        {
+                                            answers.add(answer);
+                                        }
+                                        answer = 
localHost.getDNS6AddressRecord();
+                                        if (answer != null)
+                                        {
+                                            answers.add(answer);
+                                        }
+                                        type = DNSConstants.TYPE_IGNORE;
+                                    }
+                                    else
+                                    {
+                                        if 
(serviceTypes.containsKey(q.getName().toLowerCase()))
+                                        {
+                                            type = DNSConstants.TYPE_PTR;
+                                        }
+                                    }
+                                }
+
+                                switch (type)
+                                {
+                                    case DNSConstants.TYPE_A:
+                                        {
+                                            // Answer a query for a domain name
+                                            //out = addAnswer( in, addr, port, 
out, host );
+                                            DNSRecord answer = 
localHost.getDNS4AddressRecord();
+                                            if (answer != null)
+                                            {
+                                                answers.add(answer);
+                                            }
+                                            break;
+                                        }
+                                    case DNSConstants.TYPE_AAAA:
+                                        {
+                                            // Answer a query for a domain name
+                                            DNSRecord answer = 
localHost.getDNS6AddressRecord();
+                                            if (answer != null)
+                                            {
+                                                answers.add(answer);
+                                            }
+                                            break;
+                                        }
+                                    case DNSConstants.TYPE_PTR:
+                                        {
+                                            // Answer a query for services of 
a given type
+
+                                            // find matching services
+                                            for (Iterator serviceIterator = 
services.values().iterator(); serviceIterator.hasNext();)
+                                            {
+                                                ServiceInfo info = 
(ServiceInfo) serviceIterator.next();
+                                                if (info.getState() == 
DNSState.ANNOUNCED)
+                                                {
+                                                    if 
(q.name.equalsIgnoreCase(info.type))
+                                                    {
+                                                        DNSRecord answer = 
localHost.getDNS4AddressRecord();
+                                                        if (answer != null)
+                                                        {
+                                                            
answers.add(answer);
+                                                        }
+                                                        answer = 
localHost.getDNS6AddressRecord();
+                                                        if (answer != null)
+                                                        {
+                                                            
answers.add(answer);
+                                                        }
+                                                        answers.add(new 
DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, 
DNSConstants.DNS_TTL, info.getQualifiedName()));
+                                                        answers.add(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, 
info.priority, info.weight, info.port, localHost.getName()));
+                                                        answers.add(new 
DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, 
info.text));
+                                                    }
+                                                }
+                                            }
+                                            if 
(q.name.equalsIgnoreCase("_services._mdns._udp.local."))
+                                            {
+                                                for (Iterator 
serviceTypeIterator = serviceTypes.values().iterator(); 
serviceTypeIterator.hasNext();)
+                                                {
+                                                    answers.add(new 
DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) 
serviceTypeIterator.next()));
+                                                }
+                                            }
+                                            break;
+                                        }
+                                    case DNSConstants.TYPE_SRV:
+                                    case DNSConstants.TYPE_ANY:
+                                    case DNSConstants.TYPE_TXT:
+                                        {
+                                            ServiceInfo info = (ServiceInfo) 
services.get(q.name.toLowerCase());
+                                            if (info != null && 
info.getState() == DNSState.ANNOUNCED)
+                                            {
+                                                DNSRecord answer = 
localHost.getDNS4AddressRecord();
+                                                if (answer != null)
+                                                {
+                                                    answers.add(answer);
+                                                }
+                                                answer = 
localHost.getDNS6AddressRecord();
+                                                if (answer != null)
+                                                {
+                                                    answers.add(answer);
+                                                }
+                                                answers.add(new 
DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, 
DNSConstants.DNS_TTL, info.getQualifiedName()));
+                                                answers.add(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, 
info.priority, info.weight, info.port, localHost.getName()));
+                                                answers.add(new 
DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, 
info.text));
+                                            }
+                                            break;
+                                        }
+                                    default :
+                                        {
+                                            
//System.out.println("JmDNSResponder.unhandled query:"+q);
+                                            break;
+                                        }
+                                }
+                            }
+                        }
+
+
+                        // remove known answers, if the ttl is at least half of
+                        // the correct value. (See Draft Cheshire chapter 
7.1.).
+                        for (Iterator i = in.answers.iterator(); i.hasNext();)
+                        {
+                            DNSRecord knownAnswer = (DNSRecord) i.next();
+                            if (knownAnswer.ttl > DNSConstants.DNS_TTL / 2 && 
answers.remove(knownAnswer))
+                            {
+                                logger.log(Level.FINER, "JmDNS Responder Known 
Answer Removed");
+                            }
+                        }
+
+
+                        // responde if we have answers
+                        if (answers.size() != 0)
+                        {
+                            logger.finer("run() JmDNS responding");
+                            DNSOutgoing out = null;
+                            if (isUnicast)
+                            {
+                                out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false);
+                            }
+
+                            for (Iterator i = questions.iterator(); 
i.hasNext();)
+                            {
+                                out.addQuestion((DNSQuestion) i.next());
+                            }
+                            for (Iterator i = answers.iterator(); i.hasNext();)
+                            {
+                                out = addAnswer(in, addr, port, out, 
(DNSRecord) i.next());
+                            }
+                            send(out);
+                        }
+                        cancel();
+                    }
+                    catch (Throwable e)
+                    {
+                        logger.log(Level.WARNING, "run() exception ", e);
+                        close();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper class to resolve service types.
+     * <p/>
+     * The TypeResolver queries three times consecutively for service types, 
and then
+     * removes itself from the timer.
+     * <p/>
+     * The TypeResolver will run only if JmDNS is in state ANNOUNCED.
+     */
+    private class TypeResolver extends TimerTask
+    {
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, 
DNSConstants.QUERY_WAIT_INTERVAL);
+        }
+
+        /**
+         * Counts the number of queries that were sent.
+         */
+        int count = 0;
+
+        public void run()
+        {
+            try
+            {
+                if (state == DNSState.ANNOUNCED)
+                {
+                    if (++count < 3)
+                    {
+                        logger.finer("run() JmDNS querying type");
+                        DNSOutgoing out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
+                        out.addQuestion(new 
DNSQuestion("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, 
DNSConstants.CLASS_IN));
+                        for (Iterator iterator = 
serviceTypes.values().iterator(); iterator.hasNext();)
+                        {
+                            out.addAnswer(new 
DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, 
DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) iterator.next()), 0);
+                        }
+                        send(out);
+                    }
+                    else
+                    {
+                        // After three queries, we can quit.
+                        cancel();
+                    }
+                    ;
+                }
+                else
+                {
+                    if (state == DNSState.CANCELED)
+                    {
+                        cancel();
+                    }
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+        }
+    }
+
+    /**
+     * The ServiceResolver queries three times consecutively for services of
+     * a given type, and then removes itself from the timer.
+     * <p/>
+     * The ServiceResolver will run only if JmDNS is in state ANNOUNCED.
+     * REMIND: Prevent having multiple service resolvers for the same type in 
the
+     * timer queue.
+     */
+    private class ServiceResolver extends TimerTask
+    {
+        /**
+         * Counts the number of queries being sent.
+         */
+        int count = 0;
+        private String type;
+
+        public ServiceResolver(String type)
+        {
+            this.type = type;
+        }
+
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, 
DNSConstants.QUERY_WAIT_INTERVAL);
+        }
+
+        public void run()
+        {
+            try
+            {
+                if (state == DNSState.ANNOUNCED)
+                {
+                    if (count++ < 3)
+                    {
+                        logger.finer("run() JmDNS querying service");
+                        long now = System.currentTimeMillis();
+                        DNSOutgoing out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
+                        out.addQuestion(new DNSQuestion(type, 
DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN));
+                        for (Iterator s = services.values().iterator(); 
s.hasNext();)
+                        {
+                            final ServiceInfo info = (ServiceInfo) s.next();
+                            try
+                            {
+                                out.addAnswer(new DNSRecord.Pointer(info.type, 
DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, 
info.getQualifiedName()), now);
+                            }
+                            catch (IOException ee)
+                            {
+                                break;
+                            }
+                        }
+                        send(out);
+                    }
+                    else
+                    {
+                        // After three queries, we can quit.
+                        cancel();
+                    }
+                    ;
+                }
+                else
+                {
+                    if (state == DNSState.CANCELED)
+                    {
+                        cancel();
+                    }
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+        }
+    }
+
+    /**
+     * The ServiceInfoResolver queries up to three times consecutively for
+     * a service info, and then removes itself from the timer.
+     * <p/>
+     * The ServiceInfoResolver will run only if JmDNS is in state ANNOUNCED.
+     * REMIND: Prevent having multiple service resolvers for the same info in 
the
+     * timer queue.
+     */
+    private class ServiceInfoResolver extends TimerTask
+    {
+        /**
+         * Counts the number of queries being sent.
+         */
+        int count = 0;
+        private ServiceInfo info;
+
+        public ServiceInfoResolver(ServiceInfo info)
+        {
+            this.info = info;
+            info.dns = JmDNS.this;
+            addListener(info, new DNSQuestion(info.getQualifiedName(), 
DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
+        }
+
+        public void start()
+        {
+            timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, 
DNSConstants.QUERY_WAIT_INTERVAL);
+        }
+
+        public void run()
+        {
+            try
+            {
+                if (state == DNSState.ANNOUNCED)
+                {
+                    if (count++ < 3 && !info.hasData())
+                    {
+                        long now = System.currentTimeMillis();
+                        DNSOutgoing out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
+                        out.addQuestion(new 
DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN));
+                        out.addQuestion(new 
DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN));
+                        if (info.server != null)
+                        {
+                            out.addQuestion(new DNSQuestion(info.server, 
DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
+                        }
+                        out.addAnswer((DNSRecord) 
cache.get(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN), now);
+                        out.addAnswer((DNSRecord) 
cache.get(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN), now);
+                        if (info.server != null)
+                        {
+                            out.addAnswer((DNSRecord) cache.get(info.server, 
DNSConstants.TYPE_A, DNSConstants.CLASS_IN), now);
+                        }
+                        send(out);
+                    }
+                    else
+                    {
+                        // After three queries, we can quit.
+                        cancel();
+                        removeListener(info);
+                    }
+                    ;
+                }
+                else
+                {
+                    if (state == DNSState.CANCELED)
+                    {
+                        cancel();
+                        removeListener(info);
+                    }
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+        }
+    }
+
+    /**
+     * The Canceler sends two announces with TTL=0 for the specified services.
+     */
+    private class Canceler extends TimerTask
+    {
+        /**
+         * Counts the number of announces being sent.
+         */
+        int count = 0;
+        /**
+         * The services that need cancelling.
+         * Note: We have to use a local variable here, because the services
+         * that are canceled, are removed immediately from variable 
JmDNS.services.
+         */
+        private ServiceInfo[] infos;
+        /**
+         * We call notifyAll() on the lock object, when we have canceled the
+         * service infos.
+         * This is used by method JmDNS.unregisterService() and
+         * JmDNS.unregisterAllServices, to ensure that the JmDNS
+         * socket stays open until the Canceler has canceled all services.
+         * <p/>
+         * Note: We need this lock, because ServiceInfos do the transition from
+         * state ANNOUNCED to state CANCELED before we get here. We could get
+         * rid of this lock, if we added a state named CANCELLING to DNSState.
+         */
+        private Object lock;
+        int ttl = 0;
+
+        public Canceler(ServiceInfo info, Object lock)
+        {
+            this.infos = new ServiceInfo[]{info};
+            this.lock = lock;
+            addListener(info, new DNSQuestion(info.getQualifiedName(), 
DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN));
+        }
+
+        public Canceler(ServiceInfo[] infos, Object lock)
+        {
+            this.infos = infos;
+            this.lock = lock;
+        }
+
+        public Canceler(Collection infos, Object lock)
+        {
+            this.infos = (ServiceInfo[]) infos.toArray(new 
ServiceInfo[infos.size()]);
+            this.lock = lock;
+        }
+
+        public void start()
+        {
+            timer.schedule(this, 0, DNSConstants.ANNOUNCE_WAIT_INTERVAL);
+        }
+
+        public void run()
+        {
+            try
+            {
+                if (++count < 3)
+                {
+                    logger.finer("run() JmDNS canceling service");
+                    // announce the service
+                    //long now = System.currentTimeMillis();
+                    DNSOutgoing out = new 
DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA);
+                    for (int i = 0; i < infos.length; i++)
+                    {
+                        ServiceInfo info = infos[i];
+                        out.addAnswer(new DNSRecord.Pointer(info.type, 
DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, ttl, info.getQualifiedName()), 0);
+                        out.addAnswer(new 
DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, 
DNSConstants.CLASS_IN, ttl, info.priority, info.weight, info.port, 
localHost.getName()), 0);
+                        out.addAnswer(new 
DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, 
DNSConstants.CLASS_IN, ttl, info.text), 0);
+                        DNSRecord answer = localHost.getDNS4AddressRecord();
+                        if (answer != null)
+                        {
+                            out.addAnswer(answer, 0);
+                        }
+                        answer = localHost.getDNS6AddressRecord();
+                        if (answer != null)
+                        {
+                            out.addAnswer(answer, 0);
+                        }
+                    }
+                    send(out);
+                }
+                else
+                {
+                    // After three successful announcements, we are finished.
+                    synchronized (lock)
+                    {
+                        closed=true;
+                        lock.notifyAll();
+                    }
+                    cancel();
+                }
+            }
+            catch (Throwable e)
+            {
+                logger.log(Level.WARNING, "run() exception ", e);
+                recover();
+            }
+        }
+    }
+    
+    // REMIND: Why is this not an anonymous inner class?
+    /**
+     * Shutdown operations.
+     */
+    private class Shutdown implements Runnable
+    {
+        public void run()
+        {
+            shutdown = null;
+            close();
+        }
+    }
+
+    /**
+     * Recover jmdns when there is an error.
+     */
+    protected void recover()
+    {
+        logger.finer("recover()");
+        // 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 (DNSState.CANCELED != state)
+        {
+            synchronized (this)
+            { // Synchronize only if we are not already in process to prevent 
dead locks
+                //
+                logger.finer("recover() Cleanning up");
+                // Stop JmDNS
+                state = DNSState.CANCELED; // This protects against recursive 
calls
+
+                // We need to keep a copy for reregistration
+                Collection oldServiceInfos = new ArrayList(services.values());
+
+                // Cancel all services
+                unregisterAllServices();
+                disposeServiceCollectors();
+                //
+                // close multicast socket
+                closeMulticastSocket();
+                //
+                cache.clear();
+                logger.finer("recover() All is clean");
+                //
+                // All is clear now start the services
+                //
+                try
+                {
+                    openMulticastSocket(localHost);
+                    start(oldServiceInfos);
+                }
+                catch (Exception exception)
+                {
+                    logger.log(Level.WARNING, "recover() Start services 
exception ", exception);
+                }
+                logger.log(Level.WARNING, "recover() We are back!");
+            }
+        }
+    }
+
+    /**
+     * Close down jmdns. Release all resources and unregister all services.
+     */
+    public void close()
+    {
+        if (state != DNSState.CANCELED)
+        {
+            synchronized (this)
+            { // Synchronize only if we are not already in process to prevent 
dead locks
+                // Stop JmDNS
+                state = DNSState.CANCELED; // This protects against recursive 
calls
+
+                unregisterAllServices();
+                disposeServiceCollectors();
+
+                // close socket
+                closeMulticastSocket();
+
+                // Stop the timer
+                timer.cancel();
+
+                // remove the shutdown hook
+                if (shutdown != null)
+                {
+                    Runtime.getRuntime().removeShutdownHook(shutdown);
+                }
+
+            }
+        }
+    }
+
+    /**
+     * List cache entries, for debugging only.
+     */
+    void print()
+    {
+        System.out.println("---- cache ----");
+        cache.print();
+        System.out.println();
+    }
+
+    /**
+     * List Services and serviceTypes.
+     * Debugging Only
+     */
+
+    public void printServices()
+    {
+        System.err.println(toString());
+    }
+
+    public String toString()
+    {
+        StringBuffer aLog = new StringBuffer();
+        aLog.append("\t---- Services -----");
+        if (services != null)
+        {
+            for (Iterator k = services.keySet().iterator(); k.hasNext();)
+            {
+                Object key = k.next();
+                aLog.append("\n\t\tService: " + key + ": " + 
services.get(key));
+            }
+        }
+        aLog.append("\n");
+        aLog.append("\t---- Types ----");
+        if (serviceTypes != null)
+        {
+            for (Iterator k = serviceTypes.keySet().iterator(); k.hasNext();)
+            {
+                Object key = k.next();
+                aLog.append("\n\t\tType: " + key + ": " + 
serviceTypes.get(key));
+            }
+        }
+        aLog.append("\n");
+        aLog.append(cache.toString());
+        aLog.append("\n");
+        aLog.append("\t---- Service Collectors ----");
+        if (serviceCollectors != null)
+        {
+            synchronized (serviceCollectors)
+            {
+                for (Iterator k = serviceCollectors.keySet().iterator(); 
k.hasNext();)
+                {
+                    Object key = k.next();
+                    aLog.append("\n\t\tService Collector: " + key + ": " + 
serviceCollectors.get(key));
+                }
+                serviceCollectors.clear();
+            }
+        }
+        return aLog.toString();
+    }
+
+    /**
+     * Returns a list of service infos of the specified type.
+     *
+     * @param type Service type name, such as <code>_http._tcp.local.</code>.
+     * @return An array of service instance names.
+     */
+    public ServiceInfo[] list(String type)
+    {
+        // 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.
+
+        ServiceCollector collector;
+
+        boolean newCollectorCreated;
+        synchronized (serviceCollectors)
+        {
+            collector = (ServiceCollector) serviceCollectors.get(type);
+            if (collector == null)
+            {
+                collector = new ServiceCollector(type);
+                serviceCollectors.put(type, collector);
+                addServiceListener(type, collector);
+                newCollectorCreated = true;
+            }
+            else
+            {
+                newCollectorCreated = false;
+            }
+        }
+
+        // After creating a new ServiceCollector, we collect service infos for
+        // 200 milliseconds. This should be enough time, to get some service
+        // infos from the network.
+        if (newCollectorCreated)
+        {
+            try
+            {
+                Thread.sleep(200);
+            }
+            catch (InterruptedException e)
+            {
+            }
+        }
+
+        return collector.list();
+    }
+
+    /**
+     * This method disposes all ServiceCollector instances which have been
+     * created by calls to method <code>list(type)</code>.
+     *
+     * @see #list
+     */
+    private void disposeServiceCollectors()
+    {
+        logger.finer("disposeServiceCollectors()");
+        synchronized (serviceCollectors)
+        {
+            for (Iterator i = serviceCollectors.values().iterator(); 
i.hasNext();)
+            {
+                ServiceCollector collector = (ServiceCollector) i.next();
+                removeServiceListener(collector.type, collector);
+            }
+            serviceCollectors.clear();
+        }
+    }
+
+    /**
+     * 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 = 
Logger.getLogger(ServiceCollector.class.toString());
+        /**
+         * A set of collected service instance names.
+         */
+        private Map infos = Collections.synchronizedMap(new HashMap());
+
+        public String type;
+
+        public ServiceCollector(String type)
+        {
+            this.type = type;
+        }
+
+        /**
+         * A service has been added.
+         */
+        public void serviceAdded(ServiceEvent event)
+        {
+            synchronized (infos)
+            {
+                event.getDNS().requestServiceInfo(event.getType(), 
event.getName(), 0);
+            }
+        }
+
+        /**
+         * A service has been removed.
+         */
+        public void serviceRemoved(ServiceEvent event)
+        {
+            synchronized (infos)
+            {
+                infos.remove(event.getName());
+            }
+        }
+
+        /**
+         * A service hase been resolved. Its details are now available in the
+         * ServiceInfo record.
+         */
+        public void serviceResolved(ServiceEvent event)
+        {
+            synchronized (infos)
+            {
+                infos.put(event.getName(), event.getInfo());
+            }
+        }
+
+        /**
+         * Returns an array of all service infos which have been collected by 
this
+         * ServiceCollector.
+         */
+        public ServiceInfo[] list()
+        {
+            synchronized (infos)
+            {
+                return (ServiceInfo[]) infos.values().toArray(new 
ServiceInfo[infos.size()]);
+            }
+        }
+
+        public String toString()
+        {
+            StringBuffer aLog = new StringBuffer();
+            synchronized (infos)
+            {
+                for (Iterator k = infos.keySet().iterator(); k.hasNext();)
+                {
+                    Object key = k.next();
+                    aLog.append("\n\t\tService: " + key + ": " + 
infos.get(key));
+                }
+            }
+            return aLog.toString();
+        }
+    };
+    
+    private static String toUnqualifiedName(String type, String qualifiedName)
+    {
+        if (qualifiedName.endsWith(type))
+        {
+            return qualifiedName.substring(0, qualifiedName.length() - 
type.length() - 1);
+        }
+        else
+        {
+            return qualifiedName;
+        }
+    }
+}
+

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java                   
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java   2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,99 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.EventObject;
+import java.util.logging.Logger;
+
+/**
+ * ServiceEvent.
+ *
+ * @author Werner Randelshofer, Rick Blair
+ * @version %I%, %G%
+ */
+public class ServiceEvent extends EventObject
+{
+    private static Logger logger = 
Logger.getLogger(ServiceEvent.class.toString());
+    /**
+     * The type name of the service.
+     */
+    private String type;
+    /**
+     * The instance name of the service. Or null, if the event was
+     * fired to a service type listener.
+     */
+    private String name;
+    /**
+     * The service info record, or null if the service could be be resolved.
+     * This is also null, if the event was fired to a service type listener.
+     */
+    private ServiceInfo info;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param source the JmDNS instance which originated the event.
+     * @param type   the type name of the service.
+     * @param name   the instance name of the service.
+     * @param info   the service info record, or null if the service could be 
be resolved.
+     */
+    public ServiceEvent(JmDNS source, String type, String name, ServiceInfo 
info)
+    {
+        super(source);
+        this.type = type;
+        this.name = name;
+        this.info = info;
+    }
+
+    /**
+     * Returns the JmDNS instance which originated the event.
+     */
+    public JmDNS getDNS()
+    {
+        return (JmDNS) getSource();
+    }
+
+    /**
+     * Returns the fully qualified type of the service.
+     */
+    public String getType()
+    {
+        return type;
+    }
+
+    /**
+     * Returns the instance name of the service.
+     * Always returns null, if the event is sent to a service type listener.
+     */
+    public String getName()
+    {
+        return name;
+    }
+
+    /**
+     * Returns the service info record, or null if the service could not be
+     * resolved.
+     * Always returns null, if the event is sent to a service type listener.
+     */
+    public ServiceInfo getInfo()
+    {
+        return info;
+    }
+
+    public String toString()
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append("<" + getClass().getName() + "> ");
+        buf.append(super.toString());
+        buf.append(" name ");
+        buf.append(getName());
+        buf.append(" type ");
+        buf.append(getType());
+        buf.append(" info ");
+        buf.append(getInfo());
+        return buf.toString();
+    }
+
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java                    
        (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java    2007-03-04 
15:23:01 UTC (rev 11954)
@@ -0,0 +1,659 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.TimerTask;
+import java.util.Vector;
+import java.util.logging.Logger;
+
+/**
+ * JmDNS service information.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
+ */
+public class ServiceInfo implements DNSListener
+{
+    private static Logger logger = 
Logger.getLogger(ServiceInfo.class.toString());
+    public final static byte[] NO_VALUE = new byte[0];
+    JmDNS dns;
+    
+    // State machine
+    /**
+     * The state of this service info.
+     * This is used only for services announced by JmDNS.
+     * <p/>
+     * For proper handling of concurrency, this variable must be
+     * changed only using methods advanceState(), revertState() and cancel().
+     */
+    private DNSState state = DNSState.PROBING_1;
+
+    /**
+     * Task associated to this service info.
+     * Possible tasks are JmDNS.Prober, JmDNS.Announcer, JmDNS.Responder,
+     * JmDNS.Canceler.
+     */
+    TimerTask task;
+
+    String type;
+    private String name;
+    String server;
+    int port;
+    int weight;
+    int priority;
+    byte text[];
+    Hashtable props;
+    InetAddress addr;
+
+
+    /**
+     * Construct a service description for registrating with JmDNS.
+     *
+     * @param type fully qualified service type name, such as 
<code>_http._tcp.local.</code>.
+     * @param name unqualified service instance name, such as 
<code>foobar</code>
+     * @param port the local port on which the service runs
+     * @param text string describing the service
+     */
+    public ServiceInfo(String type, String name, int port, String text)
+    {
+        this(type, name, port, 0, 0, text);
+    }
+
+    /**
+     * Construct a service description for registrating with JmDNS.
+     *
+     * @param type     fully qualified service type name, such as 
<code>_http._tcp.local.</code>.
+     * @param name     unqualified service instance name, such as 
<code>foobar</code>
+     * @param port     the local port on which the service runs
+     * @param weight   weight of the service
+     * @param priority priority of the service
+     * @param text     string describing the service
+     */
+    public ServiceInfo(String type, String name, int port, int weight, int 
priority, String text)
+    {
+        this(type, name, port, weight, priority, (byte[]) null);
+        try
+        {
+            ByteArrayOutputStream out = new 
ByteArrayOutputStream(text.length());
+            writeUTF(out, text);
+            this.text = out.toByteArray();
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("unexpected exception: " + e);
+        }
+    }
+
+    /**
+     * Construct a service description for registrating with JmDNS. The 
properties hashtable must
+     * map property names to either Strings or byte arrays describing the 
property values.
+     *
+     * @param type     fully qualified service type name, such as 
<code>_http._tcp.local.</code>.
+     * @param name     unqualified service instance name, such as 
<code>foobar</code>
+     * @param port     the local port on which the service runs
+     * @param weight   weight of the service
+     * @param priority priority of the service
+     * @param props    properties describing the service
+     */
+    public ServiceInfo(String type, String name, int port, int weight, int 
priority, Hashtable props)
+    {
+        this(type, name, port, weight, priority, new byte[0]);
+        if (props != null)
+        {
+            try
+            {
+                ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+                for (Enumeration e = props.keys(); e.hasMoreElements();)
+                {
+                    String key = (String) e.nextElement();
+                    Object val = props.get(key);
+                    ByteArrayOutputStream out2 = new 
ByteArrayOutputStream(100);
+                    writeUTF(out2, key);
+                    if (val instanceof String)
+                    {
+                        out2.write('=');
+                        writeUTF(out2, (String) val);
+                    }
+                    else
+                    {
+                        if (val instanceof byte[])
+                        {
+                            out2.write('=');
+                            byte[] bval = (byte[]) val;
+                            out2.write(bval, 0, bval.length);
+                        }
+                        else
+                        {
+                            if (val != NO_VALUE)
+                            {
+                                throw new IllegalArgumentException("invalid 
property value: " + val);
+                            }
+                        }
+                    }
+                    byte data[] = out2.toByteArray();
+                    out.write(data.length);
+                    out.write(data, 0, data.length);
+                }
+                this.text = out.toByteArray();
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException("unexpected exception: " + e);
+            }
+        }
+    }
+
+    /**
+     * Construct a service description for registrating with JmDNS.
+     *
+     * @param type     fully qualified service type name, such as 
<code>_http._tcp.local.</code>.
+     * @param name     unqualified service instance name, such as 
<code>foobar</code>
+     * @param port     the local port on which the service runs
+     * @param weight   weight of the service
+     * @param priority priority of the service
+     * @param text     bytes describing the service
+     */
+    public ServiceInfo(String type, String name, int port, int weight, int 
priority, byte text[])
+    {
+        this.type = type;
+        this.name = name;
+        this.port = port;
+        this.weight = weight;
+        this.priority = priority;
+        this.text = text;
+    }
+
+    /**
+     * Construct a service record during service discovery.
+     */
+    ServiceInfo(String type, String name)
+    {
+        if (!type.endsWith("."))
+        {
+            throw new IllegalArgumentException("type must be fully qualified 
DNS name ending in '.': " + type);
+        }
+
+        this.type = type;
+        this.name = name;
+    }
+
+    /**
+     * During recovery we need to duplicate service info to reregister them
+     */
+    public ServiceInfo(ServiceInfo info)
+    {
+        if (info != null)
+        {
+            this.type = info.type;
+            this.name = info.name;
+            this.port = info.port;
+            this.weight = info.weight;
+            this.priority = info.priority;
+            this.text = info.text;
+        }
+    }
+
+    /**
+     * Fully qualified service type name, such as 
<code>_http._tcp.local.</code> .
+     */
+    public String getType()
+    {
+        return type;
+    }
+
+    /**
+     * Unqualified service instance name, such as <code>foobar</code> .
+     */
+    public String getName()
+    {
+        return name;
+    }
+
+    /**
+     * Sets the service instance name.
+     *
+     * @param name unqualified service instance name, such as 
<code>foobar</code>
+     */
+    void setName(String name)
+    {
+        this.name = name;
+    }
+
+    /**
+     * Fully qualified service name, such as 
<code>foobar._http._tcp.local.</code> .
+     */
+    public String getQualifiedName()
+    {
+        return name + "." + type;
+    }
+
+    /**
+     * Get the name of the server.
+     */
+    public String getServer()
+    {
+        return server;
+    }
+
+    /**
+     * Get the host address of the service (ie X.X.X.X).
+     */
+    public String getHostAddress()
+    {
+        return (addr != null ? addr.getHostAddress() : "");
+    }
+
+    public InetAddress getAddress()
+    {
+        return addr;
+    }
+
+    /**
+     * Get the InetAddress of the service.
+     */
+    public InetAddress getInetAddress()
+    {
+        return addr;
+    }
+
+    /**
+     * Get the port for the service.
+     */
+    public int getPort()
+    {
+        return port;
+    }
+
+    /**
+     * Get the priority of the service.
+     */
+    public int getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Get the weight of the service.
+     */
+    public int getWeight()
+    {
+        return weight;
+    }
+
+    /**
+     * Get the text for the serivce as raw bytes.
+     */
+    public byte[] getTextBytes()
+    {
+        return text;
+    }
+
+    /**
+     * Get the text for the service. This will interpret the text bytes
+     * as a UTF8 encoded string. Will return null if the bytes are not
+     * a valid UTF8 encoded string.
+     */
+    public String getTextString()
+    {
+        if ((text == null) || (text.length == 0) || ((text.length == 1) && 
(text[0] == 0)))
+        {
+            return null;
+        }
+        return readUTF(text, 0, text.length);
+    }
+
+    /**
+     * Get the URL for this service. An http URL is created by
+     * combining the address, port, and path properties.
+     */
+    public String getURL()
+    {
+        return getURL("http");
+    }
+
+    /**
+     * Get the URL for this service. An URL is created by
+     * combining the protocol, address, port, and path properties.
+     */
+    public String getURL(String protocol)
+    {
+        String url = protocol + "://" + getAddress() + ":" + getPort();
+        String path = getPropertyString("path");
+        if (path != null)
+        {
+            if (path.indexOf("://") >= 0)
+            {
+                url = path;
+            }
+            else
+            {
+                url += path.startsWith("/") ? path : "/" + path;
+            }
+        }
+        return url;
+    }
+
+    /**
+     * Get a property of the service. This involves decoding the
+     * text bytes into a property list. Returns null if the property
+     * is not found or the text data could not be decoded correctly.
+     */
+    public synchronized byte[] getPropertyBytes(String name)
+    {
+        return (byte[]) getProperties().get(name);
+    }
+
+    /**
+     * Get a property of the service. This involves decoding the
+     * text bytes into a property list. Returns null if the property
+     * is not found, the text data could not be decoded correctly, or
+     * the resulting bytes are not a valid UTF8 string.
+     */
+    public synchronized String getPropertyString(String name)
+    {
+        byte data[] = (byte[]) getProperties().get(name);
+        if (data == null)
+        {
+            return null;
+        }
+        if (data == NO_VALUE)
+        {
+            return "true";
+        }
+        return readUTF(data, 0, data.length);
+    }
+
+    /**
+     * Enumeration of the property names.
+     */
+    public Enumeration getPropertyNames()
+    {
+        Hashtable props = getProperties();
+        return (props != null) ? props.keys() : new Vector().elements();
+    }
+
+    /**
+     * Write a UTF string with a length to a stream.
+     */
+    void writeUTF(OutputStream out, String str) throws IOException
+    {
+        for (int i = 0, len = str.length(); i < len; i++)
+        {
+            int c = str.charAt(i);
+            if ((c >= 0x0001) && (c <= 0x007F))
+            {
+                out.write(c);
+            }
+            else
+            {
+                if (c > 0x07FF)
+                {
+                    out.write(0xE0 | ((c >> 12) & 0x0F));
+                    out.write(0x80 | ((c >> 6) & 0x3F));
+                    out.write(0x80 | ((c >> 0) & 0x3F));
+                }
+                else
+                {
+                    out.write(0xC0 | ((c >> 6) & 0x1F));
+                    out.write(0x80 | ((c >> 0) & 0x3F));
+                }
+            }
+        }
+    }
+
+    /**
+     * Read data bytes as a UTF stream.
+     */
+    String readUTF(byte data[], int off, int len)
+    {
+        StringBuffer buf = new StringBuffer();
+        for (int end = off + len; off < end;)
+        {
+            int ch = data[off++] & 0xFF;
+            switch (ch >> 4)
+            {
+                case 0:
+                case 1:
+                case 2:
+                case 3:
+                case 4:
+                case 5:
+                case 6:
+                case 7:
+                    // 0xxxxxxx
+                    break;
+                case 12:
+                case 13:
+                    if (off >= len)
+                    {
+                        return null;
+                    }
+                    // 110x xxxx   10xx xxxx
+                    ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F);
+                    break;
+                case 14:
+                    if (off + 2 >= len)
+                    {
+                        return null;
+                    }
+                    // 1110 xxxx  10xx xxxx  10xx xxxx
+                    ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6) | 
(data[off++] & 0x3F);
+                    break;
+                default:
+                    if (off + 1 >= len)
+                    {
+                        return null;
+                    }
+                    // 10xx xxxx,  1111 xxxx
+                    ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f);
+                    break;
+            }
+            buf.append((char) ch);
+        }
+        return buf.toString();
+    }
+
+    synchronized Hashtable getProperties()
+    {
+        if ((props == null) && (text != null))
+        {
+            Hashtable props = new Hashtable();
+            int off = 0;
+            while (off < text.length)
+            {
+                // length of the next key value pair
+                int len = text[off++] & 0xFF;
+                if ((len == 0) || (off + len > text.length))
+                {
+                    props.clear();
+                    break;
+                }
+                // look for the '='
+                int i = 0;
+                for (; (i < len) && (text[off + i] != '='); i++)
+                {
+                    ;
+                }
+
+                // get the property name
+                String name = readUTF(text, off, i);
+                if (name == null)
+                {
+                    props.clear();
+                    break;
+                }
+                if (i == len)
+                {
+                    props.put(name, NO_VALUE);
+                }
+                else
+                {
+                    byte value[] = new byte[len - ++i];
+                    System.arraycopy(text, off + i, value, 0, len - i);
+                    props.put(name, value);
+                    off += len;
+                }
+            }
+            this.props = props;
+        }
+        return props;
+    }
+    
+    // REMIND: Oops, this shouldn't be public!
+    /**
+     * JmDNS callback to update a DNS record.
+     */
+    public void updateRecord(JmDNS jmdns, long now, DNSRecord rec)
+    {
+        if ((rec != null) && !rec.isExpired(now))
+        {
+            switch (rec.type)
+            {
+                case DNSConstants.TYPE_A:              // IPv4
+                case DNSConstants.TYPE_AAAA:   // IPv6 FIXME [PJYF Oct 14 
2004] This has not been tested
+                    if (rec.name.equals(server))
+                    {
+                        addr = ((DNSRecord.Address) rec).getAddress();
+
+                    }
+                    break;
+                case DNSConstants.TYPE_SRV:
+                    if (rec.name.equals(getQualifiedName()))
+                    {
+                        DNSRecord.Service srv = (DNSRecord.Service) rec;
+                        server = srv.server;
+                        port = srv.port;
+                        weight = srv.weight;
+                        priority = srv.priority;
+                        addr = null;
+                        // changed to use getCache() instead - jeffs
+                        // updateRecord(jmdns, now, 
(DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN));
+                        updateRecord(jmdns, now, (DNSRecord) 
jmdns.getCache().get(server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
+                    }
+                    break;
+                case DNSConstants.TYPE_TXT:
+                    if (rec.name.equals(getQualifiedName()))
+                    {
+                        DNSRecord.Text txt = (DNSRecord.Text) rec;
+                        text = txt.text;
+                    }
+                    break;
+            }
+            // Future Design Pattern
+            // This is done, to notify the wait loop in method
+            // JmDNS.getServiceInfo(type, name, timeout);
+            if (hasData() && dns != null)
+            {
+                dns.handleServiceResolved(this);
+                dns = null;
+            }
+            synchronized (this)
+            {
+                notifyAll();
+            }
+        }
+    }
+
+    /**
+     * Returns true if the service info is filled with data.
+     */
+    boolean hasData()
+    {
+        return server != null && addr != null && text != null;
+    }
+    
+    
+    // State machine
+    /**
+     * Sets the state and notifies all objects that wait on the ServiceInfo.
+     */
+    synchronized void advanceState()
+    {
+        state = state.advance();
+        notifyAll();
+    }
+
+    /**
+     * Sets the state and notifies all objects that wait on the ServiceInfo.
+     */
+    synchronized void revertState()
+    {
+        state = state.revert();
+        notifyAll();
+    }
+
+    /**
+     * Sets the state and notifies all objects that wait on the ServiceInfo.
+     */
+    synchronized void cancel()
+    {
+        state = DNSState.CANCELED;
+        notifyAll();
+    }
+
+    /**
+     * Returns the current state of this info.
+     */
+    DNSState getState()
+    {
+        return state;
+    }
+
+
+    public int hashCode()
+    {
+        return getQualifiedName().hashCode();
+    }
+
+    public boolean equals(Object obj)
+    {
+        return (obj instanceof ServiceInfo) && 
getQualifiedName().equals(((ServiceInfo) obj).getQualifiedName());
+    }
+
+    public String getNiceTextString()
+    {
+        StringBuffer buf = new StringBuffer();
+        for (int i = 0, len = text.length; i < len; i++)
+        {
+            if (i >= 20)
+            {
+                buf.append("...");
+                break;
+            }
+            int ch = text[i] & 0xFF;
+            if ((ch < ' ') || (ch > 127))
+            {
+                buf.append("\\0");
+                buf.append(Integer.toString(ch, 8));
+            }
+            else
+            {
+                buf.append((char) ch);
+            }
+        }
+        return buf.toString();
+    }
+
+    public String toString()
+    {
+        StringBuffer buf = new StringBuffer();
+        buf.append("service[");
+        buf.append(getQualifiedName());
+        buf.append(',');
+        buf.append(getAddress());
+        buf.append(':');
+        buf.append(port);
+        buf.append(',');
+        buf.append(getNiceTextString());
+        buf.append(']');
+        return buf.toString();
+    }
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java                
                (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java        
2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,42 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.EventListener;
+
+/**
+ * Listener for service updates.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Werner Randelshofer
+ */
+public interface ServiceListener extends EventListener
+{
+    /**
+     * A service has been added.
+     *
+     * @param event The ServiceEvent providing the name and fully qualified 
type
+     *              of the service.
+     */
+    void serviceAdded(ServiceEvent event);
+
+    /**
+     * A service has been removed.
+     *
+     * @param event The ServiceEvent providing the name and fully qualified 
type
+     *              of the service.
+     */
+    void serviceRemoved(ServiceEvent event);
+
+    /**
+     * A service has been resolved. Its details are now available in the
+     * ServiceInfo record.
+     *
+     * @param event The ServiceEvent providing the name, the fully qualified
+     *              type of the service, and the service info record, or null 
if the service
+     *              could not be resolved.
+     */
+    void serviceResolved(ServiceEvent event);
+}

Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java
===================================================================
--- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java            
                (rev 0)
+++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java    
2007-03-04 15:23:01 UTC (rev 11954)
@@ -0,0 +1,24 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+
+package plugins.MDNSDiscovery.javax.jmdns;
+
+import java.util.EventListener;
+
+/**
+ * Listener for service types.
+ *
+ * @version %I%, %G%
+ * @author     Arthur van Hoff, Werner Randelshofer
+ */
+public interface ServiceTypeListener extends EventListener
+{
+    /**
+     * A new service type was discovered.
+     *
+     * @param event The service event providing the fully qualified type of
+     *              the service.
+     */
+    void serviceTypeAdded(ServiceEvent event);
+}


Reply via email to