aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDamian Minkov <damencho@jitsi.org>2007-06-11 10:47:51 +0000
committerDamian Minkov <damencho@jitsi.org>2007-06-11 10:47:51 +0000
commit152b663a227d0005c2007ae7d59e1ec75e08eb03 (patch)
tree2ffd67cb4c0dcac1b8173a8a8d806833e7b69160
parent4105273561872e2cf8e64d00db5b2d59d9d97ab9 (diff)
downloadjitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.zip
jitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.tar.gz
jitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.tar.bz2
Zeroconf protocol provider.
-rw-r--r--build.xml26
-rw-r--r--resources/images/zeroconf/zeroconf-away.pngbin0 -> 728 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-color-16.pngbin0 -> 781 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-color-64.pngbin0 -> 4985 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-dnd.pngbin0 -> 755 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-invisible.pngbin0 -> 775 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-offline.pngbin0 -> 494 bytes
-rw-r--r--resources/images/zeroconf/zeroconf-online.pngbin0 -> 781 bytes
-rw-r--r--resources/images/zeroconf/zeroconf.pngbin0 -> 2737 bytes
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/BonjourService.java666
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ClientThread.java480
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ContactGroupZeroconfImpl.java597
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ContactZeroconfImpl.java467
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/MessageZeroconfImpl.java294
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetBasicInstantMessagingZeroconfImpl.java275
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetPersistentPresenceZeroconfImpl.java1219
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetTypingNotificationsZeroconfImpl.java164
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolIconZeroconfImpl.java102
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderFactoryZeroconfImpl.java285
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderServiceZeroconfImpl.java400
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfAccountID.java84
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfActivator.java120
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfStatusEnum.java149
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSCache.java277
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSConstants.java147
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSEntry.java167
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSIncoming.java507
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSListener.java25
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSOutgoing.java390
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSQuestion.java53
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSRecord.java764
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSState.java123
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/HostInfo.java158
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/JmDNS.java3072
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceEvent.java109
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceInfo.java766
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceListener.java44
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceTypeListener.java23
-rw-r--r--src/net/java/sip/communicator/impl/protocol/zeroconf/zeroconf.provider.manifest.mf11
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/FirstWizardPage.java383
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/Resources.java100
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccRegWizzActivator.java111
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistration.java119
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistrationWizard.java208
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/resources.properties11
-rw-r--r--src/net/java/sip/communicator/plugin/zeroconfaccregwizz/zeroconfaccregwizz.manifest.mf30
-rw-r--r--src/net/java/sip/communicator/service/protocol/ProtocolNames.java4
47 files changed, 12930 insertions, 0 deletions
diff --git a/build.xml b/build.xml
index 82763c6..f9e3a3f 100644
--- a/build.xml
+++ b/build.xml
@@ -854,6 +854,7 @@
bundle-gibberish-slick,bundle-plugin-gibberishaccregwizz,
bundle-plugin-extended-callhistory-search,
bundle-rss,bundle-plugin-rssaccregwizz,
+ bundle-zeroconf,bundle-plugin-zeroconfaccregwizz,
bundle-pluginmanager"/>
<!--BUNDLE-HISTORY-->
@@ -1587,4 +1588,29 @@ javax.swing.event, javax.swing.border"/>
prefix="resources/images/rss"/>
</jar>
</target>
+
+ <!-- BUNDLE-ZEROCONF -->
+ <target name="bundle-zeroconf">
+ <!-- Creates a bundle containing the Zeroconf impl of the protocol provider.-->
+ <jar compress="false" destfile="${bundles.dest}/protocol-zeroconf.jar"
+ manifest="src/net/java/sip/communicator/impl/protocol/zeroconf/zeroconf.provider.manifest.mf">
+ <zipfileset dir="${dest}/net/java/sip/communicator/impl/protocol/zeroconf"
+ prefix="net/java/sip/communicator/impl/protocol/zeroconf"/>
+ <zipfileset dir="resources/images/zeroconf"
+ prefix="resources/images/zeroconf"/>
+ </jar>
+ </target>
+
+
+ <!-- BUNDLE-PLUGIN-ZEROCONFACCREGWIZZ -->
+ <target name="bundle-plugin-zeroconfaccregwizz">
+ <!-- Creates a bundle for the plugin Zeroconf Account Registration Wizard.-->
+ <jar compress="false" destfile="${bundles.dest}/zeroconfaccregwizz.jar"
+ manifest="src/net/java/sip/communicator/plugin/zeroconfaccregwizz/zeroconfaccregwizz.manifest.mf">
+ <zipfileset dir="${dest}/net/java/sip/communicator/plugin/zeroconfaccregwizz"
+ prefix="net/java/sip/communicator/plugin/zeroconfaccregwizz"/>
+ <zipfileset dir="resources/images/zeroconf"
+ prefix="resources/images/zeroconf"/>
+ </jar>
+ </target>
</project>
diff --git a/resources/images/zeroconf/zeroconf-away.png b/resources/images/zeroconf/zeroconf-away.png
new file mode 100644
index 0000000..2ecf4bc
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-away.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-color-16.png b/resources/images/zeroconf/zeroconf-color-16.png
new file mode 100644
index 0000000..47d6b7d
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-color-16.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-color-64.png b/resources/images/zeroconf/zeroconf-color-64.png
new file mode 100644
index 0000000..6db9d49
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-color-64.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-dnd.png b/resources/images/zeroconf/zeroconf-dnd.png
new file mode 100644
index 0000000..23bd067
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-dnd.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-invisible.png b/resources/images/zeroconf/zeroconf-invisible.png
new file mode 100644
index 0000000..35dd6b4
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-invisible.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-offline.png b/resources/images/zeroconf/zeroconf-offline.png
new file mode 100644
index 0000000..35609ff
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-offline.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf-online.png b/resources/images/zeroconf/zeroconf-online.png
new file mode 100644
index 0000000..3bde6f6
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf-online.png
Binary files differ
diff --git a/resources/images/zeroconf/zeroconf.png b/resources/images/zeroconf/zeroconf.png
new file mode 100644
index 0000000..e7ad8a3
--- /dev/null
+++ b/resources/images/zeroconf/zeroconf.png
Binary files differ
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/BonjourService.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/BonjourService.java
new file mode 100644
index 0000000..56e104b
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/BonjourService.java
@@ -0,0 +1,666 @@
+/*
+ * ServerThread.java
+ *
+ * Created on 17 mars 2007, 21:54
+ *
+ * To change this template, choose Tools | Template Manager
+ * and open the template in the editor.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.impl.protocol.zeroconf.jmdns.*;
+
+/**
+ * Class dealing with JmDNS and treating all the
+ * incoming connections on the bonjour port
+ * @author Christian Vincenot
+ */
+public class BonjourService extends Thread
+ implements ServiceListener,
+ DNSListener
+{
+ private static final Logger logger =
+ Logger.getLogger(BonjourService.class);
+
+ private int port = 5298;
+ private ServerSocket sock=null;
+ private String id;
+ private JmDNS jmdns=null;
+ private Hashtable props;
+ private ServiceInfo service;
+ private boolean dead = false;
+
+ private Vector contacts = new Vector();
+
+ private ProtocolProviderServiceZeroconfImpl pps;
+ OperationSetPersistentPresenceZeroconfImpl opSetPersPresence;
+
+ private ZeroconfAccountID acc;
+
+ /* Should maybe better get the status directly from OperationSetPresence */
+ private PresenceStatus status = ZeroconfStatusEnum.OFFLINE;
+
+ /**
+ * Returns the corresponding ProtocolProviderService
+ * @return corresponding ProtocolProviderService
+ */
+ public ProtocolProviderServiceZeroconfImpl getPPS()
+ {
+ return pps;
+ }
+
+ /**
+ * Returns the id of this service.
+ * @return returns the id of this service.
+ */
+ String getID()
+ {
+ return id;
+ }
+
+ /**
+ * Creates a new instance of the Bonjour service thread
+ * @param port TCP Port number on which to try to start the Bonjour service
+ * @param pps ProtocolProviderService instance
+ * which is creating this BonjourService
+ */
+ public BonjourService(int port,
+ ProtocolProviderServiceZeroconfImpl pps)
+ {
+ this.acc = (ZeroconfAccountID) pps.getAccountID();
+ this.port = port;
+ this.id = acc.getUserID();
+ this.pps = pps;
+
+ opSetPersPresence = (OperationSetPersistentPresenceZeroconfImpl)
+ pps.getSupportedOperationSets()
+ .get(OperationSetPersistentPresence.class.getName());
+
+ props = new Hashtable();
+
+ // Gaim
+ props.put("1st", acc.getFirst());
+ props.put("email", (acc.getMail() == null)? "":acc.getMail());
+ props.put("jid", this.id);
+ props.put("last", (acc.getLast() == null)?"":acc.getLast());
+ props.put("msg", opSetPersPresence.getCurrentStatusMessage());
+ props.put("status", "avail");
+
+ //iChat
+ props.put("phsh","000");
+ //props.put("status","avail");
+ //props.put("port.p2pj", "5298");
+ props.put("vc", "C!");
+ //props.put("1st", "John");
+ props.put("txtvers","1");
+
+ //XEP-0174 (Final paper)
+ props.put("ext","");
+ props.put("nick", acc.getFirst());
+ props.put("ver", "1");
+ props.put("node", "SIP Communicator");
+
+ //Ours
+ props.put("client", "SIP Communicator");
+
+ changeStatus(opSetPersPresence.getPresenceStatus());
+
+ sock = createSocket(port);
+ if (sock == null) return;
+
+ port = sock.getLocalPort();
+
+ logger.debug("ZEROCONF: ServerSocket bound to port "+port);
+
+ props.put("port.p2pj", Integer.toString(port));
+
+ this.start();
+ }
+
+ /* TODO: Better exception checking to avoid sudden exit and bonjour
+ * service shutdown */
+
+ /**
+ * Walk?
+ */
+ public void run()
+ {
+ logger.debug("ZEROCONF: Bonjour Service Thread up and running!");
+
+ /* Put jmDNS in DEBUD Mode :
+ * Following verbosity levels can be chosen :
+ * "INFO" , "WARNING", "SEVERE", "ALL", "FINE", "FINER", "FINEST", etc
+ */
+ //System.setProperty("jmdns.debug", "0");
+
+ while (dead == false)
+ {
+ if (sock == null || sock.isClosed())
+ {
+ sock = createSocket(port);
+ /* What should we do now? TEMPORARY: shutdown()*/
+ if (sock == null) shutdown();
+ port = sock.getLocalPort();
+ props.put("port.p2pj", Integer.toString(port));
+ //TODO: update JmDNS in case the port had to be changed!
+ }
+ try
+ {
+ Socket connexion = sock.accept();
+ ContactZeroconfImpl contact = getContact(null,
+ connexion.getInetAddress());
+ /*if (status.equals(ZeroconfStatusEnum.OFFLINE)
+ || status.equals(ZeroconfStatusEnum.INVISIBLE) */
+ if (dead == true) break;
+
+ if ((contact == null)
+ || (contact.getClientThread() != null))
+ {
+ if (contact == null)
+ logger.error("ZEROCONF: Connexion from "
+ + "unknown contact ["
+ + connexion.getInetAddress()
+ +"]. REJECTING!");
+ else if (contact.getClientThread() == null)
+ logger.error("ZEROCONF: Redundant chat "
+ + "channel ["
+ + contact
+ +"]. REJECTING!");
+ connexion.close();
+ }
+ else new ClientThread(connexion, this);
+ }
+ catch(Exception e)
+ {
+ logger.error(e);
+ }
+ }
+
+ logger.debug("ZEROCONF: Going Offline - "
+ +"BonjourService Thread exiting!");
+ }
+
+ /**
+ * Might be used for shutdown...
+ */
+ public void shutdown()
+ {
+ logger.debug("ZEROCONF: Shutdown!");
+
+ dead = true;
+ try
+ { sock.close(); }
+ catch (Exception ex)
+ { logger.error(ex); }
+
+ changeStatus(ZeroconfStatusEnum.OFFLINE);
+ }
+
+ private ServerSocket createSocket(int port)
+ {
+ ServerSocket sock=null;
+ try
+ {
+ sock = new ServerSocket(port);
+ }
+ catch(Exception e)
+ {
+ logger.error("ZEROCONF: Couldn't bind socket to port "
+ +port+"! Switching to an other port...");
+ try
+ {
+ sock = new ServerSocket(0);
+ }
+ catch (IOException ex)
+ {
+ logger.error("ZEROCONF: FATAL ERROR => "
+ +"Couldn't bind to a port!!", ex);
+ }
+ }
+
+ return sock;
+ }
+
+ /**
+ * Changes the status of the local user.
+ * @param stat New presence status
+ */
+ public void changeStatus(PresenceStatus stat)
+ {
+ /* [old_status == new_status ?] => NOP */
+ if (stat.equals(status)) return;
+
+ /* [new_status == OFFLINE ?] => clean up everything */
+ if (stat.equals(ZeroconfStatusEnum.OFFLINE))
+ {
+ logger.debug("ZEROCONF: Going OFFLINE");
+ //jmdns.unregisterAllServices();
+ jmdns.removeServiceListener("_presence._tcp.local.", this);
+ jmdns.close();
+ jmdns=null;
+ //dead = true;
+
+ // Erase all contacts by putting them OFFLINE
+ opSetPersPresence.changePresenceStatusForAllContacts(
+ opSetPersPresence.getServerStoredContactListRoot(), stat);
+
+ try
+ {
+ sleep(1000);
+ } catch (InterruptedException ex)
+ {
+ logger.error(ex);
+ }
+ }
+
+ /* [old_status == OFFLINE ?] => register service */
+ else if (status.equals(ZeroconfStatusEnum.OFFLINE))
+ {
+ logger.debug("ZEROCONF: Getting out of OFFLINE state");
+ props.put("status", stat.getStatusName());
+ service = new ServiceInfo("_presence._tcp.local.", id,
+ port, 0, 0, props);
+
+ try
+ {
+ jmdns = new JmDNS();
+ jmdns.registerServiceType("_presence._tcp.local.");
+ jmdns.addServiceListener("_presence._tcp.local.", this);
+ jmdns.registerService(service);
+
+ /* In case the ID had to be changed */
+ id = service.getName();
+ }
+ catch (Exception ex)
+ { logger.error(ex); }
+
+ //dead = false;
+
+ /* Normal status change */
+ }
+ else
+ {
+ logger.debug("ZEROCONF : Changing status");
+
+ props.put("status", stat.getStatusName());
+
+ /* FIXME: Not totally race condition free since the 3 calls aren't
+ * atomic, but that's not really critical since there's little
+ * change chance of concurrent local contact change, and this
+ * wouldn't have big consequences.
+ */
+ ServiceInfo info =
+ jmdns.getLocalService(id.toLowerCase()+"._presence._tcp.local.");
+ if (info == null)
+ logger.error("ZEROCONF/JMDNS: PROBLEM GETTING "
+ +"LOCAL SERVICEINFO !!");
+
+ byte[] old = info.getTextBytes();
+ info.setProps(props);
+ jmdns.updateInfos(info, old);
+ }
+
+ status = stat;
+ }
+
+ private class AddThread extends Thread
+ {
+ private String type, name;
+ public AddThread(String type, String name)
+ {
+ this.type = type;
+ this.name = name;
+ this.start();
+ }
+
+ public void run()
+ {
+ ServiceInfo service = null;
+ while ((service == null) && (dead == false)
+ && !status.equals(ZeroconfStatusEnum.OFFLINE))
+ {
+ service = jmdns.getServiceInfo(type, name, 10000);
+ if (service == null)
+ logger.error("BONJOUR: ERROR - Service Info of "
+ + name +" not found in cache!!");
+ try
+ {
+ sleep(2);
+ }
+ catch (InterruptedException ex)
+ {
+ logger.error(ex);
+ }
+ }
+ if ((dead == false) && !status.equals(ZeroconfStatusEnum.OFFLINE))
+ jmdns.requestServiceInfo(type, name);
+ //} else handleResolvedService(name, type, service);
+ }
+ }
+
+ /* Service Listener Implementation */
+
+ /**
+ * A service has been added.
+ *
+ * @param event The ServiceEvent providing the name and fully qualified type
+ * of the service.
+ */
+ public void serviceAdded(ServiceEvent event)
+ {
+ /* WARNING: DONT PUT ANY BLOCKING CALLS OR FLAWED LOOPS IN THIS METHOD.
+ * JmDNS calls this method without creating a new thread, so if this
+ * method doesn't return, jmDNS will hang !!
+ */
+
+ String name = event.getName();
+ String type = event.getType();
+
+ if (name.equals(id)) return;
+
+ logger.debug("BONJOUR: "+name
+ +"["+type+"] detected! Trying to get information...");
+ try
+ {
+ sleep(2);
+ }
+ catch (InterruptedException ex)
+ {
+ logger.error(ex);
+ }
+
+ jmdns.printServices();
+
+ new AddThread(type, name);
+ }
+
+
+
+ /**
+ * A service has been removed.
+ *
+ * @param event The ServiceEvent providing the name and fully qualified type
+ * of the service.
+ */
+ public void serviceRemoved(ServiceEvent event)
+ {
+ String name = event.getName();
+ if (name.equals(id)) return;
+
+ ContactZeroconfImpl contact = getContact(name, null);
+
+ opSetPersPresence.changePresenceStatusForContact(contact,
+ ZeroconfStatusEnum.OFFLINE);
+ logger.debug("BONJOUR: Received announcement that "
+ +name+" went offline!");
+
+ }
+
+ /**
+ * 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.
+ */
+ public void serviceResolved(ServiceEvent event)
+ {
+ String contactID = event.getName();
+ String type = event.getType();
+ ServiceInfo info = event.getInfo();
+
+ logger.debug("BONJOUR: Information about "
+ +contactID+" discovered");
+
+ handleResolvedService(contactID, type, info);
+ }
+
+ private void handleResolvedService(String contactID,
+ String type,
+ ServiceInfo info)
+ {
+ if (contactID.equals(id))
+ return;
+
+ if (info.getAddress().toString().length() > 15)
+ {
+ logger.debug("ZEROCONF: Temporarily ignoring IPv6 addresses!");
+ return;
+ }
+
+ ContactZeroconfImpl newFriend;
+
+ synchronized(this)
+ {
+ if (getContact(contactID, info.getAddress()) != null)
+ {
+ logger.debug("Contact "
+ +contactID+" already in contact list! Skipping.");
+ return;
+ };
+ logger.debug("ZEROCNF: ContactID " + contactID +
+ " Address " + info.getAddress());
+
+ logger.debug(" Address=>"+info.getAddress()
+ +":"+info.getPort());
+
+ for (Enumeration names = info.getPropertyNames() ;
+ names.hasMoreElements() ; )
+ {
+ String prop = (String)names.nextElement();
+ logger.debug(" "+prop+"=>"
+ +info.getPropertyString(prop));
+ }
+
+ /* Creating the contact */
+ String name = info.getPropertyString("1st");
+ if (info.getPropertyString("last") != null)
+ name += " "+ info.getPropertyString("last");
+
+ int port = Integer.valueOf(
+ info.getPropertyString("port.p2pj")).intValue();
+
+ if (port < 1)
+ {
+ logger.error("ZEROCONF: Flawed contact announced himself"
+ +"without necessary parameters : "+contactID);
+ return;
+ }
+
+ logger.debug("ZEROCONF: Detected client "+name);
+
+ newFriend =
+ opSetPersPresence.createVolatileContact(
+ contactID, this, name,
+ info.getAddress(), port);
+ }
+ /* Try to detect which client type it is */
+ int clientType = ContactZeroconfImpl.XMPP;
+ if (info.getPropertyString("client") != null
+ && info.getPropertyString("client").
+ compareToIgnoreCase("SIP Communicator") == 0)
+ clientType = ContactZeroconfImpl.SIPCOM;
+
+ else if ((info.getPropertyString("jid") != null)
+ && (info.getPropertyString("node") == null))
+ clientType = ContactZeroconfImpl.GAIM;
+
+ else if (info.getPropertyString("jid") == null)
+ clientType = ContactZeroconfImpl.ICHAT;
+
+ newFriend.setClientType(clientType);
+ logger.debug("ZEROCONF: CLIENT TYPE "+clientType);
+
+ ZeroconfStatusEnum status =
+ ZeroconfStatusEnum.statusOf(info.getPropertyString("status"));
+ opSetPersPresence.
+ changePresenceStatusForContact(newFriend,
+ status == null?ZeroconfStatusEnum.ONLINE:status);
+
+ // Listening for changes
+ jmdns.addListener(this, new DNSQuestion(info.getQualifiedName(),
+ DNSConstants.TYPE_SRV,
+ DNSConstants.CLASS_UNIQUE));
+ }
+
+ /**
+ * Callback called by JmDNS to inform the
+ * BonjourService of a potential status change of some contacts.
+ * @param jmdns JmDNS instance responsible for this
+ * @param now Timestamp
+ * @param record DNSRecord which changed
+ */
+ public synchronized void updateRecord( JmDNS jmdns,
+ long now,
+ DNSRecord record)
+ {
+ logger.debug("ZEROCONF/JMDNS: Received record update for "+record);
+
+ int clazz = record.getClazz();
+ int type = record.getType();
+
+ /* Check the info returned by JmDNS since we can't really trust its
+ * filtering. */
+ if (!(((type & DNSConstants.TYPE_TXT) != 0) &&
+ ((clazz & DNSConstants.CLASS_IN) != 0) &&
+ record.isUnique() &&
+ record.getName().endsWith("_presence._tcp.local.")))
+ return;
+
+ String name = record.getName().replaceAll("._presence._tcp.local.","");
+ ContactZeroconfImpl contact;
+
+ synchronized(this)
+ {
+ contact = getContact(name, null);
+
+ if (contact == null) { //return;
+ logger.error("ZEROCONF: BUG in jmDNS => Received update without "
+ +"previous contact annoucement. Trying to add contact");
+ new AddThread("_presence._tcp.local.", name);
+ return;
+ }
+ }
+
+ logger.debug("ZEROCONF: "+ name
+ + " changed status. Requesting fresh data!");
+
+ /* Since a record was updated, we can be sure that we can do a blocking
+ * getServiceInfo without risk. (Still, we use the method with timeout
+ * to avoid bad surprises). If some problems of status change refresh
+ * appear, we'll have to fall back on the method with callback as we've
+ * done for "ServiceAdded".
+ */
+
+ ServiceInfo info = jmdns.getServiceInfo("_presence._tcp.local.", name,
+ 1000);
+ if (info == null)
+ {
+ logger.error("ZEROCONF/JMDNS: Problem!! The service "
+ +"information was not in cache. See comment in "
+ +"BonjourService.java:updateRecord !!");
+ return;
+ }
+
+ /* Let's change what we can : status, message, etc */
+ ZeroconfStatusEnum status =
+ ZeroconfStatusEnum.statusOf(info.getPropertyString("status"));
+
+ opSetPersPresence.
+ changePresenceStatusForContact(contact,
+ status == null ? ZeroconfStatusEnum.ONLINE:status);
+
+ }
+
+ /**
+ * Returns an Iterator over all contacts.
+ *
+ * @return a java.util.Iterator over all contacts
+ */
+ public Iterator contacts()
+ {
+ return contacts.iterator();
+ }
+
+ /**
+ * Adds a contact to the locally stored list of contacts
+ * @param contact Zeroconf Contact to add to the local list
+ */
+ public void addContact(ContactZeroconfImpl contact)
+ {
+ synchronized(contacts)
+ {
+ contacts.add(contact);
+ }
+ }
+ /**
+ * Returns the <tt>Contact</tt> with the specified identifier or IP address.
+ *
+ * @param id the identifier of the <tt>Contact</tt> we are
+ * looking for.
+ * @param ip the IP address of the <tt>Contact</tt> we are looking for.
+ * @return the <tt>Contact</tt> with the specified id or address.
+ */
+ public ContactZeroconfImpl getContact(String id, InetAddress ip)
+ {
+ if (id == null && ip == null) return null;
+
+ synchronized(contacts)
+ {
+ Iterator contactsIter = contacts();
+
+ while (contactsIter.hasNext())
+ {
+ ContactZeroconfImpl contact =
+ (ContactZeroconfImpl)contactsIter.next();
+ //System.out.println("ZEROCNF: Comparing "+id+ " "+ip+
+ //" with "+ contact.getAddress()+ " " + contact.getIpAddress());
+ if (((contact.getAddress().equals(id)) || (id == null))
+ && ((contact.getIpAddress().equals(ip)) || (ip == null))
+ && (contact != null))
+ return contact;
+
+ }
+ }
+ //System.out.println("ZEROCNF: ERROR - " +
+ //"Couldn't find contact to get ["+id+" / "+ip+"]");
+ return null;
+ }
+
+ /**
+ * Removes the <tt>Contact</tt> with the specified identifier or IP address.
+ *
+ *
+ * @param id the identifier of the <tt>Contact</tt> we are
+ * looking for.
+ * @param ip the IP address of the <tt>Contact</tt> we are looking for.
+ */
+ public void removeContact(String id, InetAddress ip)
+ {
+ synchronized(contacts)
+ {
+ Iterator contactsIter = contacts();
+ while (contactsIter.hasNext())
+ {
+ ContactZeroconfImpl contact =
+ (ContactZeroconfImpl)contactsIter.next();
+ if (((contact.getAddress().equals(id)) || (id == null))
+ &&((contact.getIpAddress().equals(ip)) || (ip == null)))
+ {
+ if (contact.getClientThread() != null)
+ contact.getClientThread().cleanThread();
+ contacts.remove(contact);
+ return;
+ }
+ };
+ }
+ logger.error(
+ "ZEROCONF: ERROR - Couldn't find contact to delete ["+id+" / "+ip+"]");
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ClientThread.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ClientThread.java
new file mode 100644
index 0000000..d33ad0f
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ClientThread.java
@@ -0,0 +1,480 @@
+/*
+ * ClientThread.java
+ *
+ * Created on 17 mars 2007, 22:04
+ *
+ * @author: Christian Vincenot
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Class creating a thread responsible for handling the chat
+ * with the remote user on the other end of the socket
+ *
+ * @author Christian Vincenot
+ */
+public class ClientThread
+ extends Thread
+{
+ private static final Logger logger = Logger.getLogger(BonjourService.class);
+
+ private OperationSetBasicInstantMessagingZeroconfImpl opSetBasicIM;
+ private OperationSetTypingNotificationsZeroconfImpl opSetTyping;
+ private Socket sock;
+ private InetAddress remoteIPAddress;
+ private OutputStream out;
+ private DataInputStream in;
+ private BonjourService bonjourService;
+ private ContactZeroconfImpl contact=null;
+ private boolean streamState = false;
+
+ private String messagesQueue=null;
+
+ /**
+ * Sets the contact with which we're chatting in this ClientThread
+ * @param contact Zeroconf contact with which we're chatting
+ */
+ protected void setContact(ContactZeroconfImpl contact)
+ {
+ this.contact = contact;
+ }
+
+ /**
+ * Set the stream as opened. This means that the
+ * conversation with the client is really opened
+ * from now on (the XML greetings are over)
+ */
+ protected void setStreamOpen()
+ {
+ synchronized(this)
+ {
+ this.streamState = true;
+ }
+ }
+
+ /**
+ * Says if the stream between the local user and the remote user
+ * is in an opened state (greetings are over and we can chat)
+ * @return Returns true if the stream is "opened" (ie, ready for chat)
+ */
+ protected boolean isStreamOpened()
+ {
+ synchronized(this)
+ {
+ return this.streamState;
+ }
+ }
+
+
+ /**
+ * Creates a new instance of ClientThread reponsible
+ * for handling the conversation with the remote user.
+ * @param sock Socket created for chatting
+ * @param bonjourService BonjourService which spawned this ClientThread
+ */
+ public ClientThread(Socket sock, BonjourService bonjourService)
+ {
+ this.sock = sock;
+ this.remoteIPAddress = sock.getInetAddress();
+ this.bonjourService = bonjourService;
+ this.opSetBasicIM = (OperationSetBasicInstantMessagingZeroconfImpl)
+
+ bonjourService.getPPS().getSupportedOperationSets()
+ .get(OperationSetBasicInstantMessaging.class.getName());
+
+ this.opSetTyping = (OperationSetTypingNotificationsZeroconfImpl)
+ bonjourService.getPPS().getSupportedOperationSets()
+ .get(OperationSetTypingNotifications.class.getName());
+ try
+ {
+ out = sock.getOutputStream();
+ in = new DataInputStream(sock.getInputStream());
+ }
+ catch (IOException e)
+ {
+ logger.error("Creating ClientThread: Couldn't get I/O for "
+ +"the connection", e);
+ //System.exit(1);
+ return;
+ }
+
+ this.start();
+ }
+
+ /*
+ * Read a message from the socket.
+ * TODO: clean the code a bit and optimize it.
+ */
+ private String readMessage()
+ {
+ String line;
+ byte[] bytes = new byte[10];
+
+ try
+ {
+ int i=0;
+
+ while (i < 9)
+ {
+ i += in.read(bytes,0,9-i);
+ }
+
+ line = new String(bytes);
+ bytes = new byte[1];
+ if ((line.getBytes())[0] == '\n')
+ line = line.substring(1);
+
+ if (line.startsWith("<message"))
+ {
+ while (true)
+ {
+ bytes[0] = in.readByte();
+ line += new String(bytes);
+
+ if ((line.endsWith("</message>"))
+ || (line.endsWith("stream>")))
+ return line;
+ }
+ }
+ else
+ {
+
+ while (true)
+ {
+ bytes[0] = in.readByte();
+ line += new String(bytes);
+ if ( ">".compareTo(new String(bytes)) == 0 )
+ return line;
+ }
+ }
+ }
+ catch (IOException e)
+ {
+ logger.error("Couldn't get I/O for the connection", e);
+ //System.exit(1);
+ }
+
+ return null;
+ }
+
+ /*
+ * Parse the payload and extract the information.
+ * TODO: If needed, fill in the remaining fields of MessageZeroconfImpl
+ * like the baloon icon color, color/size/font of the text.
+ */
+ private MessageZeroconfImpl parseMessage(String str)
+ {
+ //System.out.println("MESSAGE : "+str+" !!!!!!");
+ if (str.startsWith("<?xml") || str.startsWith("<stream"))
+ return new MessageZeroconfImpl
+ (null, null, MessageZeroconfImpl.STREAM_OPEN);
+
+ if (str.endsWith("stream>"))
+ return new MessageZeroconfImpl
+ (null, null, MessageZeroconfImpl.STREAM_CLOSE);
+
+ if ((str.indexOf("<delivered/>") > 0) && (str.indexOf("<body>") < 0))
+ return new MessageZeroconfImpl
+ (null, null, MessageZeroconfImpl.DELIVERED);
+
+ if (!str.startsWith("<message"))
+ return new MessageZeroconfImpl
+ (null, null, MessageZeroconfImpl.UNDEF);
+
+ /* TODO: Parse Enconding (& contact id to be able to double-check
+ * the source of a message)
+ *
+ * TODO: Check that the fields are outside of <body>..</body>
+ */
+
+ if ((str.indexOf("<body>") < 0) || (str.indexOf("</body>") < 0))
+ return new MessageZeroconfImpl
+ (null, null, MessageZeroconfImpl.UNDEF);
+
+ String temp =
+ str.substring(str.indexOf("<body>")+6, str.indexOf("</body>"));
+
+ logger.debug("ZEROCONF: received message ["+temp+"]");
+
+ int MessageType = MessageZeroconfImpl.MESSAGE;
+
+ if ((str.indexOf("<id>") >= 0) && (str.indexOf("</id>") >= 0))
+ MessageType = MessageZeroconfImpl.TYPING;
+
+ MessageZeroconfImpl msg =
+ new MessageZeroconfImpl(temp, null, MessageType);
+
+ return msg;
+ }
+
+ private int handleMessage(MessageZeroconfImpl msg)
+ {
+
+ switch(msg.getType())
+ {
+ /* STREAM INIT */
+ case MessageZeroconfImpl.STREAM_OPEN:
+ if (contact == null)
+ contact = bonjourService.getContact(null, remoteIPAddress);
+ if (!isStreamOpened())
+ {
+ sendHello();
+ setStreamOpen();
+ }
+ if (messagesQueue != null)
+ {
+ write(messagesQueue);
+ messagesQueue = null;
+ }
+ break;
+
+ /* ACK */
+ case MessageZeroconfImpl.DELIVERED : break;
+
+ /* NORMAL MESSAGE */
+ case MessageZeroconfImpl.MESSAGE:
+ if (!isStreamOpened())
+ logger.debug("ZEROCONF: client on the other side "
+ +"isn't polite (sending messages without "
+ +"saying hello :P");
+ if (contact == null)
+ //TODO: Parse contact id to double-check
+ contact = bonjourService.getContact(null, remoteIPAddress);
+
+ /* TODO: If we want to implement invisible status, we'll have to
+ * make this test less restrictive to handle messages from
+ * unannounced clients.
+ */
+ if (contact == null)
+ {
+ logger.error("ZEROCONF: ERROR - Couldn't identify "
+ +"contact. Closing socket.");
+ return -1;
+ }
+ else if (contact.getClientThread() == null)
+ contact.setClientThread(this);
+
+ MessageReceivedEvent msgReceivedEvt =
+ new MessageReceivedEvent(msg, (Contact)contact, new Date());
+ opSetBasicIM.fireMessageReceived(msg, contact);
+
+ opSetTyping.
+ fireTypingNotificationsEvent((Contact)contact,
+ opSetTyping.STATE_STOPPED);
+ break;
+
+ case MessageZeroconfImpl.TYPING:
+ if (!isStreamOpened())
+ logger.debug("ZEROCONF: client on the other side "
+ +"isn't polite (sending messages without "
+ +"saying hello :P");
+ if (contact == null)
+ //TODO: Parse contact id to double-check
+ contact = bonjourService.getContact(null, remoteIPAddress);
+ opSetTyping.
+ fireTypingNotificationsEvent((Contact)contact,
+ opSetTyping.STATE_TYPING);
+
+ /* TODO: code a private runnable class to be used as timeout
+ * to set the typing state to STATE_PAUSED when a few seconds
+ * without news have passed.
+ */
+
+ break;
+
+ case MessageZeroconfImpl.STREAM_CLOSE:
+ sendBye();
+ contact.setClientThread(null);
+ return 1;
+
+ case MessageZeroconfImpl.UNDEF:
+ logger.error("ZEROCONF: received strange message. SKIPPING!");
+ break;
+ }
+
+ //System.out.println("RECEIVED MESSAGE "+ msg.getContent()+
+ //" from "+contact.getAddress() + "!!!!!!!!!!!!!!");
+ return 0;
+ }
+
+
+ private void write(String string)
+ {
+ //System.out.println("Writing " + string + "!!!!!!!!!");
+ byte[] bytes = string.getBytes();
+ try
+ {
+ out.write(bytes);
+ out.flush();
+ }
+ catch (IOException e)
+ {
+ logger.error("Couldn't get I/O for the connection");
+ if (contact != null) contact.setClientThread(null);
+ try
+ {
+ sock.close();
+ }
+ catch (IOException ex)
+ {
+ logger.error(ex);
+ }
+
+ }
+ }
+
+ /**
+ * Say hello :)
+ */
+ protected void sendHello()
+ {
+ switch(contact.getClientType())
+ {
+ case ContactZeroconfImpl.GAIM:
+ case ContactZeroconfImpl.ICHAT:
+ case ContactZeroconfImpl.SIPCOM:
+ write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+ write("<stream:stream xmlns=\"jabber:client\" "
+ +"xmlns:stream=\"http://etherx.jabber.org/streams\">");
+ break;
+ case ContactZeroconfImpl.XMPP:
+ write("<stream:stream"
+ +"xmlns='jabber:client'"
+ +"xmlns:stream='http://etherx.jabber.org/streams'"
+ +"from='"+bonjourService.getID()+"'"
+ +"to='"+contact.getAddress()+"'"
+ +"version='1.0'>\n");
+ break;
+ }
+
+ /* Legacy: OLD XMPP (XEP-0174 Draft) */
+ //write("<stream:stream to='"+sock.getInetAddress().getHostAddress()
+ //+"' xmlns='jabber:client' stream='http://etherx.jabber.org/streams'>");
+ }
+
+ private void sendBye()
+ {
+ write("</stream:stream>\n");
+ }
+
+ private String toXHTML(MessageZeroconfImpl msg)
+ {
+ switch(contact.getClientType())
+ {
+ case ContactZeroconfImpl.XMPP:
+ return new String("<message to='"
+ +contact.getAddress()+"' from='"
+ +bonjourService.getID()+"'>"
+ + "<body>"+msg.getContent()+"</body>"
+ + "</message>\n");
+
+ case ContactZeroconfImpl.SIPCOM:
+
+ case ContactZeroconfImpl.ICHAT:
+ return new String(
+ "<message to='"+sock.getInetAddress().getHostAddress()
+ +"' type='chat' id='"+bonjourService.getID()+"'>"
+ + "<body>"+msg.getContent()+"</body>"
+ + "<html xmlns='http://www.w3.org/1999/xhtml'>"
+ + "<body ichatballooncolor='#7BB5EE' "
+ + "ichattextcolor='#000000'>"
+ + "<font face='Helvetica' ABSZ='12' color='#000000'>"
+ + msg.getContent()
+ + "</font>"
+ + "</body>"
+ + "</html>"
+ + "<x xmlns='jabber:x:event'>"
+ + "<offline/>"
+ + "<delivered/>"
+ + "<composing/>"
+ + (msg.getType()==MessageZeroconfImpl.TYPING?"<id></id>":"")
+ + "</x>"
+ + "</message>");
+
+ case ContactZeroconfImpl.GAIM:
+ default:
+ return new String(
+ "<message to='"+contact.getAddress()
+ +"' from='"+bonjourService.getID()
+ + "' type='chat'><body>"+msg.getContent()+"</body>"
+ + "<html xmlns='http://www.w3.org/1999/xhtml'><body><font>"
+ + msg.getContent()
+ + "</font></body></html><x xmlns='jabber:x:event'><composing/>"
+ + (msg.getType()==MessageZeroconfImpl.TYPING?"<id></id>":"")
+ + "</x></message>\n");
+ }
+ }
+
+
+ /**
+ * Send a message to the remote user
+ * @param msg Message to send
+ */
+ public void sendMessage(MessageZeroconfImpl msg)
+ {
+ logger.debug("ZEROCONF: Sending messag ["
+ +msg.getContent()+"] to "
+ + contact.getDisplayName());
+ if (!isStreamOpened())
+ {
+ logger.debug("ZEROCONF: Stream not opened... "
+ +"will send the message later");
+ messagesQueue += toXHTML(msg);
+ }
+ else write(toXHTML(msg));
+ }
+
+ /**
+ * Walk?
+ */
+ public void run()
+ {
+ logger.debug("Bonjour: NEW CONNEXION from "
+ + sock.getInetAddress().getCanonicalHostName()
+ +" / "+sock.getInetAddress().getHostAddress());
+ String input;
+ MessageZeroconfImpl msg=null;
+
+
+ input = readMessage();
+ msg = parseMessage(input);
+ //System.out.println("echo ["+msg.getContent()+"] :" + input);
+
+ while (handleMessage(msg) == 0 && !sock.isClosed())
+ {
+ input = readMessage();
+ msg = parseMessage(input);
+ //System.out.println("echo ["+msg.getContent()+"] :" + input);
+ }
+
+ logger.debug("ZEROCONF : OUT OF LOOP !! Closed chat.");
+ cleanThread();
+ }
+
+ /**
+ * Clean-up the thread to exit
+ */
+ public void cleanThread()
+ {
+ /* I wonder if that's ok... */
+ if (sock != null && sock.isClosed() == false)
+ {
+ sendBye();
+ try
+ {
+ sock.close();
+ }
+ catch (IOException ex)
+ {
+ logger.error(ex);
+ }
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactGroupZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactGroupZeroconfImpl.java
new file mode 100644
index 0000000..1efc311
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactGroupZeroconfImpl.java
@@ -0,0 +1,597 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * A simple, straightforward implementation of a zeroconf ContactGroup. Since
+ * the Zeroconf protocol, we simply store all group details
+ * in class fields. You should know that when implementing a real protocol,
+ * the contact group implementation would rather encapsulate group objects from
+ * the protocol stack and group property values should be returned by consulting
+ * the encapsulated object.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ * @author Jonathan Martin
+ */
+public class ContactGroupZeroconfImpl
+ implements ContactGroup
+{
+ private static final Logger logger
+ = Logger.getLogger(ContactGroupZeroconfImpl.class);
+
+ /**
+ * The name of this Zeroconf contact group.
+ */
+ private String groupName = null;
+
+ /**
+ * The list of this group's members.
+ */
+ private Vector contacts = new Vector();
+
+ /**
+ * The list of sub groups belonging to this group.
+ */
+ private Vector subGroups = new Vector();
+
+ /**
+ * The group that this group belongs to (or null if this is the root group).
+ */
+ private ContactGroupZeroconfImpl parentGroup = null;
+
+ /**
+ * Determines whether this group is really in the contact list or whether
+ * it is here only temporarily and will be gone next time we restart.
+ */
+ private boolean isPersistent = false;
+
+ /**
+ * The protocol provider that created us.
+ */
+ private ProtocolProviderServiceZeroconfImpl parentProvider = null;
+
+ /**
+ * Determines whether this group has been resolved on the server.
+ * Unresolved groups are groups that were available on previous runs and
+ * that the meta contact list has stored. During all next runs, when
+ * bootstrapping, the meta contact list would create these groups as
+ * unresolved. Once a protocol provider implementation confirms that the
+ * groups are still on the server, it would issue an event indicating that
+ * the groups are now resolved.
+ */
+ private boolean isResolved = true;
+
+ /**
+ * An id uniquely identifying the group. For many protocols this could be
+ * the group name itself.
+ */
+ private String uid = null;
+ private static final String UID_SUFFIX = ".uid";
+
+ /**
+ * Creates a ContactGroupZeroconfImpl with the specified name.
+ *
+ * @param groupName the name of the group.
+ * @param parentProvider the protocol provider that created this group.
+ */
+ public ContactGroupZeroconfImpl(
+ String groupName,
+ ProtocolProviderServiceZeroconfImpl parentProvider)
+ {
+ this.groupName = groupName;
+ this.uid = groupName + UID_SUFFIX;
+ this.parentProvider = parentProvider;
+ }
+
+ /**
+ * Determines whether the group may contain subgroups or not.
+ *
+ * @return always true in this implementation.
+ */
+ public boolean canContainSubgroups()
+ {
+ return true;
+ }
+
+ /**
+ * Returns the protocol provider that this group belongs to.
+ * @return a regerence to the ProtocolProviderService instance that this
+ * ContactGroup belongs to.
+ */
+ public ProtocolProviderService getProtocolProvider()
+ {
+ return parentProvider;
+ }
+
+ /**
+ * Returns an Iterator over all contacts, member of this
+ * <tt>ContactGroup</tt>.
+ *
+ * @return a java.util.Iterator over all contacts inside this
+ * <tt>ContactGroup</tt>
+ */
+ public Iterator contacts()
+ {
+ return contacts.iterator();
+ }
+
+ /**
+ * Adds the specified contact to this group.
+ * @param contactToAdd the ContactZeroconfImpl to add to this group.
+ */
+ public void addContact(ContactZeroconfImpl contactToAdd)
+ {
+ this.contacts.add(contactToAdd);
+ contactToAdd.setParentGroup(this);
+ }
+
+ /**
+ * Returns the number of <tt>Contact</tt> members of this
+ * <tt>ContactGroup</tt>
+ *
+ * @return an int indicating the number of <tt>Contact</tt>s, members of
+ * this <tt>ContactGroup</tt>.
+ */
+ public int countContacts()
+ {
+ return contacts.size();
+ }
+
+ /**
+ * Returns the number of subgroups contained by this
+ * <tt>ContactGroup</tt>.
+ *
+ * @return the number of subGroups currently added to this group.
+ */
+ public int countSubgroups()
+ {
+ return subGroups.size();
+ }
+
+ /**
+ * Adds the specified contact group to the contained by this group.
+ * @param subgroup the ContactGroupZeroconfImpl to add as a
+ * subgroup to this group.
+ */
+ public void addSubgroup(ContactGroupZeroconfImpl subgroup)
+ {
+ this.subGroups.add(subgroup);
+ subgroup.setParentGroup(this);
+ }
+
+ /**
+ * Sets the group that is the new parent of this group
+ * @param parent ContactGroupZeroconfImpl
+ */
+ void setParentGroup(ContactGroupZeroconfImpl parent)
+ {
+ this.parentGroup = parent;
+ }
+
+ /**
+ * Returns the contact group that currently contains this group or null if
+ * this is the root contact group.
+ * @return the contact group that currently contains this group or null if
+ * this is the root contact group.
+ */
+ public ContactGroup getParentContactGroup()
+ {
+ return this.parentGroup;
+ }
+
+ /**
+ * Removes the specified contact group from the this group's subgroups.
+ * @param subgroup the ContactGroupZeroconfImpl subgroup to remove.
+ */
+ public void removeSubGroup(ContactGroupZeroconfImpl subgroup)
+ {
+ this.subGroups.remove(subgroup);
+ subgroup.setParentGroup(null);
+ }
+
+
+ /**
+ * Returns the <tt>Contact</tt> with the specified index.
+ *
+ * @param index the index of the <tt>Contact</tt> to return.
+ * @return the <tt>Contact</tt> with the specified index.
+ */
+ public Contact getContact(int index)
+ {
+ return (ContactZeroconfImpl)contacts.get(index);
+ }
+
+ /**
+ * Returns the group that is parent of the specified zeroconfGroup or null
+ * if no parent was found.
+ * @param zeroconfGroup the group whose parent we're looking for.
+ * @return the ContactGroupZeroconfImpl instance that zeroconfGroup
+ * belongs to or null if no parent was found.
+ */
+ public ContactGroupZeroconfImpl findGroupParent(
+ ContactGroupZeroconfImpl zeroconfGroup)
+ {
+ if ( subGroups.contains(zeroconfGroup) )
+ return this;
+
+ Iterator subGroupsIter = subgroups();
+ while (subGroupsIter.hasNext())
+ {
+ ContactGroupZeroconfImpl subgroup
+ = (ContactGroupZeroconfImpl) subGroupsIter.next();
+
+ ContactGroupZeroconfImpl parent
+ = subgroup.findGroupParent(zeroconfGroup);
+
+ if(parent != null)
+ return parent;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the group that is parent of the specified zeroconfContact or
+ * null if no parent was found.
+ *
+ * @param zeroconfContact the contact whose parent we're looking for.
+ * @return the ContactGroupZeroconfImpl instance that zeroconfContact
+ * belongs to or <tt>null</tt> if no parent was found.
+ */
+ public ContactGroupZeroconfImpl findContactParent(
+ ContactZeroconfImpl zeroconfContact)
+ {
+ if ( contacts.contains(zeroconfContact) )
+ return this;
+
+ Iterator subGroupsIter = subgroups();
+ while (subGroupsIter.hasNext())
+ {
+ ContactGroupZeroconfImpl subgroup
+ = (ContactGroupZeroconfImpl) subGroupsIter.next();
+
+ ContactGroupZeroconfImpl parent
+ = subgroup.findContactParent(zeroconfContact);
+
+ if(parent != null)
+ return parent;
+ }
+ return null;
+ }
+
+
+
+ /**
+ * Returns the <tt>Contact</tt> with the specified address or identifier.
+ *
+ * @param id the addres or identifier of the <tt>Contact</tt> we are
+ * looking for.
+ * @return the <tt>Contact</tt> with the specified id or address.
+ */
+ public Contact getContact(String id)
+ {
+ Iterator contactsIter = contacts();
+ while (contactsIter.hasNext())
+ {
+ ContactZeroconfImpl contact =
+ (ContactZeroconfImpl)contactsIter.next();
+
+ if (contact.getAddress().equals(id))
+ return contact;
+
+ }
+ return null;
+ }
+
+ /**
+ * Returns the subgroup with the specified index.
+ *
+ * @param index the index of the <tt>ContactGroup</tt> to retrieve.
+ * @return the <tt>ContactGroup</tt> with the specified index.
+ */
+ public ContactGroup getGroup(int index)
+ {
+ return (ContactGroup)subGroups.get(index);
+ }
+
+ /**
+ * Returns the subgroup with the specified name.
+ *
+ * @param groupName the name of the <tt>ContactGroup</tt> to retrieve.
+ * @return the <tt>ContactGroup</tt> with the specified index.
+ */
+ public ContactGroup getGroup(String groupName)
+ {
+ Iterator groupsIter = subgroups();
+ while (groupsIter.hasNext())
+ {
+ ContactGroupZeroconfImpl contactGroup
+ = (ContactGroupZeroconfImpl) groupsIter.next();
+ if (contactGroup.getGroupName().equals(groupName))
+ return contactGroup;
+
+ }
+ return null;
+
+ }
+
+ /**
+ * Returns the name of this group.
+ *
+ * @return a String containing the name of this group.
+ */
+ public String getGroupName()
+ {
+ return this.groupName;
+ }
+
+ /**
+ * Sets this group a new name.
+ * @param newGrpName a String containing the new name of this group.
+ */
+ public void setGroupName(String newGrpName)
+ {
+ this.groupName = newGrpName;
+ }
+
+ /**
+ * Returns an iterator over the sub groups that this
+ * <tt>ContactGroup</tt> contains.
+ *
+ * @return a java.util.Iterator over the <tt>ContactGroup</tt> children
+ * of this group (i.e. subgroups).
+ */
+ public Iterator subgroups()
+ {
+ return subGroups.iterator();
+ }
+
+ /**
+ * Removes the specified contact from this group.
+ * @param contact the ContactZeroconfImpl to remove from this group
+ */
+ public void removeContact(ContactZeroconfImpl contact)
+ {
+ this.contacts.remove(contact);
+ }
+
+ /**
+ * Returns the contact with the specified id or null if no such contact
+ * exists.
+ * @param id the id of the contact we're looking for.
+ * @return ContactZeroconfImpl
+ */
+ public ContactZeroconfImpl findContactByID(String id)
+ {
+ //first go through the contacts that are direct children.
+ Iterator contactsIter = contacts();
+
+ while(contactsIter.hasNext())
+ {
+ ContactZeroconfImpl mContact =
+ (ContactZeroconfImpl)contactsIter.next();
+
+ if( mContact.getAddress().equals(id) )
+ return mContact;
+ }
+
+ //if we didn't find it here, let's try in the subougroups
+ Iterator groupsIter = subgroups();
+
+ while( groupsIter.hasNext() )
+ {
+ ContactGroupZeroconfImpl mGroup =
+ (ContactGroupZeroconfImpl)groupsIter.next();
+
+ ContactZeroconfImpl mContact = mGroup.findContactByID(id);
+
+ if (mContact != null)
+ return mContact;
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns a String representation of this group and the contacts it
+ * contains (may turn out to be a relatively long string).
+ * @return a String representing this group and its child contacts.
+ */
+ public String toString()
+ {
+
+ StringBuffer buff = new StringBuffer(getGroupName());
+ buff.append(".subGroups=" + countSubgroups() + ":\n");
+
+ Iterator subGroups = subgroups();
+ while (subGroups.hasNext())
+ {
+ ContactGroupZeroconfImpl group =
+ (ContactGroupZeroconfImpl)subGroups.next();
+ buff.append(group.toString());
+ if (subGroups.hasNext())
+ buff.append("\n");
+ }
+
+ buff.append("\nChildContacts="+countContacts()+":[");
+
+ Iterator contacts = contacts();
+ while (contacts.hasNext())
+ {
+ ContactZeroconfImpl contact = (ContactZeroconfImpl) contacts.next();
+ buff.append(contact.toString());
+ if(contacts.hasNext())
+ buff.append(", ");
+ }
+ return buff.append("]").toString();
+ }
+
+ /**
+ * Specifies whether or not this contact group is being stored by the server.
+ * Non persistent contact groups are common in the case of simple,
+ * non-persistent presence operation sets. They could however also be seen
+ * in persistent presence operation sets when for example we have received
+ * an event from someone not on our contact list and the contact that we
+ * associated with that user is placed in a non persistent group. Non
+ * persistent contact groups are volatile even when coming from a persistent
+ * presence op. set. They would only exist until the application is closed
+ * and will not be there next time it is loaded.
+ *
+ * @param isPersistent true if the contact group is to be persistent and
+ * false otherwise.
+ */
+ public void setPersistent(boolean isPersistent)
+ {
+ this.isPersistent = isPersistent;
+ }
+
+ /**
+ * Determines whether or not this contact group is being stored by the
+ * server. Non persistent contact groups exist for the sole purpose of
+ * containing non persistent contacts.
+ * @return true if the contact group is persistent and false otherwise.
+ */
+ public boolean isPersistent()
+ {
+ return isPersistent;
+ }
+
+ /**
+ * Returns null as no persistent data is required and the contact address is
+ * sufficient for restoring the contact.
+ * <p>
+ * @return null as no such data is needed.
+ */
+ public String getPersistentData()
+ {
+ return null;
+ }
+
+ /**
+ * Determines whether or not this contact has been resolved against the
+ * server. Unresolved contacts are used when initially loading a contact
+ * list that has been stored in a local file until the presence operation
+ * set has managed to retrieve all the contact list from the server and has
+ * properly mapped contacts to their on-line buddies.
+ * @return true if the contact has been resolved (mapped against a buddy)
+ * and false otherwise.
+ */
+ public boolean isResolved()
+ {
+ return isResolved;
+ }
+
+ /**
+ * Makes the group resolved or unresolved.
+ *
+ * @param resolved true to make the group resolved; false to
+ * make it unresolved
+ */
+ public void setResolved(boolean resolved)
+ {
+ this.isResolved = resolved;
+ }
+
+ /**
+ * Returns a <tt>String</tt> that uniquely represnets the group inside
+ * the current protocol. The string MUST be persistent (it must not change
+ * across connections or runs of the application). In many cases (Jabber,
+ * ICQ) the string may match the name of the group as these protocols
+ * only allow a single level of contact groups and there is no danger of
+ * having the same name twice in the same contact list. Other protocols
+ * (no examples come to mind but that doesn't bother me ;) ) may be
+ * supporting mutilple levels of grooups so it might be possible for group
+ * A and group B to both contain groups named C. In such cases the
+ * implementation must find a way to return a unique identifier in this
+ * method and this UID should never change for a given group.
+ *
+ * @return a String representing this group in a unique and persistent
+ * way.
+ */
+ public String getUID()
+ {
+ return uid;
+ }
+
+ /**
+ * Ugly but tricky conversion method.
+ * @param uid the uid we'd like to get a name from
+ * @return the name of the group with the specified <tt>uid</tt>.
+ */
+ static String createNameFromUID(String uid)
+ {
+ return uid.substring(0, uid.length() - (UID_SUFFIX.length()));
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one which in terms
+ * of contact groups translates to having the equal names and matching
+ * subgroups and child contacts. The resolved status of contactgroups and
+ * contacts is deliberately ignored so that groups and/or contacts would
+ * be assumed equal even if it differs.
+ * <p>
+ * @param obj the reference object with which to compare.
+ * @return <code>true</code> if this contact group has the equal child
+ * contacts and subgroups to those of the <code>obj</code> argument.
+ */
+ public boolean equals(Object obj)
+ {
+ if(obj == null
+ || !(obj instanceof ContactGroupZeroconfImpl))
+ return false;
+
+ ContactGroupZeroconfImpl zeroconfGroup
+ = (ContactGroupZeroconfImpl)obj;
+
+ if( ! zeroconfGroup.getGroupName().equals(getGroupName())
+ || ! zeroconfGroup.getUID().equals(getUID())
+ || zeroconfGroup.countContacts() != countContacts()
+ || zeroconfGroup.countSubgroups() != countSubgroups())
+ return false;
+
+ //traverse child contacts
+ Iterator theirContacts = zeroconfGroup.contacts();
+
+ while(theirContacts.hasNext())
+ {
+ ContactZeroconfImpl theirContact
+ = (ContactZeroconfImpl)theirContacts.next();
+
+ ContactZeroconfImpl ourContact
+ = (ContactZeroconfImpl)getContact(theirContact.getAddress());
+
+ if(ourContact == null
+ || !ourContact.equals(theirContact))
+ return false;
+ }
+
+ //traverse subgroups
+ Iterator theirSubgroups = zeroconfGroup.subgroups();
+
+ while(theirSubgroups.hasNext())
+ {
+ ContactGroupZeroconfImpl theirSubgroup
+ = (ContactGroupZeroconfImpl)theirSubgroups.next();
+
+ ContactGroupZeroconfImpl ourSubgroup
+ = (ContactGroupZeroconfImpl)getGroup(
+ theirSubgroup.getGroupName());
+
+ if(ourSubgroup == null
+ || !ourSubgroup.equals(theirSubgroup))
+ return false;
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactZeroconfImpl.java
new file mode 100644
index 0000000..df1854f
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ContactZeroconfImpl.java
@@ -0,0 +1,467 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.net.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * A simple, straightforward implementation of a zeroconf Contact. Since
+ * the Zeroconf protocol is not a real one, we simply store all contact details
+ * in class fields. You should know that when implementing a real protocol,
+ * the contact implementation would rather encapsulate contact objects from
+ * the protocol stack and group property values should be returned after
+ * consulting the encapsulated object.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ * @author Jonathan Martin
+ */
+public class ContactZeroconfImpl
+ implements Contact
+{
+ private static final Logger logger
+ = Logger.getLogger(ContactZeroconfImpl.class);
+
+
+ /**
+ * The id of the contact.
+ */
+ private String contactID = null;
+
+ /**
+ * The ClientThread attached to this contact if we're already chatting
+ * with him.
+ */
+ private ClientThread thread = null;
+
+ /*
+ * Type of Client.
+ */
+ /**
+ * Gaim/Pidgin client type
+ */
+ public static final int GAIM = 1;
+ /**
+ * iChat client type
+ */
+ public static final int ICHAT = 2;
+ /**
+ * XMPP - XEP-0174 client type
+ */
+ public static final int XMPP = 3;
+ /**
+ * Another SIP Communicator client
+ */
+ public static final int SIPCOM = 4;
+ private int clientType = XMPP;
+
+
+ /**
+ * The provider that created us.
+ */
+ private ProtocolProviderServiceZeroconfImpl parentProvider = null;
+
+
+ /*
+ * The Bonjour Service who discovered this contact.
+ * TODO: This could probably be avoided using only the
+ * Protocol Provider.
+ */
+ private BonjourService bonjourService;
+
+ /**
+ * The group that belong to.
+ */
+ private ContactGroupZeroconfImpl parentGroup = null;
+
+ /**
+ * The presence status of the contact.
+ */
+ private PresenceStatus presenceStatus = ZeroconfStatusEnum.OFFLINE;
+
+ /**
+ * Determines whether this contact is persistent,
+ * i.e. member of the contact list or whether it is here only temporarily.
+ * Chris: should be set to false here
+ */
+ private boolean isPersistent = false;
+
+ /**
+ * Determines whether the contact has been resolved (i.e. we have a
+ * confirmation that it is still on the server contact list).
+ */
+ private boolean isResolved = true;
+
+ /**
+ * IP Address
+ */
+ private InetAddress ipAddress;
+
+ /**
+ * Port on which Bonjour is listening.
+ */
+ private int port;
+
+ /**
+ * Name announced by Bonjour.
+ */
+ private String name;
+
+ /**
+ * Contact personal message
+ */
+ private String message;
+
+
+ /**
+ * Creates an instance of a meta contact with the specified string used
+ * as a name and identifier.
+ * @param bonjourId ID of the contact
+ * @param bonjourService BonjourService responsible for handling chat with
+ * this contact
+ * @param name Display name of this contact
+ * @param ipAddress IP address of this contact
+ * @param port Port declared by this contact for direct point-to-point chat
+ * @param parentProvider the provider that created us.
+ */
+ public ContactZeroconfImpl(
+ String bonjourId,
+ ProtocolProviderServiceZeroconfImpl parentProvider,
+ BonjourService bonjourService,
+ String name,
+ InetAddress ipAddress,
+ int port)
+ {
+ this.contactID = bonjourId;
+ this.parentProvider = parentProvider;
+ this.bonjourService = bonjourService;
+ this.name = name;
+ this.ipAddress = ipAddress;
+ this.port = port;
+ bonjourService.addContact(this);
+ }
+
+ /**
+ * This method is only called when the contact is added to a new
+ * <tt>ContactGroupZeroconfImpl</tt> by the
+ * <tt>ContactGroupZeroconfImpl</tt> itself.
+ *
+ * @param newParentGroup the <tt>ContactGroupZeroconfImpl</tt> that is now
+ * parent of this <tt>ContactZeroconfImpl</tt>
+ */
+ void setParentGroup(ContactGroupZeroconfImpl newParentGroup)
+ {
+ this.parentGroup = newParentGroup;
+ }
+
+ /**
+ * Return the BonjourService
+ * @return BonjourService
+ */
+ public BonjourService getBonjourService()
+ {
+ return bonjourService;
+ }
+
+ /**
+ * Return the ClientThread responsible for handling with this contact
+ * @return ClientThread corresponding to the chat with this contact or null
+ * if no chat was started
+ */
+ protected ClientThread getClientThread()
+ {
+ return thread;
+ }
+
+ /**
+ * Set the ClientThread responsible for handling with this contact
+ * @param thread ClientThread corresponding to the chat with this contact
+ * or null if the chat is over
+ */
+ protected void setClientThread(ClientThread thread)
+ {
+ this.thread = thread;
+ }
+
+ /**
+ * Return the type of client
+ * @return Type of client used by this contact
+ */
+ public int getClientType()
+ {
+ return clientType;
+ }
+
+ /**
+ * Sets the type of client
+ * @param clientType Type of client used by this contact
+ */
+ public void setClientType(int clientType)
+ {
+ this.clientType = clientType;
+ }
+
+ /**
+ * Returns a String that can be used for identifying the contact.
+ *
+ * @return a String id representing and uniquely identifying the contact.
+ */
+ public String getAddress()
+ {
+ return contactID;
+ }
+
+ /**
+ * Returns a String that could be used by any user interacting modules
+ * for referring to this contact.
+ *
+ * @return a String that can be used for referring to this contact when
+ * interacting with the user.
+ */
+ public String getDisplayName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns the IP address declared by this Contact
+ * @return IP address declared by this Contact
+ */
+ public InetAddress getIpAddress()
+ {
+ return ipAddress;
+ }
+
+ /**
+ * Returns the TCP port declared by this Contact for direct chat
+ * @return the TCP port declared by this Contact for direct chat
+ */
+ public int getPort()
+ {
+ return port;
+ }
+
+
+ /**
+ * Returns the status/private message displayed by this contact
+ * @return the status/private message displayed by this contact
+ */
+ public String getMessage()
+ {
+ return message;
+ }
+
+ /**
+ * Sets the status/private message displayed by this contact
+ * @param message the status/private message displayed by this contact
+ */
+ public void setMessage(String message)
+ {
+ this.message = message;
+ }
+
+
+ /**
+ * Returns a byte array containing an image (most often a photo or an
+ * avatar) that the contact uses as a representation.
+ *
+ * @return byte[] an image representing the contact.
+ */
+ public byte[] getImage()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the status of the contact.
+ *
+ * @return always ZeroconfStatusEnum.
+ */
+ public PresenceStatus getPresenceStatus()
+ {
+ return this.presenceStatus;
+ }
+
+ /**
+ * Sets <tt>zeroconfPresenceStatus</tt> as the PresenceStatus that this
+ * contact is currently in.
+ * @param zeroconfPresenceStatus the <tt>ZeroconfPresenceStatus</tt>
+ * currently valid for this contact.
+ */
+ public void setPresenceStatus(PresenceStatus zeroconfPresenceStatus)
+ {
+ this.presenceStatus = zeroconfPresenceStatus;
+
+ if (zeroconfPresenceStatus == ZeroconfStatusEnum.OFFLINE) {
+ try
+ {
+ bonjourService.opSetPersPresence.unsubscribe((Contact)this);
+ }
+ catch (Exception ex)
+ {
+ logger.error(ex);
+ }
+ }
+ }
+
+ /**
+ * Returns a reference to the protocol provider that created the contact.
+ *
+ * @return a refererence to an instance of the ProtocolProviderService
+ */
+ public ProtocolProviderService getProtocolProvider()
+ {
+ return parentProvider;
+ }
+
+ /**
+ * Determines whether or not this contact represents our own identity.
+ *
+ * @return true in case this is a contact that represents ourselves and
+ * false otherwise.
+ */
+ public boolean isLocal()
+ {
+ return false;
+ }
+
+ /**
+ * Returns the group that contains this contact.
+ * @return a reference to the <tt>ContactGroupZeroconfImpl</tt> that
+ * contains this contact.
+ */
+ public ContactGroup getParentContactGroup()
+ {
+ return this.parentGroup;
+ }
+
+ /**
+ * Returns a string representation of this contact, containing most of its
+ * representative details.
+ *
+ * @return a string representation of this contact.
+ */
+ public String toString()
+ {
+ StringBuffer buff
+ = new StringBuffer("ContactZeroconfImpl[ DisplayName=")
+ .append(getDisplayName()).append("]");
+
+ return buff.toString();
+ }
+
+ /**
+ * Determines whether or not this contact is being stored by the server.
+ * Non persistent contacts are common in the case of simple, non-persistent
+ * presence operation sets. They could however also be seen in persistent
+ * presence operation sets when for example we have received an event
+ * from someone not on our contact list. Non persistent contacts are
+ * volatile even when coming from a persistent presence op. set. They would
+ * only exist until the application is closed and will not be there next
+ * time it is loaded.
+ *
+ * @return true if the contact is persistent and false otherwise.
+ */
+ public boolean isPersistent()
+ {
+ return isPersistent;
+ }
+
+ /**
+ * Specifies whether or not this contact is being stored by the server.
+ * Non persistent contacts are common in the case of simple, non-persistent
+ * presence operation sets. They could however also be seen in persistent
+ * presence operation sets when for example we have received an event
+ * from someone not on our contact list. Non persistent contacts are
+ * volatile even when coming from a persistent presence op. set. They would
+ * only exist until the application is closed and will not be there next
+ * time it is loaded.
+ *
+ * @param isPersistent true if the contact is persistent and false
+ * otherwise.
+ */
+ public void setPersistent(boolean isPersistent)
+ {
+ this.isPersistent = isPersistent;
+ }
+
+
+ /**
+ * Returns null as no persistent data is required and the contact address is
+ * sufficient for restoring the contact.
+ * <p>
+ * @return null as no such data is needed.
+ */
+ public String getPersistentData()
+ {
+ return null;
+ }
+
+ /**
+ * Determines whether or not this contact has been resolved against the
+ * server. Unresolved contacts are used when initially loading a contact
+ * list that has been stored in a local file until the presence operation
+ * set has managed to retrieve all the contact list from the server and has
+ * properly mapped contacts to their on-line buddies.
+ *
+ * @return true if the contact has been resolved (mapped against a buddy)
+ * and false otherwise.
+ */
+ public boolean isResolved()
+ {
+ return isResolved;
+ }
+
+ /**
+ * Makes the contact resolved or unresolved.
+ *
+ * @param resolved true to make the contact resolved; false to
+ * make it unresolved
+ */
+ public void setResolved(boolean resolved)
+ {
+ this.isResolved = resolved;
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this one which in terms
+ * of contacts translates to having equal ids. The resolved status of the
+ * contacts deliberately ignored so that contacts would be declared equal
+ * even if it differs.
+ * <p>
+ * @param obj the reference object with which to compare.
+ * @return <code>true</code> if this contact has the same id as that of the
+ * <code>obj</code> argument.
+ */
+ public boolean equals(Object obj)
+ {
+ if (obj == null
+ || ! (obj instanceof ContactZeroconfImpl))
+ return false;
+
+ ContactZeroconfImpl zeroconfContact = (ContactZeroconfImpl) obj;
+
+ return this.getAddress().equals(zeroconfContact.getAddress());
+ }
+
+
+ /**
+ * Returns the persistent presence operation set that this contact belongs
+ * to.
+ *
+ * @return the <tt>OperationSetPersistentPresenceZeroconfImpl</tt> that
+ * this contact belongs to.
+ */
+ public OperationSetPersistentPresenceZeroconfImpl
+ getParentPresenceOperationSet()
+ {
+ return (OperationSetPersistentPresenceZeroconfImpl)parentProvider
+ .getOperationSet(OperationSetPersistentPresence.class);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/MessageZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/MessageZeroconfImpl.java
new file mode 100644
index 0000000..7bbcf70
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/MessageZeroconfImpl.java
@@ -0,0 +1,294 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * Very simple message implementation for the Zeroconf protocol.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ * @author Jonathan Martin
+ */
+public class MessageZeroconfImpl
+ implements Message
+{
+ /**
+ * The actual message content.
+ */
+ private String textContent = null;
+
+ /**
+ * The content type of the message. (text/plain if null)
+ */
+ private String contentType = null;
+
+ /**
+ * The message encoding. (UTF8 if null).
+ */
+ private String contentEncoding = null;
+
+ /**
+ * A String uniquely identifying the message
+ */
+ private String messageUID = null;
+
+ /**
+ * The subject of the message. (most often is null)
+ */
+ private String subject = null;
+
+ /**
+ * Message Type.
+ */
+ private int type;
+
+ /**
+ * Message type indicating that a stream is being created
+ */
+ public static final int STREAM_OPEN = 0x1;
+ /**
+ * Normal chat message
+ */
+ public static final int MESSAGE = 0x2;
+ /**
+ * Typing notification
+ */
+ public static final int TYPING = 0x3;
+ /**
+ * Message indicating that the stream is being closed
+ */
+ public static final int STREAM_CLOSE = 0x4;
+ /**
+ * Message indicating that the previsous message was delivered successfully
+ */
+ public static final int DELIVERED = 0x5;
+ /**
+ * Undefined message
+ */
+ public static final int UNDEF = 0x6;
+
+
+ /*
+ * The Baloon Icon color.
+ * (we probably won't ever use it)
+ */
+ private int baloonColor = 0x7BB5EE;
+
+ /*
+ * The Text Color.
+ */
+ private int textColor = 0x000000;
+
+ /*
+ * The font of the message.
+ */
+ private String textFont = "Helvetica";
+
+ /*
+ * The size of the caracters composing the message.
+ */
+ private int textSize = 12;
+
+ /*
+ * The source contact id announced in the message.
+ * TODO: Could be set & checked to identify more precisely the contact in
+ * case several users would be sharing the same IP.
+ */
+ private String contactID;
+
+ /**
+ * Creates a message instance according to the specified parameters.
+ * @param type Type of message
+ * @param content the message body
+ * @param contentEncoding message encoding or null for UTF8
+ */
+ public MessageZeroconfImpl(String content,
+ String contentEncoding,
+ int type)
+ {
+ this.textContent = content;
+ this.contentEncoding = contentEncoding;
+ this.type = type;
+
+ //generate the uid
+ this.messageUID = String.valueOf(System.currentTimeMillis())
+ + String.valueOf(hashCode());
+
+ }
+
+ /**
+ * Returns the message body.
+ *
+ * @return the message content.
+ */
+ public String getContent()
+ {
+ return textContent;
+ }
+
+ /**
+ * Returns the type of the content of this message.
+ *
+ * @return the type of the content of this message.
+ */
+ public String getContentType()
+ {
+ return contentType;
+ }
+
+ /**
+ * Returns the encoding used for the message content.
+ *
+ * @return the encoding of the message body.
+ */
+ public String getEncoding()
+ {
+ return contentEncoding;
+ }
+
+ /**
+ * A string uniquely identifying the message.
+ *
+ * @return a <tt>String</tt> uniquely identifying the message.
+ */
+ public String getMessageUID()
+ {
+ return messageUID;
+ }
+
+ /**
+ * Returns the message body in a binary form.
+ *
+ * @return a <tt>byte[]</tt> representation of the message body.
+ */
+ public byte[] getRawData()
+ {
+ return getContent().getBytes();
+ }
+
+ /**
+ * Return the length of this message.
+ *
+ * @return the length of this message.
+ */
+ public int getSize()
+ {
+ return getContent().length();
+ }
+
+ /**
+ * Returns the subject of the message. ALWAYS null in Zeroconf
+ * @return null
+ */
+ public String getSubject()
+ {
+ return subject;
+ }
+
+
+
+ /**
+ * Returns the type of message. Always text/plain for Zeroconf, so null.
+ * @return null
+ */
+ public int getType()
+ {
+ return type;
+ }
+
+ /**
+ * Gets the baloon color declared in messages sent by iChat-like clients
+ * @return baloon color
+ */
+ public int getBaloonColor()
+ {
+ return baloonColor;
+ }
+
+ /**
+ * Sets the baloon color declared in messages sent by iChat-like clients
+ * @param baloonColor baloon color
+ */
+ public void setBaloonColor(int baloonColor)
+ {
+ this.baloonColor = baloonColor;
+ }
+
+ /**
+ * Returns the text color
+ * @return Text color
+ */
+ public int getTextColor()
+ {
+ return textColor;
+ }
+
+ /**
+ * Sets the text color
+ * @param textColor Text color
+ */
+ public void setTextColor(int textColor)
+ {
+ this.textColor = textColor;
+ }
+
+ /**
+ * Returns the text font
+ * @return Text font
+ */
+ public String getTextFont()
+ {
+ return textFont;
+ }
+
+ /**
+ * Sets the text color
+ * @param textFont Text font
+ */
+ public void setTextFont(String textFont)
+ {
+ this.textFont = textFont;
+ }
+
+ /**
+ * Returns the text size
+ * @return Text size
+ */
+ public int getTextSize()
+ {
+ return textSize;
+ }
+
+ /**
+ * Sets the text size
+ * @param textSize Text size
+ */
+ public void setTextSize(int textSize)
+ {
+ this.textSize = textSize;
+ }
+
+ /**
+ * Returns the contact's ID
+ * @return String representing the contact's ID
+ */
+ public String getContactID()
+ {
+ return contactID;
+ }
+
+ /**
+ * Sets the contact's ID
+ * @param contactID String representing the contact's ID
+ */
+ public void setContactID(String contactID)
+ {
+ this.contactID = contactID;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetBasicInstantMessagingZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetBasicInstantMessagingZeroconfImpl.java
new file mode 100644
index 0000000..151d5b9
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetBasicInstantMessagingZeroconfImpl.java
@@ -0,0 +1,275 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Instant messaging functionalites for the Zeroconf protocol.
+ *
+ * @author Christian Vincenot
+ *
+ */
+public class OperationSetBasicInstantMessagingZeroconfImpl
+ implements OperationSetBasicInstantMessaging
+{
+
+ private static final Logger logger
+ = Logger.getLogger(ContactGroupZeroconfImpl.class);
+
+ /**
+ * Currently registered message listeners.
+ */
+ private Vector messageListeners = new Vector();
+
+ /**
+ * The currently valid persistent presence operation set..
+ */
+ private OperationSetPersistentPresenceZeroconfImpl opSetPersPresence = null;
+
+ /**
+ * The protocol provider that created us.
+ */
+ private ProtocolProviderServiceZeroconfImpl parentProvider = null;
+
+ /**
+ * Creates an instance of this operation set keeping a reference to the
+ * parent protocol provider and presence operation set.
+ *
+ * @param provider The provider instance that creates us.
+ * @param opSetPersPresence the currently valid
+ * <tt>OperationSetPersistentPresenceZeroconfImpl</tt> instance.
+ */
+ public OperationSetBasicInstantMessagingZeroconfImpl(
+ ProtocolProviderServiceZeroconfImpl provider,
+ OperationSetPersistentPresenceZeroconfImpl opSetPersPresence)
+ {
+ this.opSetPersPresence = opSetPersPresence;
+ this.parentProvider = provider;
+ }
+
+ /**
+ * Registers a MessageListener with this operation set so that it gets
+ * notifications of successful message delivery, failure or reception of
+ * incoming messages..
+ *
+ * @param listener the <tt>MessageListener</tt> to register.
+ */
+ public void addMessageListener(MessageListener listener)
+ {
+ if(!messageListeners.contains(listener))
+ messageListeners.add(listener);
+ }
+
+ /**
+ * Create a Message instance for sending arbitrary MIME-encoding content.
+ *
+ * @param content content value
+ * @param contentType the MIME-type for <tt>content</tt>
+ * @param contentEncoding encoding used for <tt>content</tt>
+ * @param subject a <tt>String</tt> subject or <tt>null</tt> for now
+ * subject.
+ * @return the newly created message.
+ */
+ public Message createMessage(byte[] content, String contentType,
+ String contentEncoding, String subject)
+ {
+ return new MessageZeroconfImpl(new String(content),
+ contentEncoding, MessageZeroconfImpl.MESSAGE);
+ }
+
+ /**
+ * Create a Message instance for sending a simple text messages with
+ * default (text/plain) content type and encoding.
+ *
+ * @param messageText the string content of the message.
+ * @return Message the newly created message
+ */
+ public Message createMessage(String messageText)
+ {
+ return new MessageZeroconfImpl(messageText,
+ DEFAULT_MIME_ENCODING, MessageZeroconfImpl.MESSAGE);
+ }
+
+ /**
+ * Unregisteres <tt>listener</tt> so that it won't receive any further
+ * notifications upon successful message delivery, failure or reception
+ * of incoming messages..
+ *
+ * @param listener the <tt>MessageListener</tt> to unregister.
+ */
+ public void removeMessageListener(MessageListener listener)
+ {
+ messageListeners.remove(listener);
+ }
+
+ /**
+ * Sends the <tt>message</tt> to the destination indicated by the
+ * <tt>to</tt> contact.
+ *
+ * @param to the <tt>Contact</tt> to send <tt>message</tt> to
+ * @param message the <tt>Message</tt> to send.
+ * @throws IllegalStateException if the underlying Zeroconf stack is not
+ * registered and initialized.
+ * @throws IllegalArgumentException if <tt>to</tt> is not an instance
+ * belonging to the underlying implementation.
+ */
+ public void sendInstantMessage(Contact to, Message message) throws
+ IllegalStateException, IllegalArgumentException
+ {
+ if( !(to instanceof ContactZeroconfImpl) )
+ throw new IllegalArgumentException(
+ "The specified contact is not a Zeroconf contact."
+ + to);
+
+ MessageZeroconfImpl msg = new MessageZeroconfImpl(message.getContent(),
+ null, MessageZeroconfImpl.MESSAGE);
+
+ MessageDeliveredEvent msgDeliveredEvt
+ = new MessageDeliveredEvent(
+ msg, to, new Date());
+
+ deliverMessage(msg, (ContactZeroconfImpl)to);
+
+ }
+
+ /**
+ * In case the to the <tt>to</tt> Contact corresponds to another zeroconf
+ * protocol provider registered with SIP Communicator, we deliver
+ * the message to them, in case the <tt>to</tt> Contact represents us, we
+ * fire a <tt>MessageReceivedEvent</tt>, and if <tt>to</tt> is simply
+ * a contact in our contact list, then we simply echo the message.
+ *
+ *
+ *
+ *
+ *
+ * @param message the <tt>Message</tt> the message to deliver.
+ * @param to the <tt>Contact</tt> that we should deliver the message to.
+ */
+ private void deliverMessage(Message message, ContactZeroconfImpl to)
+ {
+ ClientThread thread = to.getClientThread();
+ try
+ {
+ if (thread == null)
+ {
+ Socket sock;
+ logger.debug("ZEROCONF: Creating a chat connexion to "
+ +to.getIpAddress()+":"+to.getPort());
+ sock = new Socket(to.getIpAddress(), to.getPort());
+ thread = new ClientThread(sock, to.getBonjourService());
+ thread.setStreamOpen();
+ thread.setContact(to);
+ to.setClientThread(thread);
+ thread.sendHello();
+ if (to.getClientType() == to.GAIM)
+ {
+ try
+ {
+ Thread.sleep(300);
+ }
+ catch (InterruptedException ex)
+ {
+ logger.error(ex);
+ }
+ }
+ }
+
+ //System.out.println("ZEROCONF: Message content => "+
+ //message.getContent());
+ thread.sendMessage((MessageZeroconfImpl) message);
+
+ fireMessageDelivered(message, to);
+ }
+ catch (IOException ex)
+ {
+ logger.error(ex);
+ }
+
+ }
+
+ /**
+ * Notifies all registered message listeners that a message has been
+ * delivered successfully to its addressee..
+ *
+ * @param message the <tt>Message</tt> that has been delivered.
+ * @param to the <tt>Contact</tt> that <tt>message</tt> was delivered to.
+ */
+ private void fireMessageDelivered(Message message, Contact to)
+ {
+ MessageDeliveredEvent evt
+ = new MessageDeliveredEvent(message, to, new Date());
+
+ Iterator listeners = null;
+ synchronized (messageListeners)
+ {
+ listeners = new ArrayList(messageListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ MessageListener listener
+ = (MessageListener) listeners.next();
+
+ listener.messageDelivered(evt);
+ }
+ }
+
+ /**
+ * Notifies all registered message listeners that a message has been
+ * received.
+ *
+ * @param message the <tt>Message</tt> that has been received.
+ * @param from the <tt>Contact</tt> that <tt>message</tt> was received from.
+ */
+ public void fireMessageReceived(Message message, Contact from)
+ {
+
+ MessageReceivedEvent evt
+ = new MessageReceivedEvent(message, from, new Date());
+
+ Iterator listeners = null;
+ synchronized (messageListeners)
+ {
+ listeners = new ArrayList(messageListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ MessageListener listener
+ = (MessageListener) listeners.next();
+
+ listener.messageReceived(evt);
+ }
+ }
+
+ /**
+ * Determines wheter the protocol provider (or the protocol itself) support
+ * sending and receiving offline messages. Most often this method would
+ * return true for protocols that support offline messages and false for
+ * those that don't. It is however possible for a protocol to support these
+ * messages and yet have a particular account that does not (i.e. feature
+ * not enabled on the protocol server). In cases like this it is possible
+ * for this method to return true even when offline messaging is not
+ * supported, and then have the sendMessage method throw an
+ * OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
+ *
+ * @return <tt>true</tt> if the protocol supports offline messages and
+ * <tt>false</tt> otherwise.
+ */
+ public boolean isOfflineMessagingSupported()
+ {
+ return true;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetPersistentPresenceZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetPersistentPresenceZeroconfImpl.java
new file mode 100644
index 0000000..11810b7
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetPersistentPresenceZeroconfImpl.java
@@ -0,0 +1,1219 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.net.*;
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+import org.osgi.framework.*;
+
+/**
+ * A Zeroconf implementation of a persistent presence operation set. In order
+ * to simulate server persistence, this operation set would simply accept all
+ * unresolved contacts and resolve them immediately. A real world protocol
+ * implementation would save it on a server using methods provided by the
+ * protocol stack.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ * @author Jonathan Martin
+ */
+public class OperationSetPersistentPresenceZeroconfImpl
+ implements OperationSetPersistentPresence
+{
+ private static final Logger logger =
+ Logger.getLogger(OperationSetPersistentPresenceZeroconfImpl.class);
+ /**
+ * A list of listeners registered for <tt>SubscriptionEvent</tt>s.
+ */
+ private Vector subscriptionListeners = new Vector();
+
+ /**
+ * A list of listeners registered for <tt>ServerStoredGroupChangeEvent</tt>s.
+ */
+ private Vector serverStoredGroupListeners = new Vector();
+
+ /**
+ * A list of listeners registered for
+ * <tt>ProviderPresenceStatusChangeEvent</tt>s.
+ */
+ private Vector providerPresenceStatusListeners = new Vector();
+
+ /**
+ * A list of listeneres registered for
+ * <tt>ContactPresenceStatusChangeEvent</tt>s.
+ */
+ private Vector contactPresenceStatusListeners = new Vector();
+
+ /**
+ * The root of the zeroconf contact list.
+ */
+ private ContactGroupZeroconfImpl contactListRoot = null;
+
+ /**
+ * The provider that created us.
+ */
+ private ProtocolProviderServiceZeroconfImpl parentProvider = null;
+
+ /**
+ * The currently active status message.
+ */
+ private String statusMessage = "The truth is out there...";
+
+ /**
+ * Our default presence status.
+ */
+ private PresenceStatus presenceStatus = ZeroconfStatusEnum.OFFLINE;
+
+ /**
+ * The <tt>AuthorizationHandler</tt> instance that we'd have to transmit
+ * authorization requests to for approval.
+ */
+ private AuthorizationHandler authorizationHandler = null;
+
+ /**
+ * Creates an instance of this operation set keeping a reference to the
+ * specified parent <tt>provider</tt>.
+ * @param provider the ProtocolProviderServiceZeroconfImpl instance that
+ * created us.
+ */
+ public OperationSetPersistentPresenceZeroconfImpl(
+ ProtocolProviderServiceZeroconfImpl provider)
+ {
+
+ this.parentProvider = provider;
+ contactListRoot = new ContactGroupZeroconfImpl("RootGroup", provider);
+
+ //add our unregistration listener
+ parentProvider.addRegistrationStateChangeListener(
+ new UnregistrationListener());
+ }
+
+ /**
+ * Zeroconf implementation of the corresponding ProtocolProviderService
+ * method.
+ *
+ * @param listener a dummy param.
+ */
+ public void addContactPresenceStatusListener(
+ ContactPresenceStatusListener listener)
+ {
+ synchronized(contactPresenceStatusListeners)
+ {
+ if (!contactPresenceStatusListeners.contains(listener))
+ contactPresenceStatusListeners.add(listener);
+ }
+ }
+
+ /**
+ * Notifies all registered listeners of the new event.
+ *
+ * @param source the contact that has caused the event.
+ * @param parentGroup the group that contains the source contact.
+ * @param oldValue the status that the source contact detained before
+ * changing it.
+ */
+ public void fireContactPresenceStatusChangeEvent(ContactZeroconfImpl source,
+ ContactGroup parentGroup,
+ PresenceStatus oldValue)
+ {
+ ContactPresenceStatusChangeEvent evt
+ = new ContactPresenceStatusChangeEvent(source, parentProvider
+ , parentGroup, oldValue, source.getPresenceStatus());
+
+ Iterator listeners = null;
+ synchronized(contactPresenceStatusListeners)
+ {
+ listeners = new ArrayList(contactPresenceStatusListeners).iterator();
+ }
+
+ while(listeners.hasNext())
+ {
+ ContactPresenceStatusListener listener
+ = (ContactPresenceStatusListener)listeners.next();
+
+ listener.contactPresenceStatusChanged(evt);
+ }
+ }
+
+
+ /**
+ * Notifies all registered listeners of the new event.
+ *
+ * @param source the contact that has caused the event.
+ * @param parentGroup the group that contains the source contact.
+ * @param eventID an identifier of the event to dispatch.
+ */
+ public void fireSubscriptionEvent(ContactZeroconfImpl source,
+ ContactGroup parentGroup,
+ int eventID)
+ {
+ SubscriptionEvent evt = new SubscriptionEvent(source
+ , this.parentProvider
+ , parentGroup
+ , eventID);
+
+ //logger.debug("ZEROCNF: Creation contact " + source.getAddress());
+ Iterator listeners = null;
+ synchronized (subscriptionListeners)
+ {
+ listeners = new ArrayList(subscriptionListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ SubscriptionListener listener
+ = (SubscriptionListener) listeners.next();
+
+ if(eventID == SubscriptionEvent.SUBSCRIPTION_CREATED)
+ {
+ listener.subscriptionCreated(evt);
+ }
+ else if (eventID == SubscriptionEvent.SUBSCRIPTION_FAILED)
+ {
+ listener.subscriptionFailed(evt);
+ }
+ else if (eventID == SubscriptionEvent.SUBSCRIPTION_REMOVED)
+ {
+ listener.subscriptionRemoved(evt);
+ }
+ }
+ }
+
+ /**
+ * Notifies all registered listeners of the new event.
+ *
+ * @param source the contact that has been moved..
+ * @param oldParent the group where the contact was located before being
+ * moved.
+ * @param newParent the group where the contact has been moved.
+ */
+ public void fireSubscriptionMovedEvent(Contact source,
+ ContactGroup oldParent,
+ ContactGroup newParent)
+ {
+ SubscriptionMovedEvent evt = new SubscriptionMovedEvent(source
+ , this.parentProvider
+ , oldParent
+ , newParent);
+
+ Iterator listeners = null;
+ synchronized (subscriptionListeners)
+ {
+ listeners = new ArrayList(subscriptionListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ SubscriptionListener listener
+ = (SubscriptionListener) listeners.next();
+
+ listener.subscriptionMoved(evt);
+ }
+ }
+
+
+ /**
+ * Notifies all registered listeners of the new event.
+ *
+ * @param source the contact that has caused the event.
+ * @param eventID an identifier of the event to dispatch.
+ */
+ public void fireServerStoredGroupEvent(ContactGroupZeroconfImpl source,
+ int eventID)
+ {
+ ServerStoredGroupEvent evt = new ServerStoredGroupEvent(
+ source, eventID, (ContactGroupZeroconfImpl)
+ source.getParentContactGroup()
+ , this.parentProvider, this);
+
+ Iterator listeners = null;
+ synchronized (serverStoredGroupListeners)
+ {
+ listeners = new ArrayList(serverStoredGroupListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ ServerStoredGroupListener listener
+ = (ServerStoredGroupListener) listeners.next();
+
+ if(eventID == ServerStoredGroupEvent.GROUP_CREATED_EVENT)
+ {
+ listener.groupCreated(evt);
+ }
+ else if(eventID == ServerStoredGroupEvent.GROUP_RENAMED_EVENT)
+ {
+ listener.groupNameChanged(evt);
+ }
+ else if(eventID == ServerStoredGroupEvent.GROUP_REMOVED_EVENT)
+ {
+ listener.groupRemoved(evt);
+ }
+ }
+ }
+
+ /**
+ * Notifies all registered listeners of the new event.
+ *
+ * @param oldValue the presence status we were in before the change.
+ */
+ public void fireProviderStatusChangeEvent(PresenceStatus oldValue)
+ {
+ ProviderPresenceStatusChangeEvent evt
+ = new ProviderPresenceStatusChangeEvent(this.parentProvider,
+ oldValue, this.getPresenceStatus());
+
+ Iterator listeners = null;
+ synchronized (providerPresenceStatusListeners)
+ {
+ listeners =
+ new ArrayList(providerPresenceStatusListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ ProviderPresenceStatusListener listener
+ = (ProviderPresenceStatusListener) listeners.next();
+
+ listener.providerStatusChanged(evt);
+ }
+ }
+
+ /**
+ * Zeroconf implementation of the corresponding ProtocolProviderService
+ * method.
+ *
+ * @param listener a dummy param.
+ */
+ public void addProviderPresenceStatusListener(
+ ProviderPresenceStatusListener listener)
+ {
+ synchronized(providerPresenceStatusListeners)
+ {
+ if (!providerPresenceStatusListeners.contains(listener))
+ this.providerPresenceStatusListeners.add(listener);
+ }
+ }
+
+ /**
+ * Registers a listener that would receive events upon changes in server
+ * stored groups.
+ *
+ * @param listener a ServerStoredGroupChangeListener impl that would
+ * receive events upong group changes.
+ */
+ public void addServerStoredGroupChangeListener(ServerStoredGroupListener
+ listener)
+ {
+ synchronized(serverStoredGroupListeners)
+ {
+ if (!serverStoredGroupListeners.contains(listener))
+ serverStoredGroupListeners.add(listener);
+ }
+ }
+
+ /**
+ * Zeroconf implementation of the corresponding ProtocolProviderService
+ * method.
+ *
+ * @param listener the SubscriptionListener to register
+ */
+ public void addSubsciptionListener(SubscriptionListener listener)
+ {
+ synchronized(subscriptionListeners)
+ {
+ if (!subscriptionListeners.contains(listener))
+ this.subscriptionListeners.add(listener);
+ }
+ }
+
+ /**
+ * Creates a group with the specified name and parent in the server
+ * stored contact list.
+ *
+ * @param parent the group where the new group should be created
+ * @param groupName the name of the new group to create.
+ */
+ public void createServerStoredContactGroup(ContactGroup parent,
+ String groupName)
+ {
+ ContactGroupZeroconfImpl newGroup
+ = new ContactGroupZeroconfImpl(groupName, parentProvider);
+
+ ((ContactGroupZeroconfImpl)parent).addSubgroup(newGroup);
+
+ this.fireServerStoredGroupEvent(
+ newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
+ }
+
+ /**
+ * A Zeroconf Provider method to use for fast filling of a contact list.
+ *
+ * @param contactGroup the group to add
+ */
+ public void addZeroconfGroup(ContactGroupZeroconfImpl contactGroup)
+ {
+ contactListRoot.addSubgroup(contactGroup);
+ }
+
+ /**
+ * A Zeroconf Provider method to use for fast filling of a contact list.
+ * This method would add both the group and fire an event.
+ *
+ * @param parent the group where <tt>contactGroup</tt> should be added.
+ * @param contactGroup the group to add
+ */
+ public void addZeroconfGroupAndFireEvent(
+ ContactGroupZeroconfImpl parent
+ , ContactGroupZeroconfImpl contactGroup)
+ {
+ parent.addSubgroup(contactGroup);
+
+ this.fireServerStoredGroupEvent(
+ contactGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
+ }
+
+
+ /**
+ * Returns a reference to the contact with the specified ID in case we
+ * have a subscription for it and null otherwise/
+ *
+ * @param contactID a String identifier of the contact which we're
+ * seeking a reference of.
+ * @return a reference to the Contact with the specified
+ * <tt>contactID</tt> or null if we don't have a subscription for the
+ * that identifier.
+ */
+ public Contact findContactByID(String contactID)
+ {
+ return contactListRoot.findContactByID(contactID);
+ }
+
+ /**
+ * Sets the specified status message.
+ * @param statusMessage a String containing the new status message.
+ */
+ public void setStatusMessage(String statusMessage)
+ {
+ this.statusMessage = statusMessage;
+ }
+
+ /**
+ * Returns the status message that was last set through
+ * setCurrentStatusMessage.
+ *
+ * @return the last status message that we have requested and the aim
+ * server has confirmed.
+ */
+ public String getCurrentStatusMessage()
+ {
+ return statusMessage;
+ }
+
+ /**
+ * Returns a PresenceStatus instance representing the state this provider
+ * is currently in.
+ *
+ * @return the PresenceStatus last published by this provider.
+ */
+ public PresenceStatus getPresenceStatus()
+ {
+ return presenceStatus;
+ }
+
+ /**
+ * Returns the root group of the server stored contact list.
+ *
+ * @return the root ContactGroup for the ContactList stored by this
+ * service.
+ */
+ public ContactGroup getServerStoredContactListRoot()
+ {
+ return contactListRoot;
+ }
+
+ /**
+ * Returns the set of PresenceStatus objects that a user of this service
+ * may request the provider to enter.
+ *
+ * @return Iterator a PresenceStatus array containing "enterable" status
+ * instances.
+ */
+ public Iterator getSupportedStatusSet()
+ {
+ return ZeroconfStatusEnum.supportedStatusSet();
+ }
+
+ /**
+ * Removes the specified contact from its current parent and places it
+ * under <tt>newParent</tt>.
+ *
+ * @param contactToMove the <tt>Contact</tt> to move
+ * @param newParent the <tt>ContactGroup</tt> where <tt>Contact</tt>
+ * would be placed.
+ */
+ public void moveContactToGroup(Contact contactToMove,
+ ContactGroup newParent)
+ {
+ ContactZeroconfImpl zeroconfContact
+ = (ContactZeroconfImpl)contactToMove;
+
+ ContactGroupZeroconfImpl parentZeroconfGroup
+ = findContactParent(zeroconfContact);
+
+ parentZeroconfGroup.removeContact(zeroconfContact);
+
+ //if this is a volatile contact then we haven't really subscribed to
+ //them so we'd need to do so here
+ if(!zeroconfContact.isPersistent())
+ {
+ //first tell everyone that the volatile contact was removed
+ fireSubscriptionEvent(zeroconfContact
+ , parentZeroconfGroup
+ , SubscriptionEvent.SUBSCRIPTION_REMOVED);
+
+ try
+ {
+ //now subscribe
+ this.subscribe(newParent, contactToMove.getAddress());
+
+ //now tell everyone that we've added the contact
+ fireSubscriptionEvent(zeroconfContact
+ , newParent
+ , SubscriptionEvent.SUBSCRIPTION_CREATED);
+ }
+ catch (Exception ex)
+ {
+ logger.error("Failed to move contact "
+ + zeroconfContact.getAddress()
+ , ex);
+ }
+ }
+ else
+ {
+ ( (ContactGroupZeroconfImpl) newParent)
+ .addContact(zeroconfContact);
+
+ fireSubscriptionMovedEvent(contactToMove
+ , parentZeroconfGroup
+ , newParent);
+ }
+ }
+
+ /**
+ * Requests the provider to enter into a status corresponding to the
+ * specified paramters.
+ *
+ * @param status the PresenceStatus as returned by
+ * getRequestableStatusSet
+ * @param statusMessage the message that should be set as the reason to
+ * enter that status
+ * @throws IllegalArgumentException if the status requested is not a
+ * valid PresenceStatus supported by this provider.
+ * @throws IllegalStateException if the provider is not currently
+ * registered.
+ * @throws OperationFailedException with code NETWORK_FAILURE if
+ * publishing the status fails due to a network error.
+ */
+ public void publishPresenceStatus(PresenceStatus status,
+ String statusMessage)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ OperationFailedException
+ {
+ PresenceStatus oldPresenceStatus = this.presenceStatus;
+ this.presenceStatus = status;
+ this.statusMessage = statusMessage;
+
+ //ICI: changer le statut du plugin Zeroconf!!
+ parentProvider.getBonjourService().changeStatus(status);
+
+ this.fireProviderStatusChangeEvent(oldPresenceStatus);
+
+ }
+
+ /**
+ * Get the PresenceStatus for a particular contact.
+ *
+ * @param contactIdentifier the identifier of the contact whose status
+ * we're interested in.
+ * @return PresenceStatus the <tt>PresenceStatus</tt> of the specified
+ * <tt>contact</tt>
+ * @throws IllegalArgumentException if <tt>contact</tt> is not a contact
+ * known to the underlying protocol provider
+ * @throws IllegalStateException if the underlying protocol provider is
+ * not registered/signed on a public service.
+ * @throws OperationFailedException with code NETWORK_FAILURE if
+ * retrieving the status fails due to errors experienced during
+ * network communication
+ */
+ public PresenceStatus queryContactStatus(String contactIdentifier)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ OperationFailedException
+ {
+ return findContactByID(contactIdentifier).getPresenceStatus();
+ }
+
+ /**
+ * Sets the presence status of <tt>contact</tt> to <tt>newStatus</tt>.
+ *
+ * @param contact the <tt>ContactZeroconfImpl</tt> whose status we'd like
+ * to set.
+ * @param newStatus the new status we'd like to set to <tt>contact</tt>.
+ */
+ public void changePresenceStatusForContact(ContactZeroconfImpl contact,
+ PresenceStatus newStatus)
+ {
+ PresenceStatus oldStatus = contact.getPresenceStatus();
+ contact.setPresenceStatus(newStatus);
+
+ fireContactPresenceStatusChangeEvent(
+ contact, findContactParent(contact), oldStatus);
+ }
+
+ /**
+ * Sets the presence status of all <tt>contact</tt>s in our contact list
+ * (except those that correspond to another provider registered with SC)
+ * to <tt>newStatus</tt>.
+ *
+ * @param newStatus the new status we'd like to set to <tt>contact</tt>.
+ * @param parent the group in which we'd have to update the status of all
+ * direct and indirect child contacts.
+ */
+ protected void changePresenceStatusForAllContacts(ContactGroup parent,
+ PresenceStatus newStatus)
+ {
+ //first set the status for contacts in this group
+ Iterator childContacts = parent.contacts();
+
+ while(childContacts.hasNext())
+ {
+ ContactZeroconfImpl contact
+ = (ContactZeroconfImpl)childContacts.next();
+
+ if(findProviderForZeroconfUserID(contact.getAddress()) != null)
+ {
+ //this is a contact corresponding to another SIP Communicator
+ //provider so we won't change it's status here.
+ continue;
+ }
+ PresenceStatus oldStatus = contact.getPresenceStatus();
+ contact.setPresenceStatus(newStatus);
+
+ fireContactPresenceStatusChangeEvent(
+ contact, parent, oldStatus);
+ }
+
+ //now call this method recursively for all subgroups
+ Iterator subgroups = parent.subgroups();
+
+ while(subgroups.hasNext())
+ {
+ ContactGroup subgroup = (ContactGroup)subgroups.next();
+ changePresenceStatusForAllContacts(subgroup, newStatus);
+ }
+ }
+
+
+ /**
+ * Removes the specified listener so that it won't receive any further
+ * updates on contact presence status changes
+ *
+ * @param listener the listener to remove.
+ */
+ public void removeContactPresenceStatusListener(
+ ContactPresenceStatusListener listener)
+ {
+ synchronized(contactPresenceStatusListeners)
+ {
+ contactPresenceStatusListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Unregisters the specified listener so that it does not receive further
+ * events upon changes in local presence status.
+ *
+ * @param listener ProviderPresenceStatusListener
+ */
+ public void removeProviderPresenceStatusListener(
+ ProviderPresenceStatusListener listener)
+ {
+ synchronized(providerPresenceStatusListeners)
+ {
+ this.providerPresenceStatusListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Returns the group that is parent of the specified zeroconfGroup or null
+ * if no parent was found.
+ * @param zeroconfGroup the group whose parent we're looking for.
+ * @return the ContactGroupZeroconfImpl instance that zeroconfGroup
+ * belongs to or null if no parent was found.
+ */
+ public ContactGroupZeroconfImpl findGroupParent(
+ ContactGroupZeroconfImpl zeroconfGroup)
+ {
+ return contactListRoot.findGroupParent(zeroconfGroup);
+ }
+
+ /**
+ * Returns the group that is parent of the specified zeroconfContact or
+ * null if no parent was found.
+ * @param zeroconfContact the contact whose parent we're looking for.
+ * @return the ContactGroupZeroconfImpl instance that zeroconfContact
+ * belongs to or null if no parent was found.
+ */
+ public ContactGroupZeroconfImpl findContactParent(
+ ContactZeroconfImpl zeroconfContact)
+ {
+ return (ContactGroupZeroconfImpl)zeroconfContact
+ .getParentContactGroup();
+ }
+
+
+ /**
+ * Removes the specified group from the server stored contact list.
+ *
+ * @param group the group to remove.
+ *
+ * @throws IllegalArgumentException if <tt>group</tt> was not found in this
+ * protocol's contact list.
+ */
+ public void removeServerStoredContactGroup(ContactGroup group)
+ throws IllegalArgumentException
+ {
+ ContactGroupZeroconfImpl zeroconfGroup
+ = (ContactGroupZeroconfImpl)group;
+
+ ContactGroupZeroconfImpl parent = findGroupParent(zeroconfGroup);
+
+ if(parent == null){
+ throw new IllegalArgumentException(
+ "group " + group
+ + " does not seem to belong to this protocol's contact list.");
+ }
+
+ parent.removeSubGroup(zeroconfGroup);
+
+ this.fireServerStoredGroupEvent(
+ zeroconfGroup, ServerStoredGroupEvent.GROUP_REMOVED_EVENT);
+ }
+
+
+ /**
+ * Removes the specified group change listener so that it won't receive
+ * any further events.
+ *
+ * @param listener the ServerStoredGroupChangeListener to remove
+ */
+ public void removeServerStoredGroupChangeListener(ServerStoredGroupListener
+ listener)
+ {
+ synchronized(serverStoredGroupListeners)
+ {
+ serverStoredGroupListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Removes the specified subscription listener.
+ *
+ * @param listener the listener to remove.
+ */
+ public void removeSubscriptionListener(SubscriptionListener listener)
+ {
+ synchronized(subscriptionListeners)
+ {
+ this.subscriptionListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Renames the specified group from the server stored contact list.
+ *
+ * @param group the group to rename.
+ * @param newName the new name of the group.
+ */
+ public void renameServerStoredContactGroup(ContactGroup group,
+ String newName)
+ {
+ ((ContactGroupZeroconfImpl)group).setGroupName(newName);
+
+ this.fireServerStoredGroupEvent(
+ (ContactGroupZeroconfImpl)group,
+ ServerStoredGroupEvent.GROUP_RENAMED_EVENT);
+ }
+
+ /**
+ * Handler for incoming authorization requests.
+ *
+ * @param handler an instance of an AuthorizationHandler for
+ * authorization requests coming from other users requesting
+ * permission add us to their contact list.
+ */
+ public void setAuthorizationHandler(AuthorizationHandler handler)
+ {
+ this.authorizationHandler = handler;
+ }
+
+ /**
+ * Persistently adds a subscription for the presence status of the
+ * contact corresponding to the specified contactIdentifier and indicates
+ * that it should be added to the specified group of the server stored
+ * contact list.
+ *
+ * @param parent the parent group of the server stored contact list
+ * where the contact should be added. <p>
+ * @param contactIdentifier the contact whose status updates we are
+ * subscribing for.
+ * @throws IllegalArgumentException if <tt>contact</tt> or
+ * <tt>parent</tt> are not a contact known to the underlying protocol
+ * provider.
+ * @throws IllegalStateException if the underlying protocol provider is
+ * not registered/signed on a public service.
+ * @throws OperationFailedException with code NETWORK_FAILURE if
+ * subscribing fails due to errors experienced during network
+ * communication
+ */
+ public void subscribe(ContactGroup parent, String contactIdentifier)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ OperationFailedException
+ {
+ /* ContactZeroconfImpl contact = new ContactZeroconfImpl(
+ contactIdentifier,
+ parentProvider,
+ null, null, null, 0);
+
+ ((ContactGroupZeroconfImpl)parent).addContact(contact);
+
+ fireSubscriptionEvent(contact,
+ parent,
+ SubscriptionEvent.SUBSCRIPTION_CREATED);
+ //if the newly added contact corresponds to another provider - set their
+ //status accordingly
+ ProtocolProviderServiceZeroconfImpl gibProvider
+ = findProviderForZeroconfUserID(contactIdentifier);
+ if(gibProvider != null)
+ {
+ OperationSetPersistentPresence opSetPresence
+ = (OperationSetPersistentPresence)gibProvider.getOperationSet(
+ OperationSetPersistentPresence.class);
+
+ changePresenceStatusForContact(
+ contact
+ , (ZeroconfStatusEnum)opSetPresence.getPresenceStatus());
+ }
+ else
+ {
+ //otherwise - since we are not a real protocol, we set the contact
+ //presence status ourselves
+ changePresenceStatusForContact(contact, getPresenceStatus());
+ }
+
+ //notify presence listeners for the status change.
+ fireContactPresenceStatusChangeEvent(contact
+ , parent
+ , ZeroconfStatusEnum.OFFLINE);
+ */}
+
+
+
+ /**
+ * Adds a subscription for the presence status of the contact
+ * corresponding to the specified contactIdentifier.
+ *
+ * @param contactIdentifier the identifier of the contact whose status
+ * updates we are subscribing for. <p>
+ * @throws IllegalArgumentException if <tt>contact</tt> is not a contact
+ * known to the underlying protocol provider
+ * @throws IllegalStateException if the underlying protocol provider is
+ * not registered/signed on a public service.
+ * @throws OperationFailedException with code NETWORK_FAILURE if
+ * subscribing fails due to errors experienced during network
+ * communication
+ */
+ public void subscribe(String contactIdentifier)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ OperationFailedException
+ {
+ subscribe(contactListRoot, contactIdentifier);
+ }
+
+ /**
+ * Removes a subscription for the presence status of the specified
+ * contact.
+ *
+ * @param contact the contact whose status updates we are unsubscribing
+ * from.
+ * @throws IllegalArgumentException if <tt>contact</tt> is not a contact
+ * known to the underlying protocol provider
+ * @throws IllegalStateException if the underlying protocol provider is
+ * not registered/signed on a public service.
+ * @throws OperationFailedException with code NETWORK_FAILURE if
+ * unsubscribing fails due to errors experienced during network
+ * communication
+ */
+ public void unsubscribe(Contact contact)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ OperationFailedException
+ {
+ String name = contact.getAddress();
+
+ ContactGroupZeroconfImpl parentGroup
+ = (ContactGroupZeroconfImpl)((ContactZeroconfImpl)contact)
+ .getParentContactGroup();
+
+ //parentGroup.removeContact((ContactZeroconfImpl)contact);
+
+ BonjourService service =
+ ((ProtocolProviderServiceZeroconfImpl)contact.getProtocolProvider())
+ .getBonjourService();
+ //TODO: better check with IP
+ service.removeContact(name,null);
+
+ fireSubscriptionEvent((ContactZeroconfImpl)contact,
+ ((ContactZeroconfImpl)contact).getParentContactGroup()
+ , SubscriptionEvent.SUBSCRIPTION_REMOVED);
+ }
+
+ /**
+ * Creates and returns a unresolved contact from the specified
+ * <tt>address</tt> and <tt>persistentData</tt>. The method will not try
+ * to establish a network connection and resolve the newly created Contact
+ * against the server. The protocol provider may will later try and resolve
+ * the contact. When this happens the corresponding event would notify
+ * interested subscription listeners.
+ *
+ * @param address an identifier of the contact that we'll be creating.
+ * @param persistentData a String returned Contact's getPersistentData()
+ * method during a previous run and that has been persistently stored
+ * locally.
+ * @return the unresolved <tt>Contact</tt> created from the specified
+ * <tt>address</tt> and <tt>persistentData</tt>
+ */
+ public Contact createUnresolvedContact(String address,
+ String persistentData)
+ {
+ return createUnresolvedContact(address
+ , persistentData
+ , getServerStoredContactListRoot());
+ }
+
+ /**
+ * Creates and returns a unresolved contact from the specified
+ * <tt>address</tt> and <tt>persistentData</tt>. The method will not try
+ * to establish a network connection and resolve the newly created Contact
+ * against the server. The protocol provider may will later try and resolve
+ * the contact. When this happens the corresponding event would notify
+ * interested subscription listeners.
+ *
+ * @param address an identifier of the contact that we'll be creating.
+ * @param persistentData a String returned Contact's getPersistentData()
+ * method during a previous run and that has been persistently stored
+ * locally.
+ * @param parent the group where the unresolved contact is
+ * supposed to belong to.
+ *
+ * @return the unresolved <tt>Contact</tt> created from the specified
+ * <tt>address</tt> and <tt>persistentData</tt>
+ */
+ public Contact createUnresolvedContact(String address,
+ String persistentData,
+ ContactGroup parent)
+ {
+ return null;
+ }
+
+ /**
+ * Looks for a zeroconf protocol provider registered for a user id matching
+ * <tt>zeroconfUserID</tt>.
+ *
+ * @param zeroconfUserID the ID of the Zeroconf user whose corresponding
+ * protocol provider we'd like to find.
+ * @return ProtocolProviderServiceZeroconfImpl a zeroconf protocol
+ * provider registered for a user with id <tt>zeroconfUserID</tt> or null
+ * if there is no such protocol provider.
+ */
+ public ProtocolProviderServiceZeroconfImpl
+ findProviderForZeroconfUserID(String zeroconfUserID)
+ {
+ BundleContext bc = ZeroconfActivator.getBundleContext();
+
+ String osgiQuery = "(&"
+ + "(" + ProtocolProviderFactory.PROTOCOL
+ + "=Zeroconf)"
+ + "(" + ProtocolProviderFactory.USER_ID
+ + "=" + zeroconfUserID + ")"
+ + ")";
+
+ ServiceReference[] refs = null;
+ try
+ {
+ refs = bc.getServiceReferences(
+ ProtocolProviderService.class.getName()
+ ,osgiQuery);
+ }
+ catch (InvalidSyntaxException ex)
+ {
+ logger.error("Failed to execute the following osgi query: "
+ + osgiQuery
+ , ex);
+ }
+
+ if(refs != null && refs.length > 0)
+ {
+ return (ProtocolProviderServiceZeroconfImpl)bc.getService(refs[0]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Looks for zeroconf protocol providers that have added us to their
+ * contact list and returns list of all contacts representing us in these
+ * providers.
+ *
+ * @return a list of all contacts in other providers' contact lists that
+ * point to us.
+ */
+ public List findContactsPointingToUs()
+ {
+ List contacts = new LinkedList();
+ BundleContext bc = ZeroconfActivator.getBundleContext();
+
+ String osgiQuery =
+ "(" + ProtocolProviderFactory.PROTOCOL
+ + "=Zeroconf)";
+
+ ServiceReference[] refs = null;
+ try
+ {
+ refs = bc.getServiceReferences(
+ ProtocolProviderService.class.getName()
+ ,osgiQuery);
+ }
+ catch (InvalidSyntaxException ex)
+ {
+ logger.error("Failed to execute the following osgi query: "
+ + osgiQuery
+ , ex);
+ }
+
+ for (int i =0; refs != null && i < refs.length; i++)
+ {
+ ProtocolProviderServiceZeroconfImpl gibProvider
+ = (ProtocolProviderServiceZeroconfImpl)bc.getService(refs[i]);
+
+ OperationSetPersistentPresenceZeroconfImpl opSetPersPresence
+ = (OperationSetPersistentPresenceZeroconfImpl)gibProvider
+ .getOperationSet(OperationSetPersistentPresence.class);
+
+ Contact contact = opSetPersPresence.findContactByID(
+ parentProvider.getAccountID().getUserID());
+
+ if (contact != null)
+ contacts.add(contact);
+ }
+
+ return contacts;
+ }
+
+
+ /**
+ * Creates and returns a unresolved contact group from the specified
+ * <tt>address</tt> and <tt>persistentData</tt>. The method will not try
+ * to establish a network connection and resolve the newly created
+ * <tt>ContactGroup</tt> against the server or the contact itself. The
+ * protocol provider will later resolve the contact group. When this happens
+ * the corresponding event would notify interested subscription listeners.
+ *
+ * @param groupUID an identifier, returned by ContactGroup's getGroupUID,
+ * that the protocol provider may use in order to create the group.
+ * @param persistentData a String returned ContactGroups's
+ * getPersistentData() method during a previous run and that has been
+ * persistently stored locally.
+ * @param parentGroup the group under which the new group is to be created
+ * or null if this is group directly underneath the root.
+ * @return the unresolved <tt>ContactGroup</tt> created from the specified
+ * <tt>uid</tt> and <tt>persistentData</tt>
+ */
+ public ContactGroup createUnresolvedContactGroup(String groupUID,
+ String persistentData, ContactGroup parentGroup)
+ {
+ ContactGroupZeroconfImpl newGroup
+ = new ContactGroupZeroconfImpl(
+ ContactGroupZeroconfImpl.createNameFromUID(groupUID)
+ , parentProvider);
+ newGroup.setResolved(false);
+
+ //if parent is null then we're adding under root.
+ if(parentGroup == null)
+ parentGroup = getServerStoredContactListRoot();
+
+ ((ContactGroupZeroconfImpl)parentGroup).addSubgroup(newGroup);
+
+ this.fireServerStoredGroupEvent(
+ newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
+
+ return newGroup;
+ }
+
+ private class UnregistrationListener
+ implements RegistrationStateChangeListener
+ {
+ /**
+ * The method is called by a ProtocolProvider implementation whenver
+ * a change in the registration state of the corresponding provider had
+ * occurred. The method is particularly interested in events stating
+ * that the zeroconf provider has unregistered so that it would fire
+ * status change events for all contacts in our buddy list.
+ *
+ * @param evt ProviderStatusChangeEvent the event describing the status
+ * change.
+ */
+ public void registrationStateChanged(RegistrationStateChangeEvent evt)
+ {
+
+ logger.debug("ZEROCONF : The Zeroconf provider changed state from: "
+ + evt.getOldState()
+ + " to: " + evt.getNewState());
+
+ //send event notifications saying that all our buddies are
+ //offline. The Zeroconf protocol does not implement top level buddies
+ //nor subgroups for top level groups so a simple nested loop
+ //would be enough.
+ Iterator groupsIter = getServerStoredContactListRoot()
+ .subgroups();
+ while (groupsIter.hasNext())
+ {
+ ContactGroupZeroconfImpl group
+ = (ContactGroupZeroconfImpl) groupsIter.next();
+
+ Iterator contactsIter = group.contacts();
+
+ while (contactsIter.hasNext())
+ {
+ ContactZeroconfImpl contact
+ = (ContactZeroconfImpl) contactsIter.next();
+
+ PresenceStatus oldContactStatus
+ = contact.getPresenceStatus();
+
+ /* We set contacts to OFFLINE and send an event so that external listeners
+ * can be aware that the contacts are reachable anymore. Dunno if that's
+ * a good idea. Can be erased if not. Contacts clean is directly done by the
+ * contact status change handler.
+ */
+ if (!oldContactStatus.isOnline())
+ {
+ //contact.setPresenceStatus(ZeroconfStatusEnum.OFFLINE);
+ fireContactPresenceStatusChangeEvent(
+ contact
+ , contact.getParentContactGroup()
+ , oldContactStatus);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the volatile group or null if this group has not yet been
+ * created.
+ *
+ * @return a volatile group existing in our contact list or <tt>null</tt>
+ * if such a group has not yet been created.
+ */
+ public ContactGroupZeroconfImpl getNonPersistentGroup()
+ {
+ for (int i = 0
+ ; i < getServerStoredContactListRoot().countSubgroups()
+ ; i++)
+ {
+ ContactGroupZeroconfImpl gr =
+ (ContactGroupZeroconfImpl)getServerStoredContactListRoot()
+ .getGroup(i);
+
+ if(!gr.isPersistent())
+ return gr;
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Creates a non persistent contact for the specified address. This would
+ * also create (if necessary) a group for volatile contacts that would not
+ * be added to the server stored contact list. This method would have no
+ * effect on the server stored contact list.
+ * @return the newly created volatile contact.
+ * @param bonjourService BonjourService responsible for the chat with this contact
+ * @param name Display name of the contact
+ * @param ip IP address of the contact
+ * @param port Port declared by the contact for direct chat
+ * @param contactAddress the address of the volatile contact we'd like to
+ * create.
+ */
+ public ContactZeroconfImpl createVolatileContact(String contactAddress,
+ BonjourService bonjourService,
+ String name,
+ InetAddress ip,
+ int port)
+ {
+ //First create the new volatile contact;
+ ContactZeroconfImpl newVolatileContact
+ = new ContactZeroconfImpl(contactAddress,
+ this.parentProvider, bonjourService, name, ip, port);
+ newVolatileContact.setPersistent(false);
+
+
+ //Check whether a volatile group already exists and if not create
+ //one
+ ContactGroupZeroconfImpl theVolatileGroup = getNonPersistentGroup();
+
+
+ //if the parent volatile group is null then we create it
+ if (theVolatileGroup == null)
+ {
+ List emptyBuddies = new LinkedList();
+ theVolatileGroup = new ContactGroupZeroconfImpl(
+ "Bonjour"
+ , parentProvider);
+ theVolatileGroup.setResolved(false);
+ theVolatileGroup.setPersistent(false);
+
+ this.contactListRoot.addSubgroup(theVolatileGroup);
+
+ fireServerStoredGroupEvent(theVolatileGroup
+ , ServerStoredGroupEvent.GROUP_CREATED_EVENT);
+ }
+
+ //now add the volatile contact instide it
+ theVolatileGroup.addContact(newVolatileContact);
+ fireSubscriptionEvent(newVolatileContact
+ , theVolatileGroup
+ , SubscriptionEvent.SUBSCRIPTION_CREATED);
+
+ return newVolatileContact;
+ }
+
+ public Contact getLocalContact()
+ {
+ return null;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetTypingNotificationsZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetTypingNotificationsZeroconfImpl.java
new file mode 100644
index 0000000..0fcfc9b
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/OperationSetTypingNotificationsZeroconfImpl.java
@@ -0,0 +1,164 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Implements typing notifications for the Zeroconf protocol. The operation
+ * set would simply mirror all outgoing typing notifications and make them
+ * appear as incoming events generated by the contact that we are currently
+ * writing a message to.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ * @author Jonathan Martin
+ */
+public class OperationSetTypingNotificationsZeroconfImpl
+ implements OperationSetTypingNotifications
+{
+ private static final Logger logger =
+ Logger.getLogger(OperationSetTypingNotificationsZeroconfImpl.class);
+
+ /**
+ * All currently registered TN listeners.
+ */
+ private List typingNotificationsListeners = new ArrayList();
+
+ /**
+ * The provider that created us.
+ */
+ private ProtocolProviderServiceZeroconfImpl parentProvider = null;
+
+ /**
+ * The currently valid persistent presence operation set..
+ */
+ private OperationSetPersistentPresenceZeroconfImpl opSetPersPresence = null;
+
+
+ /**
+ * Creates a new instance of this operation set and keeps the parent
+ * provider as a reference.
+ *
+ * @param provider a ref to the <tt>ProtocolProviderServiceImpl</tt>
+ * that created us and that we'll use for retrieving the underlying aim
+ * connection.
+ * @param opSetPersPresence the currently valid
+ * <tt>OperationSetPersistentPresenceZeroconfImpl</tt> instance.
+ */
+ OperationSetTypingNotificationsZeroconfImpl(
+ ProtocolProviderServiceZeroconfImpl provider,
+ OperationSetPersistentPresenceZeroconfImpl opSetPersPresence)
+ {
+ this.parentProvider = provider;
+ this.opSetPersPresence = opSetPersPresence;
+ }
+
+ /**
+ * Adds <tt>listener</tt> to the list of listeners registered for receiving
+ * <tt>TypingNotificationEvent</tt>s
+ *
+ * @param listener the <tt>TypingNotificationsListener</tt> listener that
+ * we'd like to add to the list of listeneres registered for receiving
+ * typing notificaions.
+ */
+ public void addTypingNotificationsListener(
+ TypingNotificationsListener listener)
+ {
+ synchronized(typingNotificationsListeners)
+ {
+ typingNotificationsListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes <tt>listener</tt> from the list of listeners registered for
+ * receiving <tt>TypingNotificationEvent</tt>s
+ *
+ * @param listener the <tt>TypingNotificationsListener</tt> listener that
+ * we'd like to remove
+ */
+ public void removeTypingNotificationsListener(
+ TypingNotificationsListener listener)
+ {
+ synchronized(typingNotificationsListeners)
+ {
+ typingNotificationsListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Delivers a <tt>TypingNotificationEvent</tt> to all registered listeners.
+ * @param sourceContact the contact who has sent the notification.
+ * @param evtCode the code of the event to deliver.
+ */
+ public void fireTypingNotificationsEvent(Contact sourceContact
+ ,int evtCode)
+ {
+ logger.debug("Dispatching a TypingNotif. event to "
+ + typingNotificationsListeners.size()+" listeners. Contact "
+ + sourceContact.getAddress() + " has now a typing status of "
+ + evtCode);
+
+ TypingNotificationEvent evt = new TypingNotificationEvent(
+ sourceContact, evtCode);
+
+ Iterator listeners = null;
+ synchronized (typingNotificationsListeners)
+ {
+ listeners = new ArrayList(typingNotificationsListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ TypingNotificationsListener listener
+ = (TypingNotificationsListener) listeners.next();
+
+ logger.debug("ZEROCONF: Sending TypingNotif to Listener " + listener);
+
+ listener.typingNotificationReceifed(evt);
+ }
+ }
+
+ /**
+ * Sends a notification to <tt>notifiedContatct</tt> that we have entered
+ * <tt>typingState</tt>.
+ *
+ * @param notifiedContact the <tt>Contact</tt> to notify
+ * @param typingState the typing state that we have entered.
+ *
+ * @throws java.lang.IllegalStateException if the underlying stack is
+ * not registered and initialized.
+ * @throws java.lang.IllegalArgumentException if <tt>notifiedContact</tt> is
+ * not an instance belonging to the underlying implementation.
+ */
+ public void sendTypingNotification(Contact notifiedContact, int typingState)
+ throws IllegalStateException, IllegalArgumentException
+ {
+ if( !(notifiedContact instanceof ContactZeroconfImpl) )
+ throw new IllegalArgumentException(
+ "The specified contact is not a Zeroconf contact."
+ + notifiedContact);
+
+ ContactZeroconfImpl to = (ContactZeroconfImpl)notifiedContact;
+
+ ClientThread thread = to.getClientThread();
+ if (thread == null) return;/*throw new IllegalStateException(
+ "No communication channel opened to chat with this contact");*/
+
+ if (typingState != STATE_TYPING)
+ return;
+
+ MessageZeroconfImpl message =
+ new MessageZeroconfImpl("",null, MessageZeroconfImpl.TYPING);
+ thread.sendMessage(message);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolIconZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolIconZeroconfImpl.java
new file mode 100644
index 0000000..ad70df1
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolIconZeroconfImpl.java
@@ -0,0 +1,102 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.io.*;
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Reperesents the zeroconf protocol icon. Implements the <tt>ProtocolIcon</tt>
+ * interface in order to provide a zeroconf logo image in two different sizes.
+ *
+ * @author Christian Vincenot
+ * @author Jonathan Martin
+ */
+public class ProtocolIconZeroconfImpl
+ implements ProtocolIcon
+{
+ private static Logger logger
+ = Logger.getLogger(ProtocolIconZeroconfImpl.class);
+
+ /**
+ * A hash table containing the protocol icon in different sizes.
+ */
+ private static Hashtable iconsTable = new Hashtable();
+ static
+ {
+ iconsTable.put(ProtocolIcon.ICON_SIZE_16x16,
+ loadIcon("resources/images/zeroconf/zeroconf-online.png"));
+
+ iconsTable.put(ProtocolIcon.ICON_SIZE_64x64,
+ loadIcon("resources/images/zeroconf/zeroconf-color-64.png"));
+ }
+
+ /**
+ * Implements the <tt>ProtocolIcon.getSupportedSizes()</tt> method. Returns
+ * an iterator to a set containing the supported icon sizes.
+ * @return an iterator to a set containing the supported icon sizes
+ */
+ public Iterator getSupportedSizes()
+ {
+ return iconsTable.keySet().iterator();
+ }
+
+ /**
+ * Returne TRUE if a icon with the given size is supported, FALSE-otherwise.
+ * @param iconSize Icon size
+ * @return True if this size is supported, false otherwise
+ */
+ public boolean isSizeSupported(String iconSize)
+ {
+ return iconsTable.containsKey(iconSize);
+ }
+
+ /**
+ * Returns the icon image in the given size.
+ * @param iconSize the icon size; one of ICON_SIZE_XXX constants
+ * @return Icon image
+ */
+ public byte[] getIcon(String iconSize)
+ {
+ return (byte[])iconsTable.get(iconSize);
+ }
+
+ /**
+ * Returns the icon image used to represent the protocol connecting state.
+ * @return the icon image used to represent the protocol connecting state
+ */
+ public byte[] getConnectingIcon()
+ {
+ return loadIcon("resources/images/zeroconf/zeroconf-online.png");
+ }
+
+ /**
+ * Loads an image from a given image path.
+ * @param imagePath The identifier of the image.
+ * @return The image for the given identifier.
+ */
+ public static byte[] loadIcon(String imagePath)
+ {
+ InputStream is = ProtocolIconZeroconfImpl.class
+ .getClassLoader().getResourceAsStream(imagePath);
+
+ byte[] icon = null;
+ try
+ {
+ icon = new byte[is.available()];
+ is.read(icon);
+ }
+ catch (IOException e)
+ {
+ logger.error("Failed to load icon: " + imagePath, e);
+ }
+ return icon;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderFactoryZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderFactoryZeroconfImpl.java
new file mode 100644
index 0000000..ce7dc87
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderFactoryZeroconfImpl.java
@@ -0,0 +1,285 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+
+import org.osgi.framework.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * The Zeroconf protocol provider factory creates instances of the Zeroconf
+ * protocol provider service. One Service instance corresponds to one account.
+ *
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ProtocolProviderFactoryZeroconfImpl
+ extends ProtocolProviderFactory
+{
+ private static final Logger logger
+ = Logger.getLogger(ProtocolProviderFactoryZeroconfImpl.class);
+
+ /**
+ * The table that we store our accounts in.
+ */
+ private Hashtable registeredAccounts = new Hashtable();
+
+
+ /**
+ * Creates an instance of the ProtocolProviderFactoryZeroconfImpl.
+ */
+ public ProtocolProviderFactoryZeroconfImpl()
+ {
+ super();
+ }
+
+ /**
+ * Returns the ServiceReference for the protocol provider corresponding
+ * to the specified accountID or null if the accountID is unknown.
+ *
+ * @param accountID the accountID of the protocol provider we'd like to
+ * get
+ * @return a ServiceReference object to the protocol provider with the
+ * specified account id and null if the account id is unknwon to the
+ * provider factory.
+ */
+ public ServiceReference getProviderForAccount(AccountID accountID)
+ {
+ ServiceRegistration registration
+ = (ServiceRegistration)registeredAccounts.get(accountID);
+
+ return (registration == null )
+ ? null
+ : registration.getReference();
+ }
+
+ /**
+ * Returns a copy of the list containing the <tt>AccoudID</tt>s of all
+ * accounts currently registered in this protocol provider.
+ *
+ * @return a copy of the list containing the <tt>AccoudID</tt>s of all
+ * accounts currently registered in this protocol provider.
+ */
+ public ArrayList getRegisteredAccounts()
+ {
+ return new ArrayList(registeredAccounts.keySet());
+ }
+
+ /**
+ * Loads (and hence installs) all accounts previously stored in the
+ * configuration service.
+ */
+ public void loadStoredAccounts()
+ {
+ super.loadStoredAccounts( ZeroconfActivator.getBundleContext());
+ }
+
+
+ /**
+ * Initializaed and creates an account corresponding to the specified
+ * accountProperties and registers the resulting ProtocolProvider in the
+ * <tt>context</tt> BundleContext parameter.
+ *
+ * @param userIDStr tha/a user identifier uniquely representing the newly
+ * created account within the protocol namespace.
+ * @param accountProperties a set of protocol (or implementation)
+ * specific properties defining the new account.
+ * @return the AccountID of the newly created account.
+ */
+ public AccountID installAccount( String userIDStr,
+ Map accountProperties)
+ {
+ BundleContext context
+ = ZeroconfActivator.getBundleContext();
+ if (context == null)
+ throw new NullPointerException(
+ "The specified BundleContext was null");
+
+ if (userIDStr == null)
+ throw new NullPointerException(
+ "The specified AccountID was null");
+
+ if (accountProperties == null)
+ throw new NullPointerException(
+ "The specified property map was null");
+
+ accountProperties.put(USER_ID, userIDStr);
+
+ AccountID accountID =
+ new ZeroconfAccountID(userIDStr, accountProperties);
+
+ //make sure we haven't seen this account id before.
+ if (registeredAccounts.containsKey(accountID))
+ throw new IllegalStateException(
+ "An account for id " + userIDStr + " was already installed!");
+
+ //first store the account and only then load it as the load generates
+ //an osgi event, the osgi event triggers (through the UI) a call to the
+ //ProtocolProviderService.register() method and it needs to acces
+ //the configuration service and check for a stored password.
+ this.storeAccount(
+ ZeroconfActivator.getBundleContext()
+ , accountID);
+
+ accountID = loadAccount(accountProperties);
+
+ return accountID;
+ }
+ /**
+ * Initializes and creates an account corresponding to the specified
+ * accountProperties and registers the resulting ProtocolProvider in the
+ * <tt>context</tt> BundleContext parameter.
+ *
+ * @param accountProperties a set of protocol (or implementation)
+ * specific properties defining the new account.
+ * @return the AccountID of the newly loaded account
+ */
+ public AccountID loadAccount( Map accountProperties)
+ {
+ BundleContext context
+ = ZeroconfActivator.getBundleContext();
+ if(context == null)
+ throw new NullPointerException(
+ "The specified BundleContext was null");
+
+ String userIDStr = (String)accountProperties.get(USER_ID);
+
+ AccountID accountID =
+ new ZeroconfAccountID(userIDStr, accountProperties);
+
+ //get a reference to the configuration service and register whatever
+ //properties we have in it.
+
+ Hashtable properties = new Hashtable();
+ properties.put(PROTOCOL, ProtocolNames.ZEROCONF);
+ properties.put(USER_ID, userIDStr);
+
+ ProtocolProviderServiceZeroconfImpl zeroconfProtocolProvider
+ = new ProtocolProviderServiceZeroconfImpl();
+
+ zeroconfProtocolProvider.initialize(userIDStr, accountID);
+
+ ServiceRegistration registration
+ = context.registerService( ProtocolProviderService.class.getName(),
+ zeroconfProtocolProvider,
+ properties);
+
+ registeredAccounts.put(accountID, registration);
+
+ return accountID;
+ }
+
+
+ /**
+ * Removes the specified account from the list of accounts that this
+ * provider factory is handling.
+ *
+ * @param accountID the ID of the account to remove.
+ * @return true if an account with the specified ID existed and was
+ * removed and false otherwise.
+ */
+ public boolean uninstallAccount(AccountID accountID)
+ {
+ //unregister the protocol provider
+ ServiceReference serRef = getProviderForAccount(accountID);
+
+ ProtocolProviderService protocolProvider
+ = (ProtocolProviderService) ZeroconfActivator.getBundleContext()
+ .getService(serRef);
+ if (protocolProvider == null) logger.error("ProtocolProviderService = null !!");
+
+ try
+ {
+ protocolProvider.unregister();
+ }
+ catch (OperationFailedException exc)
+ {
+ logger.error("Failed to unregister protocol provider for account : "
+ + accountID + " caused by : " + exc);
+ }
+
+ ServiceRegistration registration
+ = (ServiceRegistration)registeredAccounts.remove(accountID);
+
+ if(registration == null)
+ return false;
+
+ //kill the service
+ registration.unregister();
+
+ registeredAccounts.remove(accountID );
+
+ return removeStoredAccount(
+ ZeroconfActivator.getBundleContext()
+ , accountID);
+ }
+
+ /**
+ * Saves the password for the specified account after scrambling it a bit
+ * so that it is not visible from first sight (Method remains highly
+ * insecure).
+ *
+ * @param accountID the AccountID for the account whose password we're
+ * storing.
+ * @param passwd the password itself.
+ *
+ * @throws java.lang.IllegalArgumentException if no account corresponding
+ * to <tt>accountID</tt> has been previously stored.
+ */
+ public void storePassword(AccountID accountID, String passwd)
+ throws IllegalArgumentException
+ {
+ super.storePassword(ZeroconfActivator.getBundleContext()
+ , accountID
+ , passwd);
+ }
+
+ /**
+ * Returns the password last saved for the specified account.
+ *
+ * @param accountID the AccountID for the account whose password we're
+ * looking for..
+ *
+ * @return a String containing the password for the specified accountID.
+ *
+ * @throws java.lang.IllegalArgumentException if no account corresponding
+ * to <tt>accountID</tt> has been previously stored.
+ */
+ public String loadPassword(AccountID accountID)
+ throws IllegalArgumentException
+ {
+ return super.loadPassword(ZeroconfActivator.getBundleContext()
+ , accountID );
+ }
+
+ /**
+ * Prepares the factory for bundle shutdown.
+ */
+ public void stop()
+ {
+ Enumeration registrations = this.registeredAccounts.elements();
+
+ while(registrations.hasMoreElements())
+ {
+ ServiceRegistration reg
+ = ((ServiceRegistration)registrations.nextElement());
+
+ reg.unregister();
+ }
+
+ Enumeration idEnum = registeredAccounts.keys();
+
+ while(idEnum.hasMoreElements())
+ {
+ registeredAccounts.remove(idEnum.nextElement());
+ }
+ }
+
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderServiceZeroconfImpl.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderServiceZeroconfImpl.java
new file mode 100644
index 0000000..e9c9a2e
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ProtocolProviderServiceZeroconfImpl.java
@@ -0,0 +1,400 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * An implementation of the protocol provider service over the Zeroconf protocol
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ProtocolProviderServiceZeroconfImpl
+ implements ProtocolProviderService
+{
+ private static final Logger logger =
+ Logger.getLogger(ProtocolProviderServiceZeroconfImpl.class);
+
+ /**
+ * The hashtable with the operation sets that we support locally.
+ */
+ private Hashtable supportedOperationSets = new Hashtable();
+
+ /**
+ * A list of all listeners registered for
+ * <tt>RegistrationStateChangeEvent</tt>s.
+ */
+ private Vector registrationStateListeners = new Vector();
+
+ /**
+ * We use this to lock access to initialization.
+ */
+ private Object initializationLock = new Object();
+
+ /**
+ * The id of the account that this protocol provider represents.
+ */
+ private AccountID accountID = null;
+
+ /**
+ * Indicates whether or not the provider is initialized and ready for use.
+ */
+ private boolean isInitialized = false;
+
+ /**
+ * The logo corresponding to the zeroconf protocol.
+ */
+ private ProtocolIconZeroconfImpl zeroconfIcon
+ = new ProtocolIconZeroconfImpl();
+
+ /**
+ * The registration state that we are currently in. Note that in a real
+ * world protocol implementation this field won't exist and the registration
+ * state would be retrieved from the protocol stack.
+ */
+ private RegistrationState currentRegistrationState
+ = RegistrationState.UNREGISTERED;
+
+ /**
+ * The BonjourService corresponding to this ProtocolProviderService
+ */
+
+ private BonjourService bonjourService;
+
+ /**
+ * The default constructor for the Zeroconf protocol provider.
+ */
+ public ProtocolProviderServiceZeroconfImpl()
+ {
+ logger.trace("Creating a zeroconf provider.");
+ }
+
+ /**
+ * Returns the AccountID that uniquely identifies the account represented
+ * by this instance of the ProtocolProviderService.
+ *
+ * @return the id of the account represented by this provider.
+ */
+ public AccountID getAccountID()
+ {
+ return accountID;
+ }
+
+ /**
+ * Returns the Bonjour Service that handles the Bonjour protocol stack.
+ *
+ *@return the Bonjour Service linked with this Protocol Provider
+ */
+ public BonjourService getBonjourService()
+ {
+ return bonjourService;
+ }
+
+
+ /**
+ * Initializes the service implementation, and puts it in a sate where it
+ * could interoperate with other services. It is strongly recomended that
+ * properties in this Map be mapped to property names as specified by
+ * <tt>AccountProperties</tt>.
+ *
+ * @param userID the user id of the zeroconf account we're currently
+ * initializing
+ * @param accountID the identifier of the account that this protocol
+ * provider represents.
+ *
+ * @see net.java.sip.communicator.service.protocol.AccountID
+ */
+ protected void initialize(String userID,
+ AccountID accountID)
+ {
+ synchronized(initializationLock)
+ {
+ this.accountID = accountID;
+
+
+ //initialize the presence operationset
+ OperationSetPersistentPresenceZeroconfImpl persistentPresence =
+ new OperationSetPersistentPresenceZeroconfImpl(this);
+
+ supportedOperationSets.put(
+ OperationSetPersistentPresence.class.getName(),
+ persistentPresence);
+
+
+ //register it once again for those that simply need presence and
+ //won't be smart enough to check for a persistent presence
+ //alternative
+ supportedOperationSets.put( OperationSetPresence.class.getName(),
+ persistentPresence);
+
+ //initialize the IM operation set
+ OperationSetBasicInstantMessagingZeroconfImpl basicInstantMessaging
+ = new OperationSetBasicInstantMessagingZeroconfImpl(
+ this, persistentPresence);
+
+ supportedOperationSets.put(
+ OperationSetBasicInstantMessaging.class.getName(),
+ basicInstantMessaging);
+
+ //initialize the typing notifications operation set
+ OperationSetTypingNotifications typingNotifications =
+ new OperationSetTypingNotificationsZeroconfImpl(
+ this, persistentPresence);
+
+ supportedOperationSets.put(
+ OperationSetTypingNotifications.class.getName(),
+ typingNotifications);
+
+ isInitialized = true;
+
+ }
+ }
+
+ /**
+ * Returns the operation set corresponding to the specified class or null
+ * if this operation set is not supported by the provider implementation.
+ *
+ * @param opsetClass the <tt>Class</tt> of the operation set that we're
+ * looking for.
+ * @return returns an OperationSet of the specified <tt>Class</tt> if
+ * the undelying implementation supports it or null otherwise.
+ */
+ public OperationSet getOperationSet(Class opsetClass)
+ {
+ return (OperationSet) getSupportedOperationSets()
+ .get(opsetClass.getName());
+ }
+
+ /**
+ * Registers the specified listener with this provider so that it would
+ * receive notifications on changes of its state or other properties such
+ * as its local address and display name.
+ *
+ * @param listener the listener to register.
+ */
+ public void addRegistrationStateChangeListener(
+ RegistrationStateChangeListener listener)
+ {
+ synchronized(registrationStateListeners)
+ {
+ if (!registrationStateListeners.contains(listener))
+ registrationStateListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes the specified registration listener so that it won't receive
+ * further notifications when our registration state changes.
+ *
+ * @param listener the listener to remove.
+ */
+ public void removeRegistrationStateChangeListener(
+ RegistrationStateChangeListener listener)
+ {
+ synchronized(registrationStateListeners)
+ {
+ registrationStateListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Creates a <tt>RegistrationStateChangeEvent</tt> corresponding to the
+ * specified old and new states and notifies all currently registered
+ * listeners.
+ *
+ * @param oldState the state that the provider had before the change
+ * occurred
+ * @param newState the state that the provider is currently in.
+ * @param reasonCode a value corresponding to one of the REASON_XXX fields
+ * of the RegistrationStateChangeEvent class, indicating the reason for
+ * this state transition.
+ * @param reason a String further explaining the reason code or null if
+ * no such explanation is necessary.
+ */
+ private void fireRegistrationStateChanged( RegistrationState oldState,
+ RegistrationState newState,
+ int reasonCode,
+ String reason)
+ {
+ RegistrationStateChangeEvent event =
+ new RegistrationStateChangeEvent(
+ this, oldState, newState, reasonCode, reason);
+
+ logger.debug("Dispatching " + event + " to "
+ + registrationStateListeners.size()+ " listeners.");
+
+ Iterator listeners = null;
+ synchronized (registrationStateListeners)
+ {
+ listeners = new ArrayList(registrationStateListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ RegistrationStateChangeListener listener
+ = (RegistrationStateChangeListener) listeners.next();
+
+ listener.registrationStateChanged(event);
+ }
+
+ logger.trace("Done.");
+ }
+
+ /**
+ * Returns the short name of the protocol that the implementation of this
+ * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for
+ * example).
+ *
+ * @return a String containing the short name of the protocol this
+ * service is implementing (most often that would be a name in
+ * ProtocolNames).
+ */
+ public String getProtocolName()
+ {
+ return ProtocolNames.ZEROCONF;
+ }
+
+ /**
+ * Returns the state of the registration of this protocol provider with
+ * the corresponding registration service.
+ *
+ * @return ProviderRegistrationState
+ */
+ public RegistrationState getRegistrationState()
+ {
+ return currentRegistrationState;
+ }
+
+ /**
+ * Returns an array containing all operation sets supported by the
+ * current implementation.
+ *
+ * @return a java.util.Map containing instance of all supported
+ * operation sets mapped against their class names (e.g.
+ * OperationSetPresence.class.getName()) .
+ */
+ public Map getSupportedOperationSets()
+ {
+ //Copy the map so that the caller is not able to modify it.
+ return (Map)supportedOperationSets.clone();
+ }
+
+ /**
+ * Indicates whether or not this provider is registered
+ *
+ * @return true if the provider is currently registered and false
+ * otherwise.
+ */
+ public boolean isRegistered()
+ {
+ return currentRegistrationState.equals(RegistrationState.REGISTERED);
+ }
+
+ /**
+ * Starts the registration process.
+ *
+ * @param authority the security authority that will be used for
+ * resolving any security challenges that may be returned during the
+ * registration or at any moment while wer're registered.
+ * @throws OperationFailedException with the corresponding code it the
+ * registration fails for some reason (e.g. a networking error or an
+ * implementation problem).
+ */
+ public void register(SecurityAuthority authority)
+ throws OperationFailedException
+ {
+ //we don't need a password here since there's no server in
+ //zeroconf.
+
+ RegistrationState oldState = currentRegistrationState;
+ currentRegistrationState = RegistrationState.REGISTERED;
+
+
+ //ICI : creer le service Zeroconf !!
+ logger.info("ZEROCONF: Starting the service");
+ ZeroconfAccountID acc = (ZeroconfAccountID)accountID;
+ this.bonjourService = new BonjourService(5298, this);
+
+ //bonjourService.changeStatus(ZeroconfStatusEnum.ONLINE);
+
+ fireRegistrationStateChanged(
+ oldState
+ , currentRegistrationState
+ , RegistrationStateChangeEvent.REASON_USER_REQUEST
+ , null);
+ }
+
+ /**
+ * Makes the service implementation close all open sockets and release
+ * any resources that it might have taken and prepare for
+ * shutdown/garbage collection.
+ */
+ public void shutdown()
+ {
+ if(!isInitialized)
+ {
+ return;
+ }
+ logger.trace("Killing the Zeroconf Protocol Provider.");
+
+ if(isRegistered())
+ {
+ try
+ {
+ //do the unregistration
+ unregister();
+ }
+ catch (OperationFailedException ex)
+ {
+ //we're shutting down so we need to silence the exception here
+ logger.error(
+ "Failed to properly unregister before shutting down. "
+ + getAccountID()
+ , ex);
+ }
+ }
+
+ isInitialized = false;
+ }
+
+ /**
+ * Ends the registration of this protocol provider with the current
+ * registration service.
+ *
+ * @throws OperationFailedException with the corresponding code it the
+ * registration fails for some reason (e.g. a networking error or an
+ * implementation problem).
+ */
+ public void unregister()
+ throws OperationFailedException
+ {
+ RegistrationState oldState = currentRegistrationState;
+ currentRegistrationState = RegistrationState.UNREGISTERED;
+
+ bonjourService.shutdown();
+
+ fireRegistrationStateChanged(
+ oldState
+ , currentRegistrationState
+ , RegistrationStateChangeEvent.REASON_USER_REQUEST
+ , null);
+ }
+
+ /**
+ * Returns the zeroconf protocol icon.
+ * @return the zeroconf protocol icon
+ */
+ public ProtocolIcon getProtocolIcon()
+ {
+ return zeroconfIcon;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfAccountID.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfAccountID.java
new file mode 100644
index 0000000..0901540
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfAccountID.java
@@ -0,0 +1,84 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * The Zeroconf implementation of a sip-communicator AccountID
+ *
+ * @author Christian Vincenot
+ */
+public class ZeroconfAccountID
+ extends AccountID
+{
+ /* Firstname, lastname, mail address */
+ String first = null;
+ String last = null;
+ String mail = null;
+
+ private boolean rememberContacts = false;
+
+ /**
+ * Creates a zeroconf account id from the specified id and account
+ * properties.
+ * @param userID id identifying this account
+ * @param accountProperties any other properties necessary for the account.
+ */
+ ZeroconfAccountID(String userID, Map accountProperties)
+ {
+ super(userID
+ , accountProperties
+ , "Zeroconf"
+ , "zeroconf.org");
+
+ first = (String)accountProperties.get("first");
+ last = (String)accountProperties.get("last");
+ mail = (String)accountProperties.get("mail");
+
+ rememberContacts =
+ new Boolean((String)accountProperties.get("rememberContacts"))
+ .booleanValue();
+ }
+
+ /**
+ * Returns a String representing the firstname of this user.
+ * @return String representing the firstname of this user.
+ */
+ public String getFirst()
+ {
+ return first;
+ }
+
+ /**
+ * Returns a String representing the lastname of this user.
+ * @return String representing the lastname of this user.
+ */
+ public String getLast()
+ {
+ return last;
+ }
+
+ /**
+ * Returns a String representing the mail address of this user.
+ * @return String representing the mail address of this user.
+ */
+ public String getMail()
+ {
+ return mail;
+ }
+
+ /**
+ * Returns a boolean indicating if we store the contacts we meet or not.
+ * @return boolean indicating if we store the contacts we meet or not.
+ */
+ public boolean isRememberContacts()
+ {
+ return rememberContacts;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfActivator.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfActivator.java
new file mode 100644
index 0000000..e25ba29
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfActivator.java
@@ -0,0 +1,120 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+import org.osgi.framework.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * Loads the Zeroconf provider factory and registers its services in the OSGI
+ * bundle context.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ZeroconfActivator
+ implements BundleActivator
+{
+ private static final Logger logger
+ = Logger.getLogger(ZeroconfActivator.class);
+
+ /**
+ * A reference to the registration of our Zeroconf protocol provider
+ * factory.
+ */
+ private ServiceRegistration zeroconfPpFactoryServReg = null;
+
+ /**
+ * A reference to the Zeroconf protocol provider factory.
+ */
+ private static ProtocolProviderFactoryZeroconfImpl
+ zeroconfProviderFactory = null;
+
+ /**
+ * The currently valid bundle context.
+ */
+ private static BundleContext bundleContext = null;
+
+
+ /**
+ * Called when this bundle is started. In here we'll export the
+ * zeroconf ProtocolProviderFactory implementation so that it could be
+ * possible to register accounts with it in SIP Communicator.
+ *
+ * @param context The execution context of the bundle being started.
+ * @throws Exception If this method throws an exception, this bundle is
+ * marked as stopped and the Framework will remove this bundle's
+ * listeners, unregister all services registered by this bundle, and
+ * release all services used by this bundle.
+ */
+ public void start(BundleContext context)
+ throws Exception
+ {
+ logger.setLevelAll();
+
+ this.bundleContext = context;
+
+ Hashtable hashtable = new Hashtable();
+ hashtable.put(ProtocolProviderFactory.PROTOCOL, "Zeroconf");
+
+ zeroconfProviderFactory = new ProtocolProviderFactoryZeroconfImpl();
+
+ //load all stored Zeroconf accounts.
+ zeroconfProviderFactory.loadStoredAccounts();
+
+ //register the zeroconf provider factory.
+ zeroconfPpFactoryServReg = context.registerService(
+ ProtocolProviderFactory.class.getName(),
+ zeroconfProviderFactory,
+ hashtable);
+
+ logger.info("Zeroconf protocol implementation [STARTED].");
+ }
+
+ /**
+ * Returns a reference to the bundle context that we were started with.
+ * @return a reference to the BundleContext instance that we were started
+ * witn.
+ */
+ public static BundleContext getBundleContext()
+ {
+ return bundleContext;
+ }
+
+ /**
+ * Retrurns a reference to the protocol provider factory that we have
+ * registered.
+ * @return a reference to the <tt>ProtocolProviderFactoryJabberImpl</tt>
+ * instance that we have registered from this package.
+ */
+ public static ProtocolProviderFactoryZeroconfImpl getProtocolProviderFactory()
+ {
+ return zeroconfProviderFactory;
+ }
+
+
+ /**
+ * Called when this bundle is stopped so the Framework can perform the
+ * bundle-specific activities necessary to stop the bundle.
+ *
+ * @param context The execution context of the bundle being stopped.
+ * @throws Exception If this method throws an exception, the bundle is
+ * still marked as stopped, and the Framework will remove the bundle's
+ * listeners, unregister all services registered by the bundle, and
+ * release all services used by the bundle.
+ */
+ public void stop(BundleContext context)
+ throws Exception
+ {
+ this.zeroconfProviderFactory.stop();
+ zeroconfPpFactoryServReg.unregister();
+
+ logger.info("Zeroconf protocol implementation [STOPPED].");
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfStatusEnum.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfStatusEnum.java
new file mode 100644
index 0000000..b22488f
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/ZeroconfStatusEnum.java
@@ -0,0 +1,149 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.protocol.zeroconf;
+
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+import java.io.*;
+
+/**
+ * An implementation of <tt>PresenceStatus</tt> that enumerates all states that
+ * a Zeroconf contact can fall into.
+ *
+ * @author Christian Vincenot
+ * @author Jonathan Martin
+ */
+public class ZeroconfStatusEnum
+ extends PresenceStatus
+{
+ private static final Logger logger
+ = Logger.getLogger(ZeroconfStatusEnum.class);
+
+ /**
+ * Indicates an Offline status or status with 0 connectivity.
+ */
+ public static final ZeroconfStatusEnum OFFLINE
+ = new ZeroconfStatusEnum(
+ 0
+ , "Offline"
+ , loadIcon("resources/images/zeroconf/zeroconf-offline.png"));
+
+ /**
+ * The DND status. Indicates that the user has connectivity but prefers
+ * not to be contacted.
+ */
+ public static final ZeroconfStatusEnum DO_NOT_DISTURB
+ = new ZeroconfStatusEnum(
+ 30
+ ,"dnd",//, "Do Not Disturb",
+ loadIcon("resources/images/zeroconf/zeroconf-dnd.png"));
+
+ /**
+ * The Invisible status. Indicates that the user has connectivity even
+ * though it may appear otherwise to others, to whom she would appear to be
+ * offline.
+ */
+ public static final ZeroconfStatusEnum INVISIBLE
+ = new ZeroconfStatusEnum(
+ 45
+ , "Invisible"
+ , loadIcon( "resources/images/zeroconf/zeroconf-invisible.png"));
+
+ /**
+ * The Online status. Indicate that the user is able and willing to
+ * communicate.
+ */
+ public static final ZeroconfStatusEnum ONLINE
+ = new ZeroconfStatusEnum(
+ 65
+ ,"avail"//, "Online"
+ , loadIcon("resources/images/zeroconf/zeroconf-online.png"));
+
+
+ /**
+ * Initialize the list of supported status states.
+ */
+ private static List supportedStatusSet = new LinkedList();
+ static
+ {
+ supportedStatusSet.add(OFFLINE);
+ supportedStatusSet.add(DO_NOT_DISTURB);
+
+ /* INVISIBLE STATUS could be supported by unregistering JmDNS and
+ * accepting unknown contacts' messages */
+ //supportedStatusSet.add(INVISIBLE);
+
+ supportedStatusSet.add(ONLINE);
+ }
+
+ /**
+ * Creates an instance of <tt>ZeroconfPresneceStatus</tt> with the
+ * specified parameters.
+ * @param status the connectivity level of the new presence status instance
+ * @param statusName the name of the presence status.
+ * @param statusIcon the icon associated with this status
+ */
+ private ZeroconfStatusEnum(int status,
+ String statusName,
+ byte[] statusIcon)
+ {
+ super(status, statusName, statusIcon);
+ }
+
+ /**
+ * Returns an iterator over all status instances supproted by the zeroconf
+ * provider.
+ * @return an <tt>Iterator</tt> over all status instances supported by the
+ * zeroconf provider.
+ */
+ static Iterator supportedStatusSet()
+ {
+ return supportedStatusSet.iterator();
+ }
+
+ /**
+ * @param status String representation of the status
+ * @return ZeroconfStatusEnum corresponding the supplied String value
+ */
+ static ZeroconfStatusEnum statusOf(String status)
+ {
+ Iterator statusIter = supportedStatusSet();
+ while (statusIter.hasNext())
+ {
+ ZeroconfStatusEnum state = (ZeroconfStatusEnum)statusIter.next();
+ if (state.statusName.equalsIgnoreCase(status))
+ return state;
+ }
+ return null;
+ }
+
+ /**
+ * Loads an image from a given image path.
+ * @param imagePath The path to the image resource.
+ * @return The image extracted from the resource at the specified path.
+ */
+ public static byte[] loadIcon(String imagePath)
+ {
+ InputStream is = ZeroconfStatusEnum.class.getClassLoader()
+ .getResourceAsStream(imagePath);
+
+ byte[] icon = null;
+ try
+ {
+ icon = new byte[is.available()];
+ is.read(icon);
+ }
+ catch (IOException exc)
+ {
+ logger.error("Failed to load icon: " + imagePath, exc);
+ }
+ return icon;
+ }
+
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSCache.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSCache.java
new file mode 100644
index 0000000..2e24064
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSCache.java
@@ -0,0 +1,277 @@
+//Copyright 2003-2005 Arthur van Hoff Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * 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;
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ this.logger.setLevel(Level.parse(SLevel));
+ }
+
+ public CacheNode next()
+ {
+ return next;
+ }
+
+ public DNSEntry getValue()
+ {
+ return value;
+ }
+ }
+
+
+ /**
+ * Create a table with a given initial size.
+ * @param size initial size.
+ */
+ public DNSCache(final int size)
+ {
+ hashtable = new HashMap(size);
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public synchronized void clear()
+ {
+ hashtable.clear();
+ size = 0;
+ }
+
+ /**
+ * Adds an entry to the table.
+ * @param entry added 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.
+ * @param entry removed from table.
+ * @return 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).
+ * @param entry to be found in table.
+ * @return 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.
+ * @param name
+ * @param type
+ * @param clazz
+ * @return Return the entry if found, null otherwise.
+ */
+ 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.
+ * @return Returns iterator with instances of DNSCache.CacheNode.
+ */
+ public Iterator iterator()
+ {
+ return Collections.unmodifiableCollection(hashtable.values()).iterator();
+ }
+
+ /**
+ * Iterate only over items with matching name.
+ * 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.
+ * @param name to be found.
+ * @return Returns an instance of DNSCache.CacheNode or null.
+ */
+ 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)
+ {
+ logger.info(n.value.toString());
+ }
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSConstants.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSConstants.java
new file mode 100644
index 0000000..a6677e3
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSConstants.java
@@ -0,0 +1,147 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Modified by Christian Vincenot for SIP Communicator
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+/**
+ * DNS constants.
+ *
+ * @version %I%, %G%
+ * @author Arthur van Hoff, Jeff Sonstein,
+ * Werner Randelshofer, Pierre Frisch, Rick Blair
+ */
+public 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;
+ // default one hour TTL
+ final static int DNS_TTL = 60 * 60;
+ // two hour TTL (draft-cheshire-dnsext-multicastdns.txt ch 13)
+ // final static int DNS_TTL = 120 * 60;
+
+ 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
+
+ public final static int FLAGS_AA = 0x0400; // Authorative answer
+ final static int FLAGS_TC = 0x0200; // Truncated
+ final static int FLAGS_RD = 0x0100; // Recursion desired
+ public 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 Internet
+ public final static int CLASS_IN = 1;
+ // CSNET
+ final static int CLASS_CS = 2;
+ // CHAOS
+ final static int CLASS_CH = 3;
+ // Hesiod
+ final static int CLASS_HS = 4;
+ // Used in DNS UPDATE [RFC 2136]
+ final static int CLASS_NONE = 254;
+ // Not a DNS class, but a DNS query class, meaning "all classes"
+ final static int CLASS_ANY = 255;
+ // Multicast DNS uses the bottom 15 bits to identify the record class...
+ final static int CLASS_MASK = 0x7FFF;
+ // ... and the top bit indicates that all other cached records are now invalid
+ public final static int CLASS_UNIQUE = 0x8000;
+
+ final static int TYPE_IGNORE = 0; // This is a hack to stop further processing
+ public 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
+ public 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]
+ public 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
+
+ //milliseconds before send shared query
+ final static int SHARED_QUERY_TIME = 20;
+ //milliseconds between query loops.
+ final static int QUERY_WAIT_INTERVAL = 225;
+ //milliseconds between probe loops.
+ final static int PROBE_WAIT_INTERVAL = 250;
+ //minimal wait interval for response.
+ final static int RESPONSE_MIN_WAIT_INTERVAL = 20;
+ //maximal wait interval for response
+ final static int RESPONSE_MAX_WAIT_INTERVAL = 115;
+ //milliseconds to wait after conflict.
+ final static int PROBE_CONFLICT_INTERVAL = 1000;
+ //After x tries go 1 time a sec. on probes.
+ final static int PROBE_THROTTLE_COUNT = 10;
+ //We only increment the throttle count, if
+ // the previous increment is inside this interval.
+ final static int PROBE_THROTTLE_COUNT_INTERVAL = 5000;
+ //milliseconds between Announce loops.
+ final static int ANNOUNCE_WAIT_INTERVAL = 1000;
+ //milliseconds between cache cleanups.
+ final static int RECORD_REAPER_INTERVAL = 10000;
+
+ final static int KNOWN_ANSWER_TTL = 120;
+ // 50% of the TTL in milliseconds
+ final static int ANNOUNCED_RENEWAL_TTL_INTERVAL = DNS_TTL * 500;
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSEntry.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSEntry.java
new file mode 100644
index 0000000..54e31f1
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSEntry.java
@@ -0,0 +1,167 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Modified by Christian Vincenot for SIP Communicator
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.logging.*;
+
+/**
+ * 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
+ */
+public 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * 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;
+ }
+
+ public int getClazz()
+ {
+ return clazz;
+ }
+
+
+ public boolean isUnique()
+ {
+ return unique;
+ }
+
+ /**
+ * 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 + "]" : "]");
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSIncoming.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSIncoming.java
new file mode 100644
index 0000000..f5d8a56
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSIncoming.java
@@ -0,0 +1,507 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.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, Daniel Bobbert
+ */
+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
+ {
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+
+ 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)
+ {
+ //System.out.println("JMDNS received "+n+" answers!");
+ 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:
+ //System.out.println("JMDNS: One is a SRV field!!");
+ 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())
+ {
+ if (that.numQuestions > 0) {
+ if (Collections.EMPTY_LIST.equals(this.questions))
+ this.questions =
+ Collections.synchronizedList(
+ new ArrayList(that.numQuestions));
+
+ 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);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSListener.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSListener.java
new file mode 100644
index 0000000..a17556b
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSListener.java
@@ -0,0 +1,25 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.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.
+ */
+public interface DNSListener
+{
+ /**
+ * Update a DNS record.
+ * @param jmdns
+ * @param now
+ * @param record
+ */
+ public void updateRecord(JmDNS jmdns, long now, DNSRecord record);
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSOutgoing.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSOutgoing.java
new file mode 100644
index 0000000..82e2b06
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSOutgoing.java
@@ -0,0 +1,390 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.io.*;
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * 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)
+ {
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+
+ 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();
+ }
+
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSQuestion.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSQuestion.java
new file mode 100644
index 0000000..c8be332
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSQuestion.java
@@ -0,0 +1,53 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.logging.*;
+
+/**
+ * A DNS question.
+ *
+ * @version %I%, %G%
+ * @author Arthur van Hoff
+ */
+public final class DNSQuestion
+ extends DNSEntry
+{
+ private static Logger logger =
+ Logger.getLogger(DNSQuestion.class.toString());
+
+ /**
+ * Create a question.
+ * @param name
+ * @param type
+ * @param clazz
+ */
+ public DNSQuestion(String name, int type, int clazz)
+ {
+ super(name, type, clazz);
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * 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);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSRecord.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSRecord.java
new file mode 100644
index 0000000..ed48197
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSRecord.java
@@ -0,0 +1,764 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * DNS record
+ *
+ * @version %I%, %G%
+ * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch
+ */
+public 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();
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * True if this record is the same as some other record.
+ * @param other obj to be compared to.
+ */
+ 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(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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ 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);
+ }
+}
+
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSState.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSState.java
new file mode 100644
index 0000000..9a119b4
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/DNSState.java
@@ -0,0 +1,123 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * 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);
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ 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.
+ * @return Returns the next advanced 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.
+ * @return Returns to the next reverted state.
+ */
+ public final DNSState revert()
+ {
+ return (this == CANCELED) ? this : PROBING_1;
+ }
+
+ /**
+ * Returns true, if this is a probing state.
+ * @return 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.
+ * @return 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.
+ * @return 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
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/HostInfo.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/HostInfo.java
new file mode 100644
index 0000000..68691f6
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/HostInfo.java
@@ -0,0 +1,158 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.net.*;
+import java.util.logging.*;
+
+
+/**
+ * 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();
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+
+ 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();
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/JmDNS.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/JmDNS.java
new file mode 100644
index 0000000..467e264
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/JmDNS.java
@@ -0,0 +1,3072 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Modified by Christian Vincenot for SIP Communicator
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.logging.*;
+
+// 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.
+ * @throws java.io.IOException
+ */
+ public JmDNS()
+ throws IOException
+ {
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+
+ logger.finer("JmDNS instance created");
+ try
+ {
+ InetAddress addr = InetAddress.getLocalHost();
+ // [PJYF Oct 14 2004] Why do we disallow the loopback address?
+ init(addr.isLoopbackAddress() ? null : addr, addr.getHostName());
+ }
+ catch (IOException e)
+ {
+ init(null, "computer");
+ }
+ }
+
+ /**
+ * Create an instance of JmDNS and bind it to a
+ * specific network interface given its IP-address.
+ * @param addr
+ * @throws java.io.IOException
+ */
+ 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.
+ * @return Return the HostName associated with this JmDNS instance.
+ */
+ 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.
+ * @return Return the address of the interface to which this instance
+ * of JmDNS is bound.
+ * @throws java.io.IOException
+ */
+ 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
+ * @throws java.io.IOException
+ */
+ 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 type of listener to be removed
+ * @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.
+ * @param info of service
+ * @throws java.io.IOException
+ */
+ 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.
+ * @param info of service
+ */
+ 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.
+ * @param type of service
+ */
+ 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.
+ * @param listener to be added
+ * @param question - which the listener is responsible for.
+ */
+ public 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);
+ }
+
+ //System.out.println("OUT OF MUTEX!!!!!");
+
+ 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;
+
+ logger.finest("JMDNS/handleResponse received " +
+ msg.answers.size()+ " messages");
+ for (Iterator i = msg.answers.iterator(); i.hasNext();)
+ { DNSRecord rec = (DNSRecord)i.next();
+ logger.finest("PRINT: "+ rec);
+ //cache.add(rec);
+ }
+
+ for (Iterator i = msg.answers.iterator(); i.hasNext();)
+ {
+ boolean isInformative = false;
+ DNSRecord rec = (DNSRecord) i.next();
+ boolean expired = rec.isExpired(now);
+
+ logger.finest("JMDNS received : " + rec + " expired: "+expired);
+
+ // update the cache
+ DNSRecord c = (DNSRecord) cache.get(rec);
+ if (c != null)
+ {
+ logger.finest("JMDNS has found "+rec+" in cache");
+ if (expired)
+ {
+ isInformative = true;
+ cache.remove(c);
+ }
+ else
+ {
+ /* Special case for SIP Communicator.
+ * We want to be informed if a cache entry is modified
+ */
+// if ((c.isUnique()
+// && c.getType() == DNSConstants.TYPE_TXT
+// && ((c.getClazz() & DNSConstants.CLASS_IN) != 0)))
+// isInformative = true;
+// c.resetTTL(rec);
+// rec = c;
+ logger.fine(
+ new Boolean(c.isUnique()).toString() +
+ c.getType()+c.getClazz() + "/" +
+ DNSConstants.TYPE_TXT + " "+DNSConstants.CLASS_IN);
+
+ if ((rec.isUnique()
+ && ((rec.getType() & DNSConstants.TYPE_TXT) != 0)
+ && ((rec.getClazz() & DNSConstants.CLASS_IN) != 0)))
+ {
+ System.out.println("UPDATING CACHE !! ");
+ isInformative = true;
+ cache.remove(c);
+ cache.add(rec);
+ }
+ else
+ {
+ c.resetTTL(rec);
+ rec = c;
+ }
+ }
+ }
+ else
+ {
+ if (!expired)
+ {
+ isInformative = true;
+ logger.finest("Adding "+rec+" to the cache");
+ 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);
+ }
+ this.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.
+ this.cancel();
+ }
+ }
+ else
+ {
+ if (state == DNSState.CANCELED)
+ {
+ this.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.
+ this.cancel();
+ }
+ }
+ else
+ {
+ if (state == DNSState.CANCELED)
+ {
+ this.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.
+ this.cancel();
+ removeListener(info);
+ }
+ }
+ else
+ {
+ if (state == DNSState.CANCELED)
+ {
+ this.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.
+ */
+ /* TODO: Clarify whether 2 or 3 announces should be sent. The header says 2,
+ * run() uses the (misleading) ++count < 3 (while all other tasks use count++ < 3)
+ * and the comment in the else block in run() says: "After three successful..."
+ */
+ 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();
+ }
+ this.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()
+ {
+ logger.info("---- cache ----\n");
+ cache.print();
+ logger.info("\n");
+ }
+
+ /**
+ * List Services and serviceTypes.
+ * Debugging Only
+ */
+
+ public void printServices()
+ {
+ logger.info(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)
+ {
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+
+ 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.
+ * @return
+ */
+ 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;
+ }
+ }
+
+ /**
+ * SC-Bonjour Implementation : Method used to update the corresponding DNS
+ * entry in the cache of JmDNS with the new information in this ServiceInfo.
+ * A call to getLocalService must first be issued to get the
+ * ServiceInfo object to be modified.
+ * THIS METHOD MUST BE USED INSTEAD OF ANY DIRECT ACCESS TO JMDNS' CACHE!!
+ * This is used in the implementation of Zeroconf in SIP Communicator
+ * to be able to change fields declared by the local contact (status, etc).
+ * @param info Updated service data to be used to replace the old
+ * stuff contained in JmDNS' cache
+ * @param old info bytes
+ */
+ public void updateInfos(ServiceInfo info, byte[] old)
+ {
+
+ DNSOutgoing out, out2;
+ synchronized (JmDNS.this)
+ {
+ //list = new ArrayList(services.values());
+ services.put(info.getQualifiedName().toLowerCase(), info);
+ }
+
+ synchronized (info)
+ {
+ logger.finer("updateInfos() JmDNS updating " +
+ info.getQualifiedName() + " state " +
+ info.getState());
+
+ out = new DNSOutgoing(
+ /*DNSConstants.FLAGS_QR_RESPONSE*/
+ DNSConstants.FLAGS_RA | DNSConstants.FLAGS_AA);
+ out2 = new DNSOutgoing(
+ /*DNSConstants.FLAGS_QR_RESPONSE*/
+ DNSConstants.FLAGS_RA | DNSConstants.FLAGS_AA);
+
+
+ try
+ {
+ //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_A, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()), 0);
+ //out.addAnswer(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()), 0);
+// out.addAnswer(
+// new DNSRecord.Text(
+// info.getQualifiedName(),
+// DNSConstants.TYPE_TXT,
+// DNSConstants.CLASS_IN ,
+// DNSConstants.DNS_TTL,
+// info.text), 0);
+ out.addAnswer(
+ new DNSRecord.Text(
+ info.getQualifiedName(),
+ DNSConstants.TYPE_TXT,
+ DNSConstants.CLASS_IN ,
+ 0,
+ old), 0);
+ out.addAnswer(
+ new DNSRecord.Text(
+ info.getQualifiedName(),
+ DNSConstants.TYPE_TXT,
+ DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
+ DNSConstants.DNS_TTL,
+ info.text), 0);
+
+ out2.addAnswer(
+ new DNSRecord.Text(
+ info.getQualifiedName(),
+ DNSConstants.TYPE_TXT,
+ DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
+ DNSConstants.DNS_TTL,
+ info.text), 0);
+
+ logger.finer("updateInfos() JmDNS updated infos for "+info);
+
+ send(out);
+ Thread.sleep(1000);
+ send(out2);
+ Thread.sleep(2000);
+ send(out2);
+ }
+ catch( Exception e)
+ {
+ logger.log(Level.WARNING, "", e);
+ }
+ }
+ }
+
+
+ /**
+ * SC-Bonjour Implementation: Method to retrieve the DNS Entry corresponding to a service
+ * that has been declared and return it as a ServiceInfo structure.
+ * It is used in the implementation of Bonjour in SIP Communicator to retrieve the information
+ * concerning the service declared by the local contact. THIS METHOD MUST BE USED INSTEAD OF ANY
+ * LOCAL COPY SAVED BEFORE SERVICE REGISTRATION!!
+ * @return information corresponding to the specified service
+ * @param FQN String representing the Fully Qualified name of the service we want info about
+ */
+ public ServiceInfo getLocalService(String FQN)
+ {
+ return (ServiceInfo)services.get(FQN);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceEvent.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceEvent.java
new file mode 100644
index 0000000..de0395e
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceEvent.java
@@ -0,0 +1,109 @@
+///Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * Returns the JmDNS instance which originated the event.
+ * @return Returns the JmDNS instance which originated the event.
+ */
+ public JmDNS getDNS()
+ {
+ return (JmDNS) getSource();
+ }
+
+ /**
+ * Returns the fully qualified type of the service.
+ * @return 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.
+ * @return Returns the instance name of the service.
+ */
+ 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.
+ * @return Returns the service info record.
+ */
+ 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();
+ }
+
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceInfo.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceInfo.java
new file mode 100644
index 0000000..7523916
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceInfo.java
@@ -0,0 +1,766 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Modified by Christian Vincenot for SIP Communicator
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.logging.*;
+
+/**
+ * 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;
+
+ String SLevel = System.getProperty("jmdns.debug");
+ if (SLevel == null) SLevel = "INFO";
+ logger.setLevel(Level.parse(SLevel));
+ }
+
+ /**
+ * 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
+ */
+ 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> .
+ * @return Returns fully qualified service type name.
+ */
+ public String getType()
+ {
+ return type;
+ }
+
+ /**
+ * Unqualified service instance name,
+ * such as <code>foobar</code> .
+ * @return Returns unqualified service instance name.
+ */
+ 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> .
+ * @return Returns fully qualified service name.
+ */
+ public String getQualifiedName()
+ {
+ return name + "." + type;
+ }
+
+ /**
+ * Get the name of the server.
+ * @return Returns name of the server.
+ */
+ public String getServer()
+ {
+ return server;
+ }
+
+ /**
+ * Get the host address of the service (ie X.X.X.X).
+ * @return Returns host address of the service.
+ */
+ public String getHostAddress()
+ {
+ return (addr != null ? addr.getHostAddress() : "");
+ }
+
+ public InetAddress getAddress()
+ {
+ return addr;
+ }
+
+ /**
+ * Get the InetAddress of the service.
+ * @return Returns the InetAddress of the service.
+ */
+ public InetAddress getInetAddress()
+ {
+ return addr;
+ }
+
+ /**
+ * Get the port for the service.
+ * @return Returns port for the service.
+ */
+ public int getPort()
+ {
+ return port;
+ }
+
+ /**
+ * Get the priority of the service.
+ * @return Returns the priority of the service.
+ */
+ public int getPriority()
+ {
+ return priority;
+ }
+
+ /**
+ * Get the weight of the service.
+ * @return Returns the weight of the service.
+ */
+ public int getWeight()
+ {
+ return weight;
+ }
+
+ /**
+ * Get the text for the serivce as raw bytes.
+ * @return Returns 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.
+ * @return Returns the text for the service.
+ */
+ 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.
+ * @return Returns the URL for this service.
+ */
+ 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.
+ * @param protocol
+ * @return Returns URL for this service.
+ */
+ public String getURL(String protocol)
+ {
+ String url = protocol + "://" + getHostAddress() + ":" + 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.
+ * @param name
+ * @return Returns property of the service as bytes.
+ */
+ 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.
+ * @param name
+ * @return Returns property of the service as string.
+ */
+ public synchronized String getPropertyString(String name)
+ {
+ byte data[] = (byte[]) getProperties().get(name);
+
+ if (data == null)
+ {
+ return null;
+ }
+ if (data == NO_VALUE)
+ {
+ return "true";
+ }
+ String res = readUTF(data, 0, data.length);
+
+ return res;
+ }
+
+ /**
+ * Enumeration of the property names.
+ * @return 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;
+ }
+
+
+ /**
+ * JmDNS callback to update a DNS record.
+ * @param rec
+ */
+ 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();
+ }
+
+ /**
+ * SC-Bonjour Implementation: Method used to set the properties of an existing ServiceInfo.
+ * This is used in the implementation of Bonjour in SIP Communicator to be able to replace
+ * old properties of the service we've declared to announce the local user with new properties
+ * (for example in case of a status change).
+ * @param props Hashtable containing all the new properties to set
+ */
+ public void setProps(Hashtable props)
+ {
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceListener.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceListener.java
new file mode 100644
index 0000000..81099a3
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceListener.java
@@ -0,0 +1,44 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.*;
+
+/**
+ * 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);
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceTypeListener.java b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceTypeListener.java
new file mode 100644
index 0000000..8987fac
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/jmdns/ServiceTypeListener.java
@@ -0,0 +1,23 @@
+//Copyright 2003-2005 Arthur van Hoff, Rick Blair
+//Licensed under Apache License version 2.0
+//Original license LGPL
+package net.java.sip.communicator.impl.protocol.zeroconf.jmdns;
+
+import java.util.*;
+
+/**
+ * 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);
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/zeroconf/zeroconf.provider.manifest.mf b/src/net/java/sip/communicator/impl/protocol/zeroconf/zeroconf.provider.manifest.mf
new file mode 100644
index 0000000..305ce56
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/zeroconf/zeroconf.provider.manifest.mf
@@ -0,0 +1,11 @@
+Bundle-Activator: net.java.sip.communicator.impl.protocol.zeroconf.ZeroconfActivator
+Bundle-Name: Zeroconf Protocol Provider
+Bundle-Description: A bundle providing support for the Zeroconf protocol.
+Bundle-Vendor: sip-communicator.org
+Bundle-Version: 0.0.1
+Import-Package: org.osgi.framework,
+ net.java.sip.communicator.service.configuration,
+ net.java.sip.communicator.service.configuration.event,
+ net.java.sip.communicator.util,
+ net.java.sip.communicator.service.protocol,
+ net.java.sip.communicator.service.protocol.event \ No newline at end of file
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/FirstWizardPage.java b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/FirstWizardPage.java
new file mode 100644
index 0000000..32f3cef
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/FirstWizardPage.java
@@ -0,0 +1,383 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.zeroconfaccregwizz;
+
+import java.util.*;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.event.*;
+
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * The <tt>FirstWizardPage</tt> is the page, where user could enter the user ID
+ * and the password of the account.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class FirstWizardPage
+ extends JPanel
+ implements WizardPage,
+ DocumentListener
+{
+
+ public static final String FIRST_PAGE_IDENTIFIER = "FirstPageIdentifier";
+
+ private JPanel userPassPanel = new JPanel(new BorderLayout(10, 10));
+
+ private JPanel labelsPanel = new JPanel();
+
+ private JPanel valuesPanel = new JPanel();
+
+ private JLabel userID = new JLabel(Resources.getString("userID"));
+
+
+ /* TEMPORARY : HARD CODED !! Should be added to Resource */
+ private JLabel firstLabel = new JLabel("Firstname:");
+ private JLabel lastLabel = new JLabel("Lastname:");
+ private JLabel mailLabel = new JLabel("Mail address:");
+
+
+ private JLabel existingAccountLabel
+ = new JLabel(Resources.getString("existingAccount"));
+
+ private JPanel emptyPanel = new JPanel();
+ private JPanel emptyPanel2 = new JPanel();
+ private JPanel emptyPanel3 = new JPanel();
+ private JPanel emptyPanel4 = new JPanel();
+
+ private JLabel userIDExampleLabel = new JLabel("Ex: Bill@microsoft");
+ private JLabel firstExampleLabel = new JLabel("Ex: Bill");
+ private JLabel lastExampleLabel = new JLabel("Ex: Gates");
+ private JLabel mailExampleLabel = new JLabel("Ex: Bill@microsoft.com");
+
+ private JTextField userIDField = new JTextField();
+ private JTextField firstField = new JTextField();
+ private JTextField lastField = new JTextField();
+ private JTextField mailField = new JTextField();
+
+ private JCheckBox rememberContacts =
+ new JCheckBox("Remember Bonjour contacts?");
+
+ private JPanel mainPanel = new JPanel();
+
+ private Object nextPageIdentifier = WizardPage.SUMMARY_PAGE_IDENTIFIER;
+
+ private ZeroconfAccountRegistration registration = null;
+
+ private WizardContainer wizardContainer;
+
+ /**
+ * Creates an instance of <tt>FirstWizardPage</tt>.
+ * @param registration the <tt>ZeroconfAccountRegistration</tt>, where
+ * all data through the wizard are stored
+ * @param wizardContainer the wizardContainer, where this page will
+ * be added
+ */
+ public FirstWizardPage(ZeroconfAccountRegistration registration,
+ WizardContainer wizardContainer)
+ {
+
+ super(new BorderLayout());
+
+ this.wizardContainer = wizardContainer;
+
+ this.registration = registration;
+
+ this.setPreferredSize(new Dimension(150, 100));
+
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+
+ this.init();
+
+ this.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+ this.labelsPanel.setLayout(
+ new BoxLayout(labelsPanel, BoxLayout.Y_AXIS));
+
+ this.valuesPanel.setLayout(
+ new BoxLayout(valuesPanel, BoxLayout.Y_AXIS));
+ }
+
+ /**
+ * Initializes all panels, buttons, etc.
+ */
+ private void init()
+ {
+ this.userIDField.getDocument().addDocumentListener(this);
+ this.firstField.getDocument().addDocumentListener(this);
+ this.rememberContacts.setSelected(false);
+
+ // not used so disable it for the moment
+ this.rememberContacts.setEnabled(false);
+
+ this.existingAccountLabel.setForeground(Color.RED);
+
+ this.userIDExampleLabel.setForeground(Color.GRAY);
+ this.userIDExampleLabel.setFont(
+ userIDExampleLabel.getFont().deriveFont(8));
+ this.emptyPanel.setMaximumSize(new Dimension(40, 35));
+ this.userIDExampleLabel.setBorder(
+ BorderFactory.createEmptyBorder(0, 0, 8,0));
+
+ this.firstExampleLabel.setForeground(Color.GRAY);
+ this.firstExampleLabel.setFont(
+ firstExampleLabel.getFont().deriveFont(8));
+ this.emptyPanel2.setMaximumSize(new Dimension(40, 35));
+ this.firstExampleLabel.setBorder(
+ BorderFactory.createEmptyBorder(0, 0, 8,0));
+
+ this.lastExampleLabel.setForeground(Color.GRAY);
+ this.lastExampleLabel.setFont(
+ lastExampleLabel.getFont().deriveFont(8));
+ this.emptyPanel3.setMaximumSize(new Dimension(40, 35));
+ this.lastExampleLabel.setBorder(
+ BorderFactory.createEmptyBorder(0, 0, 8,0));
+
+ this.mailExampleLabel.setForeground(Color.GRAY);
+ this.mailExampleLabel.setFont(
+ mailExampleLabel.getFont().deriveFont(8));
+ this.emptyPanel4.setMaximumSize(new Dimension(40, 35));
+ this.mailExampleLabel.setBorder(
+ BorderFactory.createEmptyBorder(0, 0, 8,0));
+
+
+ labelsPanel.add(userID);
+ labelsPanel.add(emptyPanel);
+ labelsPanel.add(firstLabel);
+ labelsPanel.add(emptyPanel2);
+ labelsPanel.add(lastLabel);
+ labelsPanel.add(emptyPanel3);
+ labelsPanel.add(mailLabel);
+
+
+ valuesPanel.add(userIDField);
+ valuesPanel.add(userIDExampleLabel);
+ valuesPanel.add(firstField);
+ valuesPanel.add(firstExampleLabel);
+ valuesPanel.add(lastField);
+ valuesPanel.add(lastExampleLabel);
+ valuesPanel.add(mailField);
+ valuesPanel.add(mailExampleLabel);
+
+
+ userPassPanel.add(labelsPanel, BorderLayout.WEST);
+ userPassPanel.add(valuesPanel, BorderLayout.CENTER);
+ userPassPanel.add(rememberContacts, BorderLayout.SOUTH);
+
+ userPassPanel.setBorder(BorderFactory
+ .createTitledBorder(Resources.getString(
+ "userAndPassword")));
+
+ this.add(userPassPanel, BorderLayout.NORTH);
+ }
+
+ /**
+ * Implements the <code>WizardPage.getIdentifier</code> to return
+ * this page identifier.
+ *
+ * @return the Identifier of the first page in this wizard.
+ */
+ public Object getIdentifier()
+ {
+ return FIRST_PAGE_IDENTIFIER;
+ }
+
+ /**
+ * Implements the <code>WizardPage.getNextPageIdentifier</code> to return
+ * the next page identifier - the summary page.
+ *
+ * @return the identifier of the page following this one.
+ */
+ public Object getNextPageIdentifier()
+ {
+ return nextPageIdentifier;
+ }
+
+ /**
+ * Implements the <code>WizardPage.getBackPageIdentifier</code> to return
+ * the next back identifier - the default page.
+ *
+ * @return the identifier of the default wizard page.
+ */
+ public Object getBackPageIdentifier()
+ {
+ return WizardPage.DEFAULT_PAGE_IDENTIFIER;
+ }
+
+ /**
+ * Implements the <code>WizardPage.getWizardForm</code> to return
+ * this panel.
+ *
+ * @return the component to be displayed in this wizard page.
+ */
+ public Object getWizardForm()
+ {
+ return this;
+ }
+
+ /**
+ * Before this page is displayed enables or disables the "Next" wizard
+ * button according to whether the UserID field is empty.
+ */
+ public void pageShowing()
+ {
+ this.setNextButtonAccordingToUserID();
+ }
+
+ /**
+ * Saves the user input when the "Next" wizard buttons is clicked.
+ */
+ public void pageNext()
+ {
+ String userID = userIDField.getText();
+
+ // TODO: isExistingAccount blocks (probably badly/not implemented) !!!!
+ // ----
+ if (isExistingAccount(userID))
+ {
+ nextPageIdentifier = FIRST_PAGE_IDENTIFIER;
+ userPassPanel.add(existingAccountLabel, BorderLayout.NORTH);
+ this.revalidate();
+ }
+ else
+ {
+ nextPageIdentifier = SUMMARY_PAGE_IDENTIFIER;
+ userPassPanel.remove(existingAccountLabel);
+
+ registration.setUserID(userIDField.getText());
+ registration.setFirst(firstField.getText());
+ registration.setLast(lastField.getText());
+ registration.setMail(mailField.getText());
+
+ registration.setRememberContacts(rememberContacts.isSelected());
+ }
+ }
+
+ /**
+ * Enables or disables the "Next" wizard button according to whether the
+ * User ID field is empty.
+ */
+ private void setNextButtonAccordingToUserID()
+ {
+ if (userIDField.getText() == null || userIDField.getText().equals("")
+ || firstField.getText() == null || firstField.getText().equals(""))
+ {
+ wizardContainer.setNextFinishButtonEnabled(false);
+ }
+ else
+ {
+ wizardContainer.setNextFinishButtonEnabled(true);
+ }
+ }
+
+ /**
+ * Handles the <tt>DocumentEvent</tt> triggered when user types in the
+ * User ID field. Enables or disables the "Next" wizard button according to
+ * whether the User ID field is empty.
+ *
+ * @param event the event containing the update.
+ */
+ public void insertUpdate(DocumentEvent event)
+ {
+ this.setNextButtonAccordingToUserID();
+ }
+
+ /**
+ * Handles the <tt>DocumentEvent</tt> triggered when user deletes letters
+ * from the UserID field. Enables or disables the "Next" wizard button
+ * according to whether the UserID field is empty.
+ *
+ * @param event the event containing the update.
+ */
+ public void removeUpdate(DocumentEvent event)
+ {
+ this.setNextButtonAccordingToUserID();
+ }
+
+ /**
+ * Implemented from Wizard interface
+ * @param event Event that happened
+ */
+ public void changedUpdate(DocumentEvent event)
+ {
+ }
+
+ /**
+ * Created to
+ */
+ public void pageHiding()
+ {
+ }
+
+ /**
+ * Implemented from Wizard interface
+ */
+ public void pageShown()
+ {
+ }
+
+ /**
+ * Implemented from Wizard interface
+ */
+ public void pageBack()
+ {
+ }
+
+ /**
+ * Fills the UserID field in this panel with the data comming
+ * from the given protocolProvider.
+ * @param protocolProvider The <tt>ProtocolProviderService</tt> to load the
+ * data from.
+ */
+ public void loadAccount(ProtocolProviderService protocolProvider)
+ {
+ AccountID accountID = protocolProvider.getAccountID();
+
+ this.userIDField.setText(accountID.getUserID());
+ this.firstField.setText((String)accountID.getAccountProperties()
+ .get("first"));
+ this.lastField.setText((String)accountID.getAccountProperties()
+ .get("last"));
+ this.mailField.setText((String)accountID.getAccountProperties()
+ .get("mail"));
+ Boolean remember = (Boolean)accountID.getAccountProperties()
+ .get("rememberContacts");
+ if (remember.booleanValue()) this.rememberContacts.setSelected(true);
+
+ }
+
+ /**
+ * Verifies whether there is already an account installed with the same
+ * details as the one that the user has just entered.
+ *
+ * @param userID the name of the user that the account is registered for
+ * @return true if there is already an account for this userID and false
+ * otherwise.
+ */
+ private boolean isExistingAccount(String userID)
+ {
+ ProtocolProviderFactory factory
+ = ZeroconfAccRegWizzActivator.getZeroconfProtocolProviderFactory();
+
+ ArrayList registeredAccounts = factory.getRegisteredAccounts();
+
+ for (int i = 0; i < registeredAccounts.size(); i++)
+ {
+ AccountID accountID = (AccountID) registeredAccounts.get(i);
+
+ if (userID.equalsIgnoreCase(accountID.getUserID()))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/Resources.java b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/Resources.java
new file mode 100644
index 0000000..a064d3d
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/Resources.java
@@ -0,0 +1,100 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+
+package net.java.sip.communicator.plugin.zeroconfaccregwizz;
+
+import java.io.*;
+import java.util.*;
+
+import net.java.sip.communicator.util.*;
+
+/**
+ * The Resources class manages the access to the internationalization
+ * properties files.
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class Resources
+{
+
+ private static Logger log = Logger.getLogger(Resources.class);
+
+ private static final String BUNDLE_NAME
+ = "net.java.sip.communicator.plugin.zeroconfaccregwizz.resources";
+
+ private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle
+ .getBundle(BUNDLE_NAME);
+
+ public static ImageID ZEROCONF_LOGO = new ImageID("protocolIcon");
+
+ public static ImageID PAGE_IMAGE = new ImageID("pageImage");
+
+ /**
+ * Returns an internationalized string corresponding to the given key.
+ * @param key The key of the string.
+ * @return An internationalized string corresponding to the given key.
+ */
+ public static String getString(String key)
+ {
+ try
+ {
+ return RESOURCE_BUNDLE.getString(key);
+
+ }
+ catch (MissingResourceException exc)
+ {
+ return '!' + key + '!';
+ }
+ }
+
+ /**
+ * Loads an image from a given image identifier.
+ * @param imageID The identifier of the image.
+ * @return The image for the given identifier.
+ */
+ public static byte[] getImage(ImageID imageID)
+ {
+ byte[] image = new byte[100000];
+
+ String path = Resources.getString(imageID.getId());
+ try
+ {
+ Resources.class.getClassLoader()
+ .getResourceAsStream(path).read(image);
+
+ }
+ catch (IOException exc)
+ {
+ log.error("Failed to load image:" + path, exc);
+ }
+
+ return image;
+ }
+
+ /**
+ * Represents the Image Identifier.
+ */
+ public static class ImageID
+ {
+ private String id;
+
+ private ImageID(String id)
+ {
+ this.id = id;
+ }
+
+ /**
+ * Returns the user ID of this account
+ * @return user ID
+ */
+ public String getId()
+ {
+ return id;
+ }
+ }
+
+}
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccRegWizzActivator.java b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccRegWizzActivator.java
new file mode 100644
index 0000000..5740f3e
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccRegWizzActivator.java
@@ -0,0 +1,111 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.zeroconfaccregwizz;
+
+import org.osgi.framework.*;
+import net.java.sip.communicator.service.configuration.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Registers the <tt>ZeroconfAccountRegistrationWizard</tt> in the UI Service.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ZeroconfAccRegWizzActivator
+ implements BundleActivator
+{
+ private static Logger logger = Logger.getLogger(
+ ZeroconfAccRegWizzActivator.class.getName());
+
+ /**
+ * A currently valid bundle context.
+ */
+ public static BundleContext bundleContext;
+
+ /**
+ * A currently valid reference to the configuration service.
+ */
+ private static ConfigurationService configService;
+
+ /**
+ * Starts this bundle.
+ * @param bc the currently valid <tt>BundleContext</tt>.
+ */
+ public void start(BundleContext bc)
+ {
+ logger.info("Loading zeroconf account wizard.");
+
+ bundleContext = bc;
+
+ ServiceReference uiServiceRef = bundleContext
+ .getServiceReference(UIService.class.getName());
+
+ UIService uiService
+ = (UIService) bundleContext.getService(uiServiceRef);
+
+ AccountRegistrationWizardContainer wizardContainer
+ = uiService.getAccountRegWizardContainer();
+
+ ZeroconfAccountRegistrationWizard zeroconfWizard
+ = new ZeroconfAccountRegistrationWizard(wizardContainer);
+
+ wizardContainer.addAccountRegistrationWizard(zeroconfWizard);
+
+ logger.info("Zeroconf account registration wizard [STARTED].");
+ }
+
+ /**
+ * Called when this bundle is stopped so the Framework can perform the
+ * bundle-specific activities necessary to stop the bundle.
+ *
+ * @param context The execution context of the bundle being stopped.
+ */
+ public void stop(BundleContext context)
+ {
+
+ }
+
+ /**
+ * Returns the <tt>ProtocolProviderFactory</tt> for the Zeroconf protocol.
+ * @return the <tt>ProtocolProviderFactory</tt> for the Zeroconf protocol
+ */
+ public static ProtocolProviderFactory getZeroconfProtocolProviderFactory()
+ {
+
+ ServiceReference[] serRefs = null;
+
+ String osgiFilter = "("
+ + ProtocolProviderFactory.PROTOCOL
+ + "=" + ProtocolNames.ZEROCONF + ")";
+
+ try
+ {
+ serRefs = bundleContext.getServiceReferences(
+ ProtocolProviderFactory.class.getName(), osgiFilter);
+ }
+ catch (InvalidSyntaxException ex)
+ {
+ logger.error(ex);
+ }
+
+ //System.out.println(" SerRefs " +serRefs);
+
+ return (ProtocolProviderFactory) bundleContext.getService(serRefs[0]);
+ }
+
+ /**
+ * Returns the bundleContext that we received when we were started.
+ * @return a currently valid instance of a bundleContext.
+ */
+ public BundleContext getBundleContext()
+ {
+ return bundleContext;
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistration.java b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistration.java
new file mode 100644
index 0000000..05705eb
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistration.java
@@ -0,0 +1,119 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.zeroconfaccregwizz;
+
+/**
+ * The <tt>ZeroconfAccountRegistration</tt> is used to store
+ * all user input data
+ * through the <tt>ZeroconfAccountRegistrationWizard</tt>.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ZeroconfAccountRegistration
+{
+ private String userID;
+ private String first;
+ private String last;
+ private String mail;
+ private boolean rememberContacts;
+
+ /**
+ * Returns the User ID of the zeroconf registration account.
+ * @return the User ID of the zeroconf registration account.
+ */
+ public String getUserID()
+ {
+ return userID;
+ }
+
+ /**
+ * Sets the user ID of the zeroconf registration account.
+ * @param userID the userID of the zeroconf registration account.
+ */
+ public void setUserID(String userID)
+ {
+ this.userID = userID;
+ }
+
+ /**
+ * Returns the password of the Zeroconf registration account.
+ * @return the password of the Zeroconf registration account.
+ */
+ public String getFirst()
+ {
+ return first;
+ }
+
+ /**
+ * Sets the password of the Zeroconf registration account.
+ * @param first first name
+ */
+ public void setFirst(String first)
+ {
+ this.first = first;
+ }
+
+ /**
+ * Returns <tt>true</tt> if password has to remembered, <tt>false</tt>
+ * otherwise.
+ * @return <tt>true</tt> if password has to remembered, <tt>false</tt>
+ * otherwise.
+ */
+ public boolean isRememberContacts()
+ {
+ return rememberContacts;
+ }
+
+ /**
+ * Sets the rememberPassword value of this Zeroconf account registration.
+ * @param rememberContacts true if we want to remember the
+ * contacts we meet, false otherwise
+ */
+ public void setRememberContacts(boolean rememberContacts)
+ {
+ this.rememberContacts = rememberContacts;
+ }
+
+ /**
+ * Returns the last name
+ * @return last name
+ */
+ public String getLast()
+ {
+ return last;
+ }
+
+ /**
+ * Sets the last name
+ * @param last last name
+ */
+ public void setLast(String last)
+ {
+ this.last = last;
+ }
+
+ /**
+ * Returns the mail address
+ * @return mail address
+ */
+ public String getMail()
+ {
+ return mail;
+ }
+
+ /**
+ * Sets the mail address
+ * @param mail mail address
+ */
+ public void setMail(String mail)
+ {
+ this.mail = mail;
+ }
+
+
+}
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistrationWizard.java b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistrationWizard.java
new file mode 100644
index 0000000..10cf245
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/ZeroconfAccountRegistrationWizard.java
@@ -0,0 +1,208 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.zeroconfaccregwizz;
+
+import java.util.*;
+
+import org.osgi.framework.*;
+
+import net.java.sip.communicator.impl.gui.customcontrols.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * The <tt>ZeroconfAccountRegistrationWizard</tt> is an implementation of the
+ * <tt>AccountRegistrationWizard</tt> for the Zeroconf protocol. It allows
+ * the user to create and configure a new Zeroconf account.
+ *
+ * @author Christian Vincenot
+ * @author Maxime Catelin
+ */
+public class ZeroconfAccountRegistrationWizard
+ implements AccountRegistrationWizard
+{
+
+ /**
+ * The first page of the zeroconf account registration wizard.
+ */
+ private FirstWizardPage firstWizardPage;
+
+ /**
+ * The object that we use to store details on an account that we will be
+ * creating.
+ */
+ private ZeroconfAccountRegistration registration
+ = new ZeroconfAccountRegistration();
+
+ private WizardContainer wizardContainer;
+
+ private ProtocolProviderService protocolProvider;
+
+ private String propertiesPackage
+ = "net.java.sip.communicator.plugin.zeroconfaccregwizz";
+
+ private boolean isModification;
+
+ /**
+ * Creates an instance of <tt>ZeroconfAccountRegistrationWizard</tt>.
+ * @param wizardContainer the wizard container, where this wizard
+ * is added
+ */
+ public ZeroconfAccountRegistrationWizard(WizardContainer wizardContainer)
+ {
+ this.wizardContainer = wizardContainer;
+ }
+
+ /**
+ * Implements the <code>AccountRegistrationWizard.getIcon</code> method.
+ * Returns the icon to be used for this wizard.
+ * @return byte[]
+ */
+ public byte[] getIcon()
+ {
+ return Resources.getImage(Resources.ZEROCONF_LOGO);
+ }
+
+ /**
+ * Implements the <code>AccountRegistrationWizard.getPageImage</code> method.
+ * Returns the image used to decorate the wizard page
+ *
+ * @return byte[] the image used to decorate the wizard page
+ */
+ public byte[] getPageImage()
+ {
+ return Resources.getImage(Resources.PAGE_IMAGE);
+ }
+
+ /**
+ * Implements the <code>AccountRegistrationWizard.getProtocolName</code>
+ * method. Returns the protocol name for this wizard.
+ * @return String
+ */
+ public String getProtocolName()
+ {
+ return Resources.getString("protocolName");
+ }
+
+ /**
+ * Implements the <code>AccountRegistrationWizard.getProtocolDescription
+ * </code> method. Returns the description of the protocol for this wizard.
+ * @return String
+ */
+ public String getProtocolDescription()
+ {
+ return Resources.getString("protocolDescription");
+ }
+
+ /**
+ * Returns the set of pages contained in this wizard.
+ * @return Iterator
+ */
+ public Iterator getPages()
+ {
+ ArrayList pages = new ArrayList();
+ firstWizardPage = new FirstWizardPage(registration, wizardContainer);
+
+ pages.add(firstWizardPage);
+
+ return pages.iterator();
+ }
+
+ /**
+ * Returns the set of data that user has entered through this wizard.
+ * @return Iterator
+ */
+ public Iterator getSummary()
+ {
+ Hashtable summaryTable = new Hashtable();
+
+ summaryTable.put("User ID", registration.getUserID());
+ summaryTable.put("First Name", registration.getFirst());
+ summaryTable.put("Last Name", registration.getLast());
+ summaryTable.put("Mail Address", registration.getMail());
+ summaryTable.put("Remember Bonjour contacts?",
+ Boolean.toString(registration.isRememberContacts()));
+
+ return summaryTable.entrySet().iterator();
+ }
+
+ /**
+ * Installs the account created through this wizard.
+ * @return ProtocolProviderService
+ */
+ public ProtocolProviderService finish()
+ {
+ firstWizardPage = null;
+ ProtocolProviderFactory factory
+ = ZeroconfAccRegWizzActivator.getZeroconfProtocolProviderFactory();
+
+ return this.installAccount(factory,
+ registration.getUserID());
+ }
+
+ /**
+ * Creates an account for the given user and password.
+ *
+ * @return the <tt>ProtocolProviderService</tt> for the new account.
+ * @param providerFactory the ProtocolProviderFactory which will create
+ * the account
+ * @param user the user identifier
+ */
+ public ProtocolProviderService installAccount(
+ ProtocolProviderFactory providerFactory,
+ String user)
+ {
+
+ Hashtable accountProperties = new Hashtable();
+
+ accountProperties.put("first", registration.getFirst());
+ accountProperties.put("last", registration.getLast());
+ accountProperties.put("mail", registration.getMail());
+
+ accountProperties.put("rememberContacts",
+ new Boolean(registration.isRememberContacts()).toString());
+
+ try
+ {
+ AccountID accountID = providerFactory.installAccount(
+ user, accountProperties);
+
+ ServiceReference serRef = providerFactory
+ .getProviderForAccount(accountID);
+
+ protocolProvider = (ProtocolProviderService)
+ ZeroconfAccRegWizzActivator.bundleContext
+ .getService(serRef);
+ }
+ catch (IllegalArgumentException exc)
+ {
+ new ErrorDialog(null, exc.getMessage(), exc).showDialog();
+ }
+ catch (IllegalStateException exc)
+ {
+ new ErrorDialog(null, exc.getMessage(), exc).showDialog();
+ }
+
+ return protocolProvider;
+ }
+
+ /**
+ * Fills the UserID and Password fields in this panel with the data comming
+ * from the given protocolProvider.
+ * @param protocolProvider The <tt>ProtocolProviderService</tt> to load the
+ * data from.
+ */
+ public void loadAccount(ProtocolProviderService protocolProvider)
+ {
+
+ this.protocolProvider = protocolProvider;
+
+ this.firstWizardPage.loadAccount(protocolProvider);
+
+ isModification = true;
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/resources.properties b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/resources.properties
new file mode 100644
index 0000000..48ca9f2
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/resources.properties
@@ -0,0 +1,11 @@
+protocolName=Zeroconf
+protocolDescription=The Zeroconf (Bonjour) service protocol.
+userID=User ID:
+firstname=Firname:
+password=Password:
+rememberPassword=Remember password
+userAndPassword=Identification
+existingAccount=* The account you entered is already installed.
+
+protocolIcon=resources/images/zeroconf/zeroconf-color-16.png
+pageImage=resources/images/zeroconf/zeroconf-color-64.png \ No newline at end of file
diff --git a/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/zeroconfaccregwizz.manifest.mf b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/zeroconfaccregwizz.manifest.mf
new file mode 100644
index 0000000..9762b18
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/zeroconfaccregwizz/zeroconfaccregwizz.manifest.mf
@@ -0,0 +1,30 @@
+Bundle-Activator: net.java.sip.communicator.plugin.zeroconfaccregwizz.ZeroconfAccRegWizzActivator
+Bundle-Name: Zeroconf account registration wizard
+Bundle-Description: Zeroconf account registration wizard.
+Bundle-Vendor: sip-communicator.org
+Bundle-Version: 0.0.1
+Import-Package: org.osgi.framework,
+ net.java.sip.communicator.util,
+ net.java.sip.communicator.service.configuration,
+ net.java.sip.communicator.service.configuration.event,
+ net.java.sip.communicator.service.protocol,
+ net.java.sip.communicator.service.protocol.event,
+ net.java.sip.communicator.service.contactlist,
+ net.java.sip.communicator.service.contactlist.event,
+ net.java.sip.communicator.service.gui,
+ net.java.sip.communicator.service.gui.event,
+ net.java.sip.communicator.service.browserlauncher,
+ javax.swing,
+ javax.swing.event,
+ javax.swing.table,
+ javax.swing.text,
+ javax.swing.text.html,
+ javax.accessibility,
+ javax.swing.plaf,
+ javax.swing.plaf.metal,
+ javax.swing.plaf.basic,
+ javax.imageio,
+ javax.swing.filechooser,
+ javax.swing.tree,
+ javax.swing.undo,
+ javax.swing.border
diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolNames.java b/src/net/java/sip/communicator/service/protocol/ProtocolNames.java
index 71bddd8..d215465 100644
--- a/src/net/java/sip/communicator/service/protocol/ProtocolNames.java
+++ b/src/net/java/sip/communicator/service/protocol/ProtocolNames.java
@@ -65,4 +65,8 @@ public interface ProtocolNames
*/
public static final String SIP_COMMUNICATOR_MOCK = "sip-communicator-mock";
+ /**
+ * The Zeroconf protcool.
+ */
+ public static final String ZEROCONF = "Zeroconf";
}