mirror of https://github.com/sipwise/jitsi.git
parent
4a18163bbc
commit
ec05a7a407
@ -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 <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.getSupportedOperationSets()
|
||||
.get(OperationSetPersistentPresence.class.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.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);
|
||||
|
||||
// <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();
|
||||
|
||||
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<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(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue