diff options
author | Damian Minkov <damencho@jitsi.org> | 2007-06-11 10:47:51 +0000 |
---|---|---|
committer | Damian Minkov <damencho@jitsi.org> | 2007-06-11 10:47:51 +0000 |
commit | 152b663a227d0005c2007ae7d59e1ec75e08eb03 (patch) | |
tree | 2ffd67cb4c0dcac1b8173a8a8d806833e7b69160 | |
parent | 4105273561872e2cf8e64d00db5b2d59d9d97ab9 (diff) | |
download | jitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.zip jitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.tar.gz jitsi-152b663a227d0005c2007ae7d59e1ec75e08eb03.tar.bz2 |
Zeroconf protocol provider.
47 files changed, 12930 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..2ecf4bc --- /dev/null +++ b/resources/images/zeroconf/zeroconf-away.png diff --git a/resources/images/zeroconf/zeroconf-color-16.png b/resources/images/zeroconf/zeroconf-color-16.png Binary files differnew file mode 100644 index 0000000..47d6b7d --- /dev/null +++ b/resources/images/zeroconf/zeroconf-color-16.png diff --git a/resources/images/zeroconf/zeroconf-color-64.png b/resources/images/zeroconf/zeroconf-color-64.png Binary files differnew file mode 100644 index 0000000..6db9d49 --- /dev/null +++ b/resources/images/zeroconf/zeroconf-color-64.png diff --git a/resources/images/zeroconf/zeroconf-dnd.png b/resources/images/zeroconf/zeroconf-dnd.png Binary files differnew file mode 100644 index 0000000..23bd067 --- /dev/null +++ b/resources/images/zeroconf/zeroconf-dnd.png diff --git a/resources/images/zeroconf/zeroconf-invisible.png b/resources/images/zeroconf/zeroconf-invisible.png Binary files differnew file mode 100644 index 0000000..35dd6b4 --- /dev/null +++ b/resources/images/zeroconf/zeroconf-invisible.png diff --git a/resources/images/zeroconf/zeroconf-offline.png b/resources/images/zeroconf/zeroconf-offline.png Binary files differnew file mode 100644 index 0000000..35609ff --- /dev/null +++ b/resources/images/zeroconf/zeroconf-offline.png diff --git a/resources/images/zeroconf/zeroconf-online.png b/resources/images/zeroconf/zeroconf-online.png Binary files differnew file mode 100644 index 0000000..3bde6f6 --- /dev/null +++ b/resources/images/zeroconf/zeroconf-online.png diff --git a/resources/images/zeroconf/zeroconf.png b/resources/images/zeroconf/zeroconf.png Binary files differnew file mode 100644 index 0000000..e7ad8a3 --- /dev/null +++ b/resources/images/zeroconf/zeroconf.png 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 < PROBING_2 < PROBING_3 < ANNOUNCING_1 < + * ANNOUNCING_2 < RESPONDING < ANNOUNCED < 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"; } |