diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicInstantMessagingSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicInstantMessagingSipImpl.java
index c13fecca0..70084d639 100644
--- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicInstantMessagingSipImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicInstantMessagingSipImpl.java
@@ -775,7 +775,7 @@ public void processTimeout(TimeoutEvent timeoutEvent)
return;
}
- Contact to = resolveContact(
+ Contact to = opSetPersPresence.resolveContactID(
toHeader.getAddress().getURI().toString());
Message failedMessage = null;
@@ -863,7 +863,7 @@ public void processRequest(RequestEvent requestEvent)
return;
}
- Contact from = resolveContact(
+ Contact from = opSetPersPresence.resolveContactID(
fromHeader.getAddress().getURI().toString());
Message newMessage = createMessage(content);
@@ -957,7 +957,7 @@ public void processResponse(ResponseEvent responseEvent)
return;
}
- Contact to = resolveContact(toHeader.getAddress()
+ Contact to = opSetPersPresence.resolveContactID(toHeader.getAddress()
.getURI().toString());
if (to == null) {
@@ -1093,45 +1093,6 @@ private String getCharset(Request req)
return charset;
}
- /**
- * Try to find a contact registered using a string to identify him.
- *
- * @param contactID A string with which the contact may have
- * been registered
- * @return A valid contact if it has been found, null otherwise
- */
- private Contact resolveContact(String contactID) {
- Contact res = opSetPersPresence.findContactByID(contactID);
-
- if (res == null) {
- // we try to resolve the conflict by removing "sip:" from the id
- if (contactID.startsWith("sip:")) {
- res = opSetPersPresence.findContactByID(
- contactID.substring(4));
- }
-
- if (res == null) {
- // we try to remove the part after the '@'
- if (contactID.indexOf('@') > -1) {
- res = opSetPersPresence.findContactByID(
- contactID.substring(0,
- contactID.indexOf('@')));
-
- if (res == null) {
- // try the same thing without sip:
- if (contactID.startsWith("sip:")) {
- res = opSetPersPresence.findContactByID(
- contactID.substring(4,
- contactID.indexOf('@')));
- }
- }
- }
- }
- }
-
- return res;
- }
-
/**
* Attempts to re-generate the corresponding request with the proper
* credentials.
diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetPresenceSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetPresenceSipImpl.java
index 3f084d97f..36e9eee6d 100644
--- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetPresenceSipImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetPresenceSipImpl.java
@@ -3502,7 +3502,7 @@ private ContactGroupSipImpl getNonPersistentGroup()
* been registered
* @return A valid contact if it has been found, null otherwise
*/
- private Contact resolveContactID(String contactID) {
+ Contact resolveContactID(String contactID) {
Contact res = this.findContactByID(contactID);
if (res == null) {
@@ -3571,7 +3571,7 @@ private ContactSipImpl getWatcher(String contactAddress) {
*
* @return a correct xml document or null if an error occurs
*/
- private Document createDocument() {
+ Document createDocument() {
try {
if (this.docBuilderFactory == null) {
this.docBuilderFactory = DocumentBuilderFactory.newInstance();
@@ -3596,7 +3596,7 @@ private Document createDocument() {
* @return a string representing document or null if an error
* occur
*/
- private String convertDocument(Document document) {
+ String convertDocument(Document document) {
DOMSource source = new DOMSource(document);
StringWriter stringWriter = new StringWriter();
StreamResult result = new StreamResult(stringWriter);
@@ -3627,7 +3627,7 @@ private String convertDocument(Document document) {
* @return a Document representing the document or null if an
* error occur
*/
- private Document convertDocument(String document) {
+ Document convertDocument(String document) {
StringReader reader = new StringReader(document);
StreamSource source = new StreamSource(reader);
Document doc = createDocument();
diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTypingNotificationsSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTypingNotificationsSipImpl.java
new file mode 100644
index 000000000..79ad579e5
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTypingNotificationsSipImpl.java
@@ -0,0 +1,550 @@
+/*
+ * 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.sip;
+
+import java.text.*;
+import java.util.*;
+import javax.sip.*;
+import javax.sip.header.*;
+import javax.sip.message.*;
+
+import org.w3c.dom.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.xml.*;
+import net.java.sip.communicator.service.protocol.Message;
+
+/**
+ * A implementation of the typing notification operation
+ * set.
+ *
+ * rfc3994
+ *
+ * @author Damian Minkov
+ */
+public class OperationSetTypingNotificationsSipImpl
+ implements OperationSetTypingNotifications,
+ SipMessageProcessor,
+ MessageListener
+{
+ private static final Logger logger =
+ Logger.getLogger(OperationSetTypingNotificationsSipImpl.class);
+
+ /**
+ * A list of listeners registered for message events.
+ */
+ private Vector typingNotificationsListeners = new Vector();
+
+ /**
+ * The provider that created us.
+ */
+ private ProtocolProviderServiceSipImpl sipProvider = null;
+
+ /**
+ * A reference to the persistent presence operation set that we use
+ * to match incoming messages to Contacts and vice versa.
+ */
+ private OperationSetPresenceSipImpl opSetPersPresence = null;
+
+ /**
+ * A reference to the persistent presence operation set that we use
+ * to match incoming messages to Contacts and vice versa.
+ */
+ private OperationSetBasicInstantMessagingSipImpl opSetBasicIm = null;
+
+ // XML documents types
+ private final String CONTENT_TYPE = "application/im-iscomposing+xml";
+ private final String CONTENT_SUBTYPE = "im-iscomposing+xml";
+
+ // isComposing elements and attributes
+ private static final String NS_VALUE = "urn:ietf:params:xml:ns:im-iscomposing";
+ private static final String STATE_ELEMENT= "state";
+ private static final String REFRESH_ELEMENT= "refresh";
+
+ private static final int REFRESH_DEFAULT_TIME = 120;
+
+ private static final String COMPOSING_STATE_ACTIVE = "active";
+ private static final String COMPOSING_STATE_IDLE = "idle";
+
+ private Timer timer = new Timer();
+ private Vector typingTasks = new Vector();
+
+ /**
+ * Creates an instance of this operation set.
+ * @param provider a ref to the ProtocolProviderServiceImpl
+ * that created us and that we'll use for retrieving the underlying aim
+ * connection.
+ */
+ OperationSetTypingNotificationsSipImpl(
+ ProtocolProviderServiceSipImpl provider,
+ OperationSetBasicInstantMessagingSipImpl opSetBasicIm)
+ {
+ this.sipProvider = provider;
+
+ provider.addRegistrationStateChangeListener(new
+ RegistrationStateListener());
+ this.opSetBasicIm = opSetBasicIm;
+ opSetBasicIm.addMessageProcessor(this);
+ }
+
+ /**
+ * Utility method throwing an exception if the stack is not properly
+ * initialized.
+ * @throws java.lang.IllegalStateException if the underlying stack is
+ * not registered and initialized.
+ */
+ private void assertConnected()
+ throws IllegalStateException
+ {
+ if (this.sipProvider == null)
+ throw new IllegalStateException(
+ "The provider must be non-null and signed on the "
+ + "service before being able to communicate.");
+ if (!this.sipProvider.isRegistered())
+ throw new IllegalStateException(
+ "The provider must be signed on the service before "
+ + "being able to communicate.");
+ }
+
+ /**
+ * Our listener that will tell us when we're registered to
+ */
+ private class RegistrationStateListener
+ implements RegistrationStateChangeListener
+ {
+ /**
+ * The method is called by a ProtocolProvider implementation whenever
+ * a change in the registration state of the corresponding provider had
+ * occurred.
+ * @param evt ProviderStatusChangeEvent the event describing the status
+ * change.
+ */
+ public void registrationStateChanged(RegistrationStateChangeEvent evt)
+ {
+ logger.debug("The provider changed state from: "
+ + evt.getOldState()
+ + " to: " + evt.getNewState());
+
+ if (evt.getNewState() == RegistrationState.REGISTERED)
+ {
+ opSetPersPresence = (OperationSetPresenceSipImpl)
+ sipProvider.getSupportedOperationSets()
+ .get(OperationSetPersistentPresence.class.getName());
+ }
+ }
+ }
+
+ /**
+ * Delivers a TypingNotificationEvent to all registered listeners.
+ * @param sourceContact the contact who has sent the notification.
+ * @param evtCode the code of the event to deliver.
+ */
+ private 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();
+
+ listener.typingNotificationReceifed(evt);
+ }
+ }
+
+ /**
+ * Process the incoming sip messages
+ * @param requestEvent the incoming event holding the message
+ * @return whether this message needs further processing(true) or no(false)
+ */
+ public boolean processMessage(RequestEvent requestEvent)
+ {
+ // get the content
+ String content = null;
+
+ Request req = requestEvent.getRequest();
+
+ ContentTypeHeader ctheader =
+ (ContentTypeHeader)req.getHeader(ContentTypeHeader.NAME);
+
+ // ignore messages which are not typing
+ // notifications and continue processing
+ if (ctheader == null && !ctheader.getContentSubType()
+ .equalsIgnoreCase(CONTENT_SUBTYPE))
+ return true;
+
+ content = new String(req.getRawContent());
+
+ if(content == null || content.length() == 0)
+ {
+ // send error
+ sendResponse(requestEvent, Response.BAD_REQUEST);
+ return false;
+ }
+
+ // who sent this request ?
+ FromHeader fromHeader = (FromHeader)
+ requestEvent.getRequest().getHeader(FromHeader.NAME);
+
+ if (fromHeader == null)
+ {
+ logger.error("received a request without a from header");
+ return true;
+ }
+
+ Contact from = opSetPersPresence.resolveContactID(
+ fromHeader.getAddress().getURI().toString());
+
+ // parse content
+ Document doc = null;
+ try
+ {
+ // parse content
+ doc = opSetPersPresence.convertDocument(content);
+ }
+ catch(Exception e){}
+
+ if (doc == null)
+ {
+ // send error
+ sendResponse(requestEvent, Response.BAD_REQUEST);
+ return false;
+ }
+
+ logger.debug("parsing:\n" + content);
+
+ //
+ NodeList stateList = doc.getElementsByTagNameNS(NS_VALUE,
+ STATE_ELEMENT);
+
+ if (stateList.getLength() == 0)
+ {
+ logger.error("no state element in this document");
+ // send error
+ sendResponse(requestEvent, Response.BAD_REQUEST);
+ return false;
+ }
+
+ Node stateNode = stateList.item(0);
+ if (stateNode.getNodeType() != Node.ELEMENT_NODE)
+ {
+ logger.error("the state node is not an element");
+ // send error
+ sendResponse(requestEvent, Response.BAD_REQUEST);
+ return false;
+ }
+
+ String state = XMLUtils.getText((Element)stateNode);
+
+ if(state == null || state.length() == 0)
+ {
+ logger.error("the state element without value");
+ // send error
+ sendResponse(requestEvent, Response.BAD_REQUEST);
+ return false;
+ }
+
+ //
+ NodeList refreshList = doc.getElementsByTagNameNS(NS_VALUE,
+ REFRESH_ELEMENT);
+ int refresh = REFRESH_DEFAULT_TIME;
+ if (refreshList.getLength() != 0)
+ {
+ Node refreshNode = refreshList.item(0);
+ if (refreshNode.getNodeType() == Node.ELEMENT_NODE)
+ {
+ String refreshStr = XMLUtils.getText((Element)refreshNode);
+
+ try
+ {
+ refresh = Integer.parseInt(refreshStr);
+ }
+ catch (Exception e)
+ {
+ logger.error("Wrong content for refresh", e);
+ }
+ }
+ }
+
+ // process the typing info we have gathered
+ if(state.equals(COMPOSING_STATE_ACTIVE))
+ {
+ TypingTask task = findTypigTask(from);
+
+ if(task == null)
+ {
+ task = new TypingTask(from);
+ typingTasks.add(task);
+ }
+ else
+ task.cancel();
+
+ timer.schedule(task, refresh * 1000);
+
+ fireTypingNotificationsEvent(from, STATE_TYPING);
+ }
+ else
+ if(state.equals(COMPOSING_STATE_IDLE))
+ {
+ fireTypingNotificationsEvent(from, STATE_PAUSED);
+ }
+
+ // send ok
+ sendResponse(requestEvent, Response.OK);
+ return false;
+ }
+
+ /**
+ * Process the responses of sent messages
+ * @param responseEvent the incoming event holding the response
+ * @return whether this message needs further processing(true) or no(false)
+ */
+ public boolean processResponse(ResponseEvent responseEvent, Map sentMsg)
+ {
+ // get the content
+ String content = null;
+
+ Request req = responseEvent.getClientTransaction().getRequest();
+
+ ContentTypeHeader ctheader =
+ (ContentTypeHeader)req.getHeader(ContentTypeHeader.NAME);
+
+ // ignore messages which are not typing
+ // notifications and continue processing
+ if (ctheader == null && !ctheader.getContentSubType()
+ .equalsIgnoreCase(CONTENT_SUBTYPE))
+ return true;
+
+ int status = responseEvent.getResponse().getStatusCode();
+
+ if (status >= 200 && status < 300)
+ {
+ logger.debug(
+ "Ack received from the network : "
+ + responseEvent.getResponse().getReasonPhrase());
+
+ // we retrieve the original message
+ String key = ((CallIdHeader)req.getHeader(CallIdHeader.NAME))
+ .getCallId();
+
+ // we don't need this message anymore
+ sentMsg.remove(key);
+
+ return false;
+ }
+ return true;
+ }
+
+ public boolean processTimeout(TimeoutEvent timeoutEvent, Map sentMessages)
+ {
+ return true;
+ }
+
+ private TypingTask findTypigTask(Contact contact)
+ {
+ Iterator tasksIter = typingTasks.iterator();
+ while (tasksIter.hasNext())
+ {
+ TypingTask typingTask = tasksIter.next();
+ if(typingTask.equals(contact))
+ return typingTask;
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds l to the list of listeners registered for receiving
+ * TypingNotificationEvents
+ *
+ * @param listener the TypingNotificationsListener listener that
+ * we'd like to add.
+ */
+ public void addTypingNotificationsListener(
+ TypingNotificationsListener listener)
+ {
+ synchronized(typingNotificationsListeners)
+ {
+ if(!typingNotificationsListeners.contains(listener))
+ typingNotificationsListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes l from the list of listeners registered for receiving
+ * TypingNotificationEvents
+ *
+ * @param listener the TypingNotificationsListener listener that
+ * we'd like to remove
+ */
+ public void removeTypingNotificationsListener(
+ TypingNotificationsListener listener)
+ {
+ synchronized(typingNotificationsListeners)
+ {
+ typingNotificationsListeners.remove(listener);
+ }
+ }
+
+ public void sendTypingNotification(Contact to, int typingState)
+ throws IllegalStateException, IllegalArgumentException
+ {
+ assertConnected();
+
+ if( !(to instanceof ContactSipImpl) )
+ throw new IllegalArgumentException(
+ "The specified contact is not a Sip contact."
+ + to);
+
+ Document doc = opSetPersPresence.createDocument();
+
+ Element rootEl = doc.createElementNS(NS_VALUE, "isComposing");
+ rootEl.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+ doc.appendChild(rootEl);
+
+
+ /*
+ Element contentType = doc.createElement("contenttype");
+ Node contentTypeValue =
+ doc.createTextNode(OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
+ contentType.appendChild(contentTypeValue);
+ rootEl.appendChild(contentType);*/
+
+ if(typingState == STATE_TYPING)
+ {
+ Element state = doc.createElement("state");
+ Node stateValue =
+ doc.createTextNode(COMPOSING_STATE_ACTIVE);
+ state.appendChild(stateValue);
+ rootEl.appendChild(state);
+
+ Element refresh = doc.createElement("refresh");
+ Node refreshValue = doc.createTextNode("60");
+ refresh.appendChild(refreshValue);
+ rootEl.appendChild(refresh);
+ }
+ else if(typingState == STATE_STOPPED)
+ {
+ Element state = doc.createElement("state");
+ Node stateValue =
+ doc.createTextNode(COMPOSING_STATE_IDLE);
+ state.appendChild(stateValue);
+ rootEl.appendChild(state);
+ }
+ else // ignore other events
+ return;
+
+ Message message = opSetBasicIm.createMessage(
+ new String(opSetPersPresence.convertDocument(doc)).getBytes(),
+ CONTENT_TYPE,
+ OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
+ null);
+
+ //create the message
+ Request mes;
+ try
+ {
+ mes = opSetBasicIm.createMessage(to, message);
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error(
+ "Failed to create the message."
+ , ex);
+ return;
+ }
+
+ opSetBasicIm.sendRequestMessage(mes, to, message);
+ }
+
+ private void sendResponse(RequestEvent requestEvent, int response)
+ {
+ // answer
+ try
+ {
+ Response ok = sipProvider.getMessageFactory()
+ .createResponse(response, requestEvent.getRequest());
+ SipProvider jainSipProvider = (SipProvider) requestEvent.
+ getSource();
+ jainSipProvider.getNewServerTransaction(
+ requestEvent.getRequest()).sendResponse(ok);
+ }
+ catch (ParseException exc)
+ {
+ logger.error("failed to build the response", exc);
+ }
+ catch (SipException exc)
+ {
+ logger.error("failed to send the response : "
+ + exc.getMessage(),
+ exc);
+ }
+ catch (InvalidArgumentException exc)
+ {
+ logger.debug("Invalid argument for createResponse : "
+ + exc.getMessage(),
+ exc);
+ }
+ }
+
+ /**
+ * When a message is delivered fire that typing has stoped.
+ * @param evt the received message event
+ */
+ public void messageReceived(MessageReceivedEvent evt)
+ {
+ Contact from = evt.getSourceContact();
+ TypingTask task = findTypigTask(from);
+
+ if(task != null)
+ {
+ task.cancel();
+
+ fireTypingNotificationsEvent(from, STATE_STOPPED);
+ }
+ }
+
+ public void messageDelivered(MessageDeliveredEvent evt)
+ {}
+
+ public void messageDeliveryFailed(MessageDeliveryFailedEvent evt)
+ {}
+
+ /**
+ * Task that will fire typing stoppped when refresh time expires.
+ */
+ private class TypingTask
+ extends TimerTask
+ {
+ Contact typingContact = null;
+
+ TypingTask(Contact typingContact)
+ {
+ this.typingContact = typingContact;
+ }
+
+ public void run()
+ {
+ fireTypingNotificationsEvent(typingContact, STATE_STOPPED);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/sip/ProtocolProviderServiceSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/ProtocolProviderServiceSipImpl.java
index b15c81cf4..07c09b0ae 100644
--- a/src/net/java/sip/communicator/impl/protocol/sip/ProtocolProviderServiceSipImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/sip/ProtocolProviderServiceSipImpl.java
@@ -703,12 +703,19 @@ protected void initialize(String sipAddress,
, opSetPersPresence);
// init instant messaging
- OperationSetBasicInstantMessaging opSetBasicIM =
+ OperationSetBasicInstantMessagingSipImpl opSetBasicIM =
new OperationSetBasicInstantMessagingSipImpl(this);
this.supportedOperationSets.put(
OperationSetBasicInstantMessaging.class.getName(),
opSetBasicIM);
+ // init typing notifications
+ OperationSetTypingNotificationsSipImpl opSetTyping =
+ new OperationSetTypingNotificationsSipImpl(this, opSetBasicIM);
+ this.supportedOperationSets.put(
+ OperationSetTypingNotifications.class.getName(),
+ opSetTyping);
+
// init DTMF (from JM Heitz)
OperationSetDTMF opSetDTMF = new OperationSetDTMFSipImpl(this);
this.supportedOperationSets.put(