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(