/* * 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.Message; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.xml.*; /** * 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 <tt>Contact</tt>s and vice versa. */ private OperationSetPresenceSipImpl opSetPersPresence = null; /** * A reference to the persistent presence operation set that we use * to match incoming messages to <tt>Contact</tt>s 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<TypingTask> typingTasks = new Vector<TypingTask>(); /** * Creates an instance of this operation set. * @param provider a ref to the <tt>ProtocolProviderServiceImpl</tt> * 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 .getOperationSet(OperationSetPersistentPresence.class); } } } /** * 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. */ 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.typingNotificationReceived(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); // <state> 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; } // <refresh> 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(); // we retrieve the original message String key = ((CallIdHeader)req.getHeader(CallIdHeader.NAME)) .getCallId(); if (status >= 200 && status < 300) { logger.debug( "Ack received from the network : " + responseEvent.getResponse().getReasonPhrase()); // we don't need this message anymore sentMsg.remove(key); return false; } else if (status >= 400 && status != 401 && status != 407) { logger.warn( "Error received : " + responseEvent.getResponse().getReasonPhrase()); // we don't need this message anymore sentMsg.remove(key); return false; } // process messages as auth required return true; } /** * Process the timeouts of sent messages * @param timeoutEvent the event holding the request * @return whether this message needs further processing(true) or no(false) */ public boolean processTimeout(TimeoutEvent timeoutEvent, Map sentMessages) { Request req = timeoutEvent.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; return false; } private TypingTask findTypigTask(Contact contact) { Iterator<TypingTask> tasksIter = typingTasks.iterator(); while (tasksIter.hasNext()) { TypingTask typingTask = tasksIter.next(); if(typingTask.equals(contact)) return typingTask; } return null; } /** * Adds <tt>l</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. */ public void addTypingNotificationsListener( TypingNotificationsListener listener) { synchronized(typingNotificationsListeners) { if(!typingNotificationsListeners.contains(listener)) typingNotificationsListeners.add(listener); } } /** * Removes <tt>l</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); } } 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(opSetPersPresence.convertDocument(doc), 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; } try { opSetBasicIm.sendRequestMessage(mes, to, message); } catch(TransactionUnavailableException ex) { logger.error( "Failed to create messageTransaction.\n" + "This is most probably a network connection error." , ex); return; } catch(SipException ex) { logger.error( "Failed to send the message." , ex); return; } } 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); } } }