/*
* 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.beans.PropertyChangeEvent;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
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.*;
/**
* Sip presence implementation (SIMPLE).
*
* Compliant with rfc3261, rfc3265, rfc3856, rfc3863, rfc4480 and rfc3903
*
* @author Benoit Pradelle
* @author Lubomir Marinov
* @author Emil Ivov
*/
public class OperationSetPresenceSipImpl
extends AbstractOperationSetPersistentPresence<ProtocolProviderServiceSipImpl>
implements MethodProcessor
{
private static final Logger logger =
Logger.getLogger(OperationSetPresenceSipImpl.class);
/**
* A list of listeners registered for
* <tt>ProviderPresenceStatusChangeEvent</tt>s.
*/
private Vector<ProviderPresenceStatusListener>
providerPresenceStatusListeners
= new Vector<ProviderPresenceStatusListener>();
/**
* A list of listeners registered for
* <tt>ServerStoredGroupChangeEvent</tt>s.
*/
private Vector<ServerStoredGroupListener> serverStoredGroupListeners
= new Vector<ServerStoredGroupListener>();
/**
* A list of listeners registered for
* <tt>ContactPresenceStatusChangeEvent</tt>s.
*/
private Vector<ContactPresenceStatusListener> contactPresenceStatusListeners
= new Vector<ContactPresenceStatusListener>();
/**
* The root of the SIP contact list.
*/
private ContactGroupSipImpl contactListRoot = null;
/**
* The currently active status message.
*/
private String statusMessage = "Default Status Message";
/**
* Our default presence status.
*/
private PresenceStatus presenceStatus;
/**
* Hashtable which contains the contacts with which we want to subscribe
* or with which we successfully subscribed
* Index : String, Content : ContactSipImpl
*/
private Hashtable<String, ContactSipImpl> subscribedContacts
= new Hashtable<String, ContactSipImpl>();
/**
* List of all the contact interested by our presence status
* Content : ContactSipImpl
*/
private Vector<ContactSipImpl> ourWatchers = new Vector<ContactSipImpl>();
/**
* List of all the CallIds to wait before unregister
* Content : String
*/
private Vector<String> waitedCallIds = new Vector<String>();
/**
* Do we have to use a distant presence agent (default initial value)
*/
private boolean useDistantPA = false;
/**
* Entity tag associated with the current communication with the distant PA
*/
private String distantPAET = null;
/**
* The default expiration value of a request as defined for the presence
* package in rfc3856. This is the value used when there is no Expires
* header in the received subscription requests.
*/
private static final int PRESENCE_DEFAULT_EXPIRE = 3600;
/**
* The minimal Expires value for a SUBSCRIBE
*/
private static final int SUBSCRIBE_MIN_EXPIRE = 120;
/**
* How many seconds before a timeout should we refresh our state
*/
private static final int REFRESH_MARGIN = 60;
/**
* User chosen expiration value of any of our subscriptions.
* Currently, the value is the default value defined in the rfc.
*/
private int subscriptionDuration = PRESENCE_DEFAULT_EXPIRE;
/**
* The current CSeq used in the PUBLISH requests
*/
private static long publish_cseq = 1;
/**
* The timer which will handle all the scheduled tasks
*/
private Timer timer = null;
/**
* The lock which synchronizes the access to the {@link #timer} handling all
* scheduled tasks.
*/
private final Object timerLock = new Object();
/**
* The re-PUBLISH task if any
*/
private RePublishTask republishTask = null;
/**
* The interval between two execution of the polling task (in ms.)
*/
private int pollingTaskPeriod = 30000;
/**
* The task in charge of polling offline contacts
*/
private PollOfflineContactsTask pollingTask = null;
/**
* If we should be totally silenced, just doing local operations
*/
private boolean presenceEnabled = true;
/**
* The document builder factory for generating document builders
*/
private DocumentBuilderFactory docBuilderFactory = null;
/**
* The document builder which produce xml documents
*/
private DocumentBuilder docBuilder = null;
/**
* The transformer factory used to create transformer
*/
private TransformerFactory transFactory = null;
/**
* The transformer used to convert XML documents
*/
private Transformer transformer = null;
private SipStatusEnum sipStatusEnum = null;
/**
* The id used in <tt><tuple></tt> and <tt><person></tt> elements
* of pidf documents.
*/
private static String tupleid =
String.valueOf("t" + (long)(Math.random() * 10000));
private static String personid =
String.valueOf("p" + (long)(Math.random() * 10000));
// XML documents types
private static final String PIDF_XML = "pidf+xml";
// pidf elements and attributes
private static final String PRESENCE_ELEMENT= "presence";
private static final String NS_ELEMENT = "xmlns";
private static final String NS_VALUE = "urn:ietf:params:xml:ns:pidf";
private static final String ENTITY_ATTRIBUTE= "entity";
private static final String TUPLE_ELEMENT = "tuple";
private static final String ID_ATTRIBUTE = "id";
private static final String STATUS_ELEMENT = "status";
private static final String ONLINE_STATUS = "open";
private static final String OFFLINE_STATUS = "closed";
private static final String BASIC_ELEMENT = "basic";
private static final String CONTACT_ELEMENT = "contact";
private static final String NOTE_ELEMENT = "note";
private static final String PRIORITY_ATTRIBUTE = "priority";
// rpid elements and attributes
private static final String RPID_NS_ELEMENT = "xmlns:rpid";
private static final String RPID_NS_VALUE =
"urn:ietf:params:xml:ns:pidf:rpid";
private static final String DM_NS_ELEMENT = "xmlns:dm";
private static final String DM_NS_VALUE =
"urn:ietf:params:xml:ns:pidf:data-model";
private static final String PERSON_ELEMENT = "person";
private static final String NS_PERSON_ELT = "dm:person";
private static final String ACTIVITY_ELEMENT= "activities";
private static final String NS_ACTIVITY_ELT = "rpid:activities";
private static final String AWAY_ELEMENT = "away";
private static final String NS_AWAY_ELT = "rpid:away";
private static final String BUSY_ELEMENT = "busy";
private static final String NS_BUSY_ELT = "rpid:busy";
private static final String OTP_ELEMENT = "on-the-phone";
private static final String NS_OTP_ELT = "rpid:on-the-phone";
// namespace wildcard
private static final String ANY_NS = "*";
/**
* Creates an instance of this operation set keeping a reference to the
* specified parent <tt>provider</tt>.
* @param provider the ProtocolProviderServiceSipImpl instance that
* created us.
* @param isPresenceEnabled if we are activated or if we don't have to
* handle the presence informations for contacts
* @param forceP2PMode if we should start in the p2p mode directly
* @param pollingPeriod the period between two poll for offline contacts
* @param subscriptionExpiration the default subscription expiration value
* to use
*/
public OperationSetPresenceSipImpl(ProtocolProviderServiceSipImpl provider,
boolean isPresenceEnabled, boolean forceP2PMode, int pollingPeriod,
int subscriptionExpiration)
{
super(provider);
this.contactListRoot = new ContactGroupSipImpl("RootGroup", provider);
//add our registration listener
this.parentProvider.addRegistrationStateChangeListener(
new RegistrationListener());
this.parentProvider.registerMethodProcessor(Request.SUBSCRIBE, this);
this.parentProvider.registerMethodProcessor(Request.NOTIFY, this);
this.parentProvider.registerMethodProcessor(Request.PUBLISH, this);
this.parentProvider.registerEvent("presence");
logger.debug("presence initialized with :" + isPresenceEnabled + ", "
+ forceP2PMode + ", " + pollingPeriod + ", "
+ subscriptionExpiration + " for "
+ provider.getOurDisplayName());
// retrieve the options for this account
if (pollingPeriod > 0)
{
this.pollingTaskPeriod = pollingPeriod * 1000;
}
// if we force the p2p mode, we start by not using a distant PA
this.useDistantPA = !forceP2PMode;
if (subscriptionDuration > 0)
{
this.subscriptionDuration = subscriptionExpiration;
}
this.presenceEnabled = isPresenceEnabled;
this.sipStatusEnum = parentProvider.getSipStatusEnum();
this.presenceStatus = sipStatusEnum.getStatus(SipStatusEnum.OFFLINE);
}
/**
* Returns a PresenceStatus instance representing the state this provider is
* currently in. Note that PresenceStatus instances returned by this method
* MUST adequately represent all possible states that a provider might
* enter during its lifecycle, including those that would not be visible
* to others (e.g. Initializing, Connecting, etc ..) and those that will be
* sent to contacts/buddies (On-Line, Eager to chat, etc.).
*
* @return the PresenceStatus last published by this provider.
*/
public PresenceStatus getPresenceStatus()
{
return this.presenceStatus;
}
/**
* Return true if we use a distant presence agent
*
* @return true if we use a distant presence agent
*/
public boolean usesDistantPA()
{
return this.useDistantPA;
}
/**
* Sets if we should use a distant presence agent
*
* @param useDistPA true if we should use a distant presence agent
*/
public void setDistantPA(boolean useDistPA)
{
this.useDistantPA = useDistPA;
}
/**
* 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(ContactGroupSipImpl source,
int eventID)
{
ServerStoredGroupEvent evt = new ServerStoredGroupEvent(
source, eventID, source.getParentContactGroup(),
this.parentProvider, this);
Iterator listeners = null;
synchronized (this.serverStoredGroupListeners)
{
listeners = new ArrayList(this.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);
}
}
}
/**
* 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 this.contactListRoot;
}
/**
* 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)
{
ContactGroupSipImpl newGroup = new ContactGroupSipImpl(groupName,
this.parentProvider);
((ContactGroupSipImpl) parent).addSubgroup(newGroup);
this.fireServerStoredGroupEvent(newGroup,
ServerStoredGroupEvent.GROUP_CREATED_EVENT);
}
/**
* 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)
{
ContactGroupSipImpl newGroup = new ContactGroupSipImpl(
ContactGroupSipImpl.createNameFromUID(groupUID),
this.parentProvider);
newGroup.setResolved(false);
//if parent is null then we're adding under root.
if(parentGroup == null)
{
parentGroup = getServerStoredContactListRoot();
}
((ContactGroupSipImpl) parentGroup).addSubgroup(newGroup);
this.fireServerStoredGroupEvent(
newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
return newGroup;
}
/**
* 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)
{
((ContactGroupSipImpl) group).setGroupName(newName);
this.fireServerStoredGroupEvent(
(ContactGroupSipImpl) group,
ServerStoredGroupEvent.GROUP_RENAMED_EVENT);
}
/**
* 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)
{
if (!(contactToMove instanceof ContactSipImpl))
{
return;
}
ContactSipImpl sipContact
= (ContactSipImpl)contactToMove;
ContactGroupSipImpl parentSipGroup
= (ContactGroupSipImpl) sipContact.getParentContactGroup();
parentSipGroup.removeContact(sipContact);
((ContactGroupSipImpl) newParent).addContact(sipContact);
fireSubscriptionMovedEvent(contactToMove,
parentSipGroup,
newParent);
}
/**
* 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
{
ContactGroupSipImpl sipGroup = (ContactGroupSipImpl)group;
ContactGroupSipImpl parent = this.contactListRoot
.findGroupParent(sipGroup);
if(parent == null){
throw new IllegalArgumentException(
"group " + group
+ " does not seem to belong to this protocol's contact list.");
}
parent.removeSubGroup(sipGroup);
this.fireServerStoredGroupEvent(sipGroup,
ServerStoredGroupEvent.GROUP_REMOVED_EVENT);
}
/**
* Requests the provider to enter into a status corresponding to the
* specified parameters.
*
* @param status the PresenceStatus as returned by
* getRequestableStatusSet
* @param statusMsg 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 statusMsg)
throws IllegalArgumentException,
IllegalStateException,
OperationFailedException
{
PresenceStatus oldStatus = this.presenceStatus;
this.presenceStatus = status;
String oldMessage = this.statusMessage;
this.statusMessage = statusMsg;
if (this.presenceEnabled == false)
{
return;
}
// in the offline status, the protocol provider is already unregistered
if (!status.equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)))
{
assertConnected();
}
// now inform our distant presence agent if we have one
if (this.useDistantPA)
{
Request req = null;
if (status.equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)))
{
// unpublish our state
req = createPublish(0, false);
// remember the callid to be sure that the publish arrived
// before unregister
synchronized (this.waitedCallIds)
{
this.waitedCallIds.add(((CallIdHeader)
req.getHeader(CallIdHeader.NAME)).getCallId());
}
}
else
{
req = createPublish(this.subscriptionDuration, true);
}
ClientTransaction transac = null;
try
{
transac = this.parentProvider
.getDefaultJainSipProvider().getNewClientTransaction(req);
}
catch (TransactionUnavailableException e)
{
logger.error("can't create the client transaction", e);
throw new OperationFailedException(
"can't create the client transaction",
OperationFailedException.NETWORK_FAILURE);
}
try
{
transac.sendRequest();
}
catch (SipException e)
{
logger.error("can't send the PUBLISH request", e);
throw new OperationFailedException(
"can't send the PUBLISH request",
OperationFailedException.NETWORK_FAILURE);
}
}
// no distant presence agent, send notify to every one
else
{
synchronized (this.ourWatchers)
{ // avoid any modification during
// the parsing of ourWatchers
Iterator<ContactSipImpl> iter = this.ourWatchers.iterator();
while (iter.hasNext())
{
ContactSipImpl contact = (ContactSipImpl) iter.next();
ContactSipImpl me = getLocalContactForDst(contact);
// let the subscription end before sending him a new status
if (!contact.isResolved())
{
continue;
}
ClientTransaction transac = null;
try
{
if (status.equals(sipStatusEnum.getStatus(
SipStatusEnum.OFFLINE)))
{
transac = createNotify(contact,
getPidfPresenceStatus(me),
SubscriptionStateHeader.TERMINATED,
SubscriptionStateHeader.PROBATION);
// register the callid to wait it before unregister
synchronized (this.waitedCallIds)
{
this.waitedCallIds.add(transac.getDialog()
.getCallId().getCallId());
}
}
else
{
transac = createNotify(contact,
getPidfPresenceStatus(me),
SubscriptionStateHeader.ACTIVE, null);
}
}
catch (OperationFailedException e)
{
logger.error("failed to create the new notify", e);
return;
}
try
{
contact.getServerDialog().sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return;
}
}
if (status.equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)))
{
this.ourWatchers.removeAllElements();
}
}
}
// must be done in last to avoid some problem when terminating a
// subscription of a contact who is also one of our watchers
if (status.equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)))
{
unsubscribeToAllContact();
}
// inform the listeners of these changes
this.fireProviderStatusChangeEvent(oldStatus);
this.fireProviderMsgStatusChangeEvent(oldMessage);
}
/**
* Notifies all registered listeners of the new event.
*
* @param oldValue the presence status we were in before the change.
*/
private void fireProviderStatusChangeEvent(PresenceStatus oldValue)
{
ProviderPresenceStatusChangeEvent evt
= new ProviderPresenceStatusChangeEvent(this.parentProvider,
oldValue, this.getPresenceStatus());
logger.debug("Dispatching Provider Status Change. Listeners="
+ this.providerPresenceStatusListeners.size()
+ " evt=" + evt);
Iterator listeners = null;
synchronized (this.providerPresenceStatusListeners)
{
listeners = new ArrayList(this.providerPresenceStatusListeners)
.iterator();
}
while (listeners.hasNext())
{
ProviderPresenceStatusListener listener
= (ProviderPresenceStatusListener) listeners.next();
listener.providerStatusChanged(evt);
}
logger.debug("status dispatching done.");
}
/**
* Notifies all registered listeners of the new event.
*
* @param oldValue the presence status we were in before the change.
*/
public void fireProviderMsgStatusChangeEvent(String oldValue)
{
PropertyChangeEvent evt = new PropertyChangeEvent(
this.parentProvider,
ProviderPresenceStatusListener.STATUS_MESSAGE,
oldValue,
this.statusMessage);
logger.debug("Dispatching stat. msg change. Listeners="
+ this.providerPresenceStatusListeners.size()
+ " evt=" + evt);
Iterator<ProviderPresenceStatusListener> listeners = null;
synchronized (this.providerPresenceStatusListeners)
{
listeners = new ArrayList<ProviderPresenceStatusListener>(
this.providerPresenceStatusListeners).iterator();
}
while (listeners.hasNext())
{
ProviderPresenceStatusListener listener =
(ProviderPresenceStatusListener) listeners.next();
listener.providerStatusMessageChanged(evt);
}
logger.debug("status dispatching done.");
}
/**
* Create a valid PUBLISH request corresponding to the current presence
* state. The request is forged to be send to the current distant presence
* agent.
*
* @param expires the expires value to send
* @param insertPresDoc if a presence document has to be added (typically
* = false when refreshing a publication)
*
* @return a valid <tt>Request</tt> containing the PUBLISH
*
* @throws OperationFailedException if something goes wrong
*/
private Request createPublish(int expires, boolean insertPresDoc)
throws OperationFailedException
{
// Call ID
CallIdHeader callIdHeader = this.parentProvider
.getDefaultJainSipProvider().getNewCallId();
// FromHeader and ToHeader
String localTag = ProtocolProviderServiceSipImpl.generateLocalTag();
FromHeader fromHeader = null;
ToHeader toHeader = null;
try
{
//the publish method can only be used if we have a presence agent
//so we deliberately use our AOR and do not use the
//getOurSipAddress() method.
Address ourAOR = parentProvider.getRegistrarConnection()
.getAddressOfRecord();
//FromHeader
fromHeader = this.parentProvider.getHeaderFactory()
.createFromHeader(ourAOR,
localTag);
//ToHeader (it's ourselves)
toHeader = this.parentProvider.getHeaderFactory()
.createToHeader(ourAOR, null);
}
catch (ParseException ex)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the FromHeader or ToHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the FromHeader or ToHeader",
OperationFailedException.INTERNAL_ERROR,
ex);
}
//ViaHeaders
ArrayList<ViaHeader> viaHeaders = parentProvider.getLocalViaHeaders(
toHeader.getAddress());
//MaxForwards
MaxForwardsHeader maxForwards = this.parentProvider
.getMaxForwardsHeader();
// Content params
byte[] doc = null;
if (insertPresDoc)
{
//this is a publish request so we would use the default
//getLocalContact that would return a method based on the registrar
//address
doc = getPidfPresenceStatus((ContactSipImpl)
this.getLocalContactForDst(toHeader.getAddress()));
}
else
{
doc = new byte[0];
}
ContentTypeHeader contTypeHeader;
try
{
contTypeHeader = this.parentProvider.getHeaderFactory()
.createContentTypeHeader("application",
PIDF_XML);
}
catch (ParseException ex)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the content headers", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the content headers"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
// eventually add the entity tag
SIPIfMatchHeader ifmHeader = null;
try
{
if (this.distantPAET != null)
{
ifmHeader = this.parentProvider.getHeaderFactory()
.createSIPIfMatchHeader(this.distantPAET);
}
}
catch (ParseException e)
{
logger.error(
"An unexpected error occurred while"
+ "constructing the SIPIfMatch header", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the SIPIfMatch header",
OperationFailedException.INTERNAL_ERROR,
e);
}
//CSeq
CSeqHeader cSeqHeader = null;
try
{
cSeqHeader = this.parentProvider.getHeaderFactory()
.createCSeqHeader(publish_cseq++, Request.PUBLISH);
}
catch (InvalidArgumentException ex)
{
//Shouldn't happen
logger.error(
"An unexpected error occurred while"
+ "constructing the CSeqHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the CSeqHeader"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
catch (ParseException ex)
{
//shouldn't happen
logger.error(
"An unexpected error occurred while"
+ "constructing the CSeqHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the CSeqHeader"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
// expires
ExpiresHeader expHeader = null;
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(expires);
}
catch (InvalidArgumentException e)
{
// will never happen
logger.error(
"An unexpected error occurred while"
+ "constructing the Expires header", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the Expires header"
, OperationFailedException.INTERNAL_ERROR
, e);
}
// event
EventHeader evtHeader = null;
try
{
evtHeader = this.parentProvider.getHeaderFactory()
.createEventHeader("presence");
}
catch (ParseException e)
{
// will never happen
logger.error(
"An unexpected error occurred while"
+ "constructing the Event header", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the Event header"
, OperationFailedException.INTERNAL_ERROR
, e);
}
Request req = null;
try
{
req = this.parentProvider.getMessageFactory().createRequest(
toHeader.getAddress().getURI(),
Request.PUBLISH,
callIdHeader,
cSeqHeader,
fromHeader,
toHeader,
viaHeaders,
maxForwards,
contTypeHeader,
doc);
}
catch (ParseException ex)
{
//shouldn't happen
logger.error(
"Failed to create message Request!", ex);
throw new OperationFailedException(
"Failed to create message Request!"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
req.setHeader(expHeader);
req.setHeader(evtHeader);
if (ifmHeader != null)
{
req.setHeader(ifmHeader);
}
//check whether there's a cached authorization header for this
//call id and if so - attach it to the request.
// add authorization header
CallIdHeader call = (CallIdHeader)req
.getHeader(CallIdHeader.NAME);
String callid = call.getCallId();
AuthorizationHeader authorization = parentProvider
.getSipSecurityManager()
.getCachedAuthorizationHeader(callid);
if(authorization != null)
req.addHeader(authorization);
//User Agent
UserAgentHeader userAgentHeader
= parentProvider.getSipCommUserAgentHeader();
if(userAgentHeader != null)
req.addHeader(userAgentHeader);
return req;
}
/**
* Returns the set of PresenceStatus objects that a user of this service
* may request the provider to enter. Note that the provider would most
* probably enter more states than those returned by this method as they
* only depict instances that users may request to enter. (e.g. a user
* may not request a "Connecting..." state - it is a temporary state
* that the provider enters while trying to enter the "Connected" state).
*
* @return Iterator a PresenceStatus array containing "enterable"
* status instances.
*/
public Iterator getSupportedStatusSet()
{
return sipStatusEnum.getSupportedStatusSet();
}
/**
* 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
{
Contact contact = resolveContactID(contactIdentifier);
if (contact == null)
{
throw new IllegalArgumentException("contact " + contactIdentifier
+ " unknown");
}
return contact.getPresenceStatus();
}
/**
* 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(this.contactListRoot, contactIdentifier);
}
/**
* 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 parentGroup 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 parentGroup, String contactIdentifier)
throws IllegalArgumentException,
IllegalStateException,
OperationFailedException
{
logger.debug("let's subscribe " + contactIdentifier);
//if the contact is already in the contact list
ContactSipImpl contact = (ContactSipImpl)
resolveContactID(contactIdentifier);
if (contact != null)
{
throw new OperationFailedException(
"Contact " + contactIdentifier + " already exists.",
OperationFailedException.SUBSCRIPTION_ALREADY_EXISTS);
}
Address contactAddress;
try
{
contactAddress = parentProvider
.parseAddressString(contactIdentifier);
}
catch (ParseException exc)
{
throw new IllegalArgumentException(
contactIdentifier + " is not a valid string.", exc);
}
// create a new contact, marked as resolvable and non resolved
contact = new ContactSipImpl(contactAddress, this.parentProvider);
((ContactGroupSipImpl) parentGroup).addContact(contact);
fireSubscriptionEvent(contact,
parentGroup,
SubscriptionEvent.SUBSCRIPTION_CREATED);
// do not query the presence state
if (this.presenceEnabled == false)
return;
assertConnected();
//create the subscription
Request subscription;
try
{
subscription = createSubscription(contact,
this.subscriptionDuration);
}
catch (OperationFailedException ex)
{
logger.error(
"Failed to create the subcription",
ex);
throw new OperationFailedException(
"Failed to create the subscription",
OperationFailedException.INTERNAL_ERROR);
}
//Transaction
ClientTransaction subscribeTransaction;
SipProvider jainSipProvider
= this.parentProvider.getDefaultJainSipProvider();
try
{
subscribeTransaction = jainSipProvider
.getNewClientTransaction(subscription);
}
catch (TransactionUnavailableException ex)
{
logger.error(
"Failed to create subscriptionTransaction.\n"
+ "This is most probably a network connection error.",
ex);
throw new OperationFailedException(
"Failed to create the subscription transaction",
OperationFailedException.NETWORK_FAILURE);
}
// we register the contact to find him when the OK will arrive
CallIdHeader idheader = (CallIdHeader)
subscription.getHeader(CallIdHeader.NAME);
this.subscribedContacts.put(idheader.getCallId(), contact);
// send the message
try
{
subscribeTransaction.sendRequest();
}
catch (SipException ex)
{
logger.error(
"Failed to send the message.",
ex);
// this contact will never been accepted or rejected
this.subscribedContacts.remove(idheader.getCallId());
throw new OperationFailedException(
"Failed to send the subscription",
OperationFailedException.NETWORK_FAILURE);
}
}
/**
* Creates a new SUBSCRIBE message with the provided parameters.
*
* @param contact The contact concerned by this subscription
* @param expires The expires value
*
* @return a valid sip request representing this message.
*
* @throws OperationFailedException if the message can't be generated
*/
private Request createSubscription(ContactSipImpl contact, int expires)
throws OperationFailedException
{
Address toAddress = null;
try
{
toAddress = parentProvider
.parseAddressString(contact.getAddress());
}
catch (ParseException ex)
{
//Shouldn't happen
logger.error(
"An unexpected error occurred while"
+ "constructing the address", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the address"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
Request req;
// Call ID
CallIdHeader callIdHeader = this.parentProvider
.getDefaultJainSipProvider().getNewCallId();
//CSeq
CSeqHeader cSeqHeader = null;
try
{
cSeqHeader = this.parentProvider.getHeaderFactory()
.createCSeqHeader(1l, Request.SUBSCRIBE);
}
catch (InvalidArgumentException ex)
{
//Shouldn't happen
logger.error(
"An unexpected error occurred while"
+ "constructing the CSeqHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the CSeqHeader"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
catch (ParseException ex)
{
//shouldn't happen
logger.error(
"An unexpected error occurred while"
+ "constructing the CSeqHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the CSeqHeader"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
//FromHeader and ToHeader
String localTag = ProtocolProviderServiceSipImpl.generateLocalTag();
FromHeader fromHeader = null;
ToHeader toHeader = null;
try
{
//FromHeader
fromHeader = this.parentProvider.getHeaderFactory()
.createFromHeader(
this.parentProvider.getOurSipAddress(toAddress), localTag);
//ToHeader
toHeader = this.parentProvider.getHeaderFactory()
.createToHeader(toAddress, null);
}
catch (ParseException ex)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the FromHeader or ToHeader", ex);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the FromHeader or ToHeader"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
//ViaHeaders
ArrayList<ViaHeader> viaHeaders = this.parentProvider
.getLocalViaHeaders(toAddress);
//MaxForwards
MaxForwardsHeader maxForwards = this.parentProvider
.getMaxForwardsHeader();
try
{
req = this.parentProvider.getMessageFactory().createRequest(
toHeader.getAddress().getURI(),
Request.SUBSCRIBE,
callIdHeader,
cSeqHeader,
fromHeader,
toHeader,
viaHeaders,
maxForwards);
}
catch (ParseException ex)
{
//shouldn't happen
logger.error(
"Failed to create message Request!", ex);
throw new OperationFailedException(
"Failed to create message Request!"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
// Event
EventHeader evHeader = null;
try
{
evHeader = this.parentProvider.getHeaderFactory()
.createEventHeader("presence");
}
catch (ParseException e)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the EventHeader", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the EventHeader"
, OperationFailedException.INTERNAL_ERROR
, e);
}
// Contact
ContactHeader contactHeader = this.parentProvider.getContactHeader(
toAddress);
req.setHeader(evHeader);
req.setHeader(contactHeader);
// Accept
AcceptHeader accept = null;
try
{
accept = this.parentProvider.getHeaderFactory()
.createAcceptHeader("application", PIDF_XML);
}
catch (ParseException e)
{
logger.error("wrong accept header", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the AcceptHeader",
OperationFailedException.INTERNAL_ERROR,
e);
}
req.setHeader(accept);
// Expires
ExpiresHeader expHeader = null;
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(expires);
}
catch (InvalidArgumentException e)
{
logger.error("Invalid expires value: " + expires, e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the ExpiresHeader",
OperationFailedException.INTERNAL_ERROR,
e);
}
req.setHeader(expHeader);
//check whether there's a cached authorization header for this
//call id and if so - attach it to the request.
// add authorization header
CallIdHeader call = (CallIdHeader)req.getHeader(CallIdHeader.NAME);
String callid = call.getCallId();
AuthorizationHeader authorization = parentProvider
.getSipSecurityManager()
.getCachedAuthorizationHeader(callid);
if(authorization != null)
req.addHeader(authorization);
//User Agent
UserAgentHeader userAgentHeader
= parentProvider.getSipCommUserAgentHeader();
if(userAgentHeader != null)
req.addHeader(userAgentHeader);
return req;
}
/**
* Creates a new SUBSCRIBE message with the provided parameters.
*
* @param expires The expires value
* @param dialog The dialog with which this request should be associated
* or null if this request has to create a new dialog
*
* @return a <tt>ClientTransaction</tt> which may be used with
* <tt>dialog.sendRequest(ClientTransaction)</tt> for send the request.
*
* @throws OperationFailedException if the message can't be generated
*/
private ClientTransaction createSubscription(int expires, Dialog dialog)
throws OperationFailedException
{
Request req = null;
try
{
req = dialog.createRequest(Request.SUBSCRIBE);
}
catch (SipException e)
{
logger.error("Can't create the SUBSCRIBE message", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the SUBSCRIBE request"
, OperationFailedException.INTERNAL_ERROR
, e);
}
// Address
Address toAddress = dialog.getRemoteTarget();
// no Contact field
if (toAddress == null)
{
toAddress = dialog.getRemoteParty();
}
//MaxForwards
MaxForwardsHeader maxForwards = this.parentProvider
.getMaxForwardsHeader();
// EventHeader
EventHeader evHeader = null;
try
{
evHeader = this.parentProvider.getHeaderFactory()
.createEventHeader("presence");
}
catch (ParseException e)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the EventHeader", e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the EventHeader"
, OperationFailedException.INTERNAL_ERROR
, e);
}
// Contact
ContactHeader contactHeader = this.parentProvider
.getContactHeader(toAddress);
// Accept
AcceptHeader accept = null;
try
{
accept = this.parentProvider.getHeaderFactory()
.createAcceptHeader("application", PIDF_XML);
}
catch (ParseException e)
{
logger.error("wrong accept header");
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the AcceptHeader",
OperationFailedException.INTERNAL_ERROR,
e);
}
// Expires
ExpiresHeader expHeader = null;
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(expires);
}
catch (InvalidArgumentException e)
{
logger.error("Invalid expires value: " + expires, e);
throw new OperationFailedException(
"An unexpected error occurred while"
+ "constructing the ExpiresHeader",
OperationFailedException.INTERNAL_ERROR,
e);
}
req.setHeader(expHeader);
req.setHeader(accept);
req.setHeader(maxForwards);
req.setHeader(evHeader);
req.setHeader(contactHeader);
//check whether there's a cached authorization header for this
//call id and if so - attach it to the request.
// add authorization header
CallIdHeader call = (CallIdHeader)req.getHeader(CallIdHeader.NAME);
String callid = call.getCallId();
AuthorizationHeader authorization = parentProvider
.getSipSecurityManager()
.getCachedAuthorizationHeader(callid);
if(authorization != null)
req.addHeader(authorization);
// create the transaction (then add the via header as recommended
// by the jain-sip documentation at:
// http://snad.ncsl.nist.gov/proj/iptel/jain-sip-1.2
// /javadoc/javax/sip/Dialog.html#createRequest(java.lang.String))
ClientTransaction transac = null;
try
{
transac = this.parentProvider.getDefaultJainSipProvider()
.getNewClientTransaction(req);
}
catch (TransactionUnavailableException ex)
{
logger.error(
"Failed to create subscriptionTransaction.\n"
+ "This is most probably a network connection error."
, ex);
throw new OperationFailedException(
"Failed to create the subscription transaction",
OperationFailedException.NETWORK_FAILURE);
}
//ViaHeaders
ArrayList<ViaHeader> viaHeaders = this.parentProvider
.getLocalViaHeaders(toAddress);
req.setHeader((Header) viaHeaders.get(0));
//User Agent
UserAgentHeader userAgentHeader
= parentProvider.getSipCommUserAgentHeader();
if(userAgentHeader != null)
req.addHeader(userAgentHeader);
return transac;
}
/**
* 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.parentProvider == null)
throw new IllegalStateException(
"The provider must be non-null and signed on the "
+ "service before being able to communicate.");
if (!this.parentProvider.isRegistered())
throw new IllegalStateException(
"The provider must be signed on the service before "
+ "being able to communicate.");
}
/**
* Removes a subscription for the presence status of the specified contact.
* @param contact the contact whose status updates we are unsubscribing
* from.
*
* @throws OperationFailedException with code NETWORK_FAILURE if
* unsubscribing fails due to errors experienced during network
* communication
* @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.
*/
public void unsubscribe(Contact contact)
throws IllegalArgumentException,
IllegalStateException,
OperationFailedException
{
if (!(contact instanceof ContactSipImpl))
{
throw new IllegalArgumentException("the contact is not a SIP" +
" contact");
}
ContactSipImpl sipcontact = (ContactSipImpl) contact;
Dialog dialog = sipcontact.getClientDialog();
// handle the case of a distant presence agent is used
// and test if we are subscribed to this contact
if (dialog != null && this.presenceEnabled)
{
// check if we heard about this contact
if (this.subscribedContacts.get(dialog.getCallId().getCallId())
== null)
{
throw new IllegalArgumentException("trying to unregister a " +
"not registered contact");
}
// we stop the subscription if we're subscribed to this contact
if (sipcontact.isResolvable())
{
assertConnected();
ClientTransaction transac = null;
try
{
transac = createSubscription(0, dialog);
}
catch (OperationFailedException e)
{
logger.debug("failed to create the unsubscription", e);
throw e;
}
// we are not anymore subscribed to this contact
// this ensure that the response of this request will be
// handled as an unsubscription response
this.subscribedContacts.remove(dialog.getCallId().getCallId());
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.debug("Can't send the request");
throw new OperationFailedException(
"Failed to send the subscription message",
OperationFailedException.NETWORK_FAILURE);
}
}
}
// remove any trace of this contact
terminateSubscription(sipcontact);
((ContactGroupSipImpl) sipcontact.getParentContactGroup())
.removeContact(sipcontact);
// inform the listeners
fireSubscriptionEvent(sipcontact,
sipcontact.getParentContactGroup(),
SubscriptionEvent.SUBSCRIPTION_REMOVED);
}
/**
* Analyzes the incoming <tt>responseEvent</tt> and then forwards it to the
* proper event handler.
*
* @param responseEvent the responseEvent that we received
* ProtocolProviderService.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processResponse(ResponseEvent responseEvent)
{
if (this.presenceEnabled == false)
{
return false;
}
ClientTransaction clientTransaction = responseEvent
.getClientTransaction();
Response response = responseEvent.getResponse();
CSeqHeader cseq = ((CSeqHeader)response.getHeader(CSeqHeader.NAME));
if (cseq == null)
{
logger.error("An incoming response did not contain a CSeq header");
return false;
}
String method = cseq.getMethod();
SipProvider sourceProvider = (SipProvider)responseEvent.getSource();
boolean processed = false;
// SUBSCRIBE
if (method.equals(Request.SUBSCRIBE))
{
// find the contact
CallIdHeader idheader = (CallIdHeader)
response.getHeader(CallIdHeader.NAME);
ContactSipImpl contact = (ContactSipImpl) this.subscribedContacts
.get(idheader.getCallId());
// if it's the response to an unsubscribe message, we just ignore it
// whatever the response is however if we need to handle a
// challenge, we do it
ExpiresHeader expHeader = response.getExpires();
if ((expHeader != null && expHeader.getExpires() == 0)
|| contact == null) // this handle the unsubscription case
// where we removed the contact from
// subscribedContacts
{
if (response.getStatusCode() == Response.UNAUTHORIZED
|| response.getStatusCode() ==
Response.PROXY_AUTHENTICATION_REQUIRED)
{
try
{
processAuthenticationChallenge(clientTransaction,
response, sourceProvider);
processed = true;
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
}
}
else if (response.getStatusCode() != Response.OK
&& response.getStatusCode() != Response.ACCEPTED)
{
// this definitivly ends the subscription
synchronized (this.waitedCallIds)
{
this.waitedCallIds.remove(idheader.getCallId());
}
processed = true;
}
// any other cases (200/202) will imply a NOTIFY, so we will
// handle the end of a subscription there
return processed;
}
if(response.getStatusCode() >= Response.OK &&
response.getStatusCode() < Response.MULTIPLE_CHOICES)
{
// OK (200/202)
if (response.getStatusCode() == Response.OK
|| response.getStatusCode() == Response.ACCEPTED)
{
if (expHeader == null)
{
// not conform to rfc3265
logger.error("no Expires header in this response");
return false;
}
if (contact.getResfreshTask() != null)
{
contact.getResfreshTask().cancel();
}
RefreshSubscriptionTask refresh =
new RefreshSubscriptionTask(contact);
contact.setResfreshTask(refresh);
int refreshDelay = expHeader.getExpires();
// try to keep a margin
if (refreshDelay >= REFRESH_MARGIN)
refreshDelay -= REFRESH_MARGIN;
getTimer().schedule(refresh, refreshDelay * 1000);
// do it to remember the dialog in case of a polling
// subscription (which means no call to finalizeSubscription)
contact.setClientDialog(clientTransaction.getDialog());
try
{
if (!contact.isResolved())
{
// if contact is not in the contact list
// create it, and add to parent, later will be resolved
if(resolveContactID(contact.getAddress()) == null)
{
ContactGroup parentGroup =
contact.getParentContactGroup();
((ContactGroupSipImpl) parentGroup).
addContact(contact);
// pretend that the contact is created
fireSubscriptionEvent(contact,
parentGroup,
SubscriptionEvent.SUBSCRIPTION_CREATED);
}
finalizeSubscription(contact,
clientTransaction.getDialog());
}
}
catch (NullPointerException e)
{
// should not happen
logger.debug("failed to finalize the subscription of the" +
"contact", e);
return false;
}
}
}
else if(response.getStatusCode() >= Response.MULTIPLE_CHOICES &&
response.getStatusCode() < Response.BAD_REQUEST)
{
logger.info("Response to Subscribe of contact: " + contact +
" - " + response.getReasonPhrase());
}
else if(response.getStatusCode() >= Response.BAD_REQUEST)
{
// if the response is a 423 response, just re-send the request
// with a valid expires value
if (response.getStatusCode() == Response.INTERVAL_TOO_BRIEF)
{
MinExpiresHeader min = (MinExpiresHeader)
response.getHeader(MinExpiresHeader.NAME);
if (min == null)
{
logger.error("no minimal expires value in this 423 " +
"response");
return false;
}
Request request = responseEvent.getClientTransaction()
.getRequest();
ExpiresHeader exp = request.getExpires();
try
{
exp.setExpires(min.getExpires());
}
catch (InvalidArgumentException e)
{
logger.error("can't set the new expires value", e);
return false;
}
ClientTransaction transac = null;
try
{
transac = this.parentProvider.getDefaultJainSipProvider()
.getNewClientTransaction(request);
}
catch (TransactionUnavailableException e)
{
logger.error("can't create the client transaction", e);
return false;
}
try
{
transac.sendRequest();
}
catch (SipException e)
{
logger.error("can't send the new request", e);
return false;
}
return true;
// UNAUTHORIZED (401/407)
}
else if (response.getStatusCode() == Response.UNAUTHORIZED
|| response.getStatusCode() == Response
.PROXY_AUTHENTICATION_REQUIRED)
{
try
{
processAuthenticationChallenge(clientTransaction,
response, sourceProvider);
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
// we probably won't be able to communicate with the contact
changePresenceStatusForContact(contact,
sipStatusEnum.getStatus(SipStatusEnum.UNKNOWN));
this.subscribedContacts.remove(idheader.getCallId());
contact.setClientDialog(null);
}
// 408 480 486 600 603 : non definitive reject
// others: definitive reject (or not implemented)
}
else
{
logger.debug("error received from the network:\n"
+ response);
if (response.getStatusCode() == Response
.TEMPORARILY_UNAVAILABLE)
{
changePresenceStatusForContact(contact,
sipStatusEnum.getStatus(SipStatusEnum.OFFLINE));
}
else
{
changePresenceStatusForContact(contact,
sipStatusEnum.getStatus(SipStatusEnum.UNKNOWN));
}
this.subscribedContacts.remove(idheader.getCallId());
contact.setClientDialog(null);
// we'll never be able to resolve this contact
contact.setResolvable(false);
}
}
processed = true;
}
// NOTIFY
else if (method.equals(Request.NOTIFY))
{
/*
* Make sure we're working only on responses to NOTIFY requests
* we're interested in.
*/
Request notifyRequest = clientTransaction.getRequest();
if (notifyRequest != null)
{
EventHeader eventHeader =
(EventHeader) notifyRequest.getHeader(EventHeader.NAME);
if ((eventHeader == null)
|| !"presence".equalsIgnoreCase(eventHeader.getEventType()))
{
return false;
}
}
// if it's a final response to a NOTIFY, we try to remove it from
// the list of waited NOTIFY end
if (response.getStatusCode() != Response.UNAUTHORIZED
&& response.getStatusCode() != Response
.PROXY_AUTHENTICATION_REQUIRED)
{
synchronized (this.waitedCallIds)
{
this.waitedCallIds.remove(((CallIdHeader) response
.getHeader(CallIdHeader.NAME)).getCallId());
}
}
// OK (200)
if (response.getStatusCode() == Response.OK)
{
// simply nothing to do here, the contact received our NOTIFY,
// everything is ok
// UNAUTHORIZED (401/407)
}
else if (response.getStatusCode() == Response.UNAUTHORIZED
|| response.getStatusCode() == Response
.PROXY_AUTHENTICATION_REQUIRED)
{
try
{
processAuthenticationChallenge(clientTransaction,
response, sourceProvider);
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
// don't try to tell him anything more
String contactAddress = ((FromHeader)
response.getHeader(FromHeader.NAME)).getAddress()
.getURI().toString();
Contact watcher = getWatcher(contactAddress);
if (watcher != null)
{
// avoid the case where we receive an error after having
// close an old subscription before accepting a new one
// from the same contact
synchronized (watcher)
{
if (((ContactSipImpl) watcher).getServerDialog()
.equals(clientTransaction.getDialog()))
{
synchronized (this.ourWatchers)
{
this.ourWatchers.remove(watcher);
}
}
}
}
}
// every error cause the subscription to be removed
// as recommended in rfc3265
}
else
{
logger.debug("error received from the network" + response);
String contactAddress = ((FromHeader)
response.getHeader(FromHeader.NAME)).getAddress()
.getURI().toString();
Contact watcher = getWatcher(contactAddress);
if (watcher != null)
{
// avoid the case where we receive an error after having
// close an old subscription before accepting a new one
// from the same contact
synchronized (watcher)
{
if (((ContactSipImpl) watcher).getServerDialog()
.equals(clientTransaction.getDialog()))
{
synchronized (this.ourWatchers)
{
this.ourWatchers.remove(watcher);
}
}
}
}
}
processed = true;
// PUBLISH
}
else if (method.equals(Request.PUBLISH))
{
// if it's a final response to a PUBLISH, we try to remove it from
// the list of waited PUBLISH end
if (response.getStatusCode() != Response.UNAUTHORIZED
&& response.getStatusCode() != Response
.PROXY_AUTHENTICATION_REQUIRED
&& response.getStatusCode() != Response.INTERVAL_TOO_BRIEF)
{
synchronized (this.waitedCallIds)
{
this.waitedCallIds.remove(((CallIdHeader) response
.getHeader(CallIdHeader.NAME)).getCallId());
}
}
// OK (200)
if (response.getStatusCode() == Response.OK)
{
// remember the entity tag
SIPETagHeader etHeader = (SIPETagHeader)
response.getHeader(SIPETagHeader.NAME);
// must be one (rfc3903)
if (etHeader == null)
{
logger.debug("can't find the ETag header");
return false;
}
this.distantPAET = etHeader.getETag();
// schedule a re-publish task
ExpiresHeader expires = (ExpiresHeader)
response.getHeader(ExpiresHeader.NAME);
if (expires == null)
{
logger.error("no Expires header in the response");
return false;
}
// if it's a response to an unpublish request (Expires: 0),
// invalidate the etag and don't schedule a republish
if (expires.getExpires() == 0)
{
this.distantPAET = null;
return true;
}
// just to be sure to not have two refreshing task
if (this.republishTask != null)
{
this.republishTask.cancel();
}
this.republishTask = new RePublishTask();
int republishDelay = expires.getExpires();
// keep a margin
if (republishDelay >= REFRESH_MARGIN)
republishDelay -= REFRESH_MARGIN;
getTimer().schedule(this.republishTask, republishDelay * 1000);
// UNAUTHORIZED (401/407)
}
else if (response.getStatusCode() == Response.UNAUTHORIZED
|| response.getStatusCode() == Response
.PROXY_AUTHENTICATION_REQUIRED)
{
try
{
processAuthenticationChallenge(clientTransaction,
response, sourceProvider);
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
return false;
}
// INTERVAL TOO BRIEF (423)
}
else if (response.getStatusCode() == Response.INTERVAL_TOO_BRIEF)
{
// we get the Min expires and we use it as the interval
MinExpiresHeader min = (MinExpiresHeader)
response.getHeader(MinExpiresHeader.NAME);
if (min == null)
{
logger.error("can't find a min expires header in the 423" +
" error message");
return false;
}
// send a new publish with the new expires value
Request req = null;
try
{
req = createPublish(min.getExpires(), true);
}
catch (OperationFailedException e)
{
logger.error("can't create the new publish request", e);
return false;
}
ClientTransaction transac = null;
try
{
transac = this.parentProvider
.getDefaultJainSipProvider()
.getNewClientTransaction(req);
}
catch (TransactionUnavailableException e)
{
logger.error("can't create the client transaction", e);
return false;
}
try
{
transac.sendRequest();
}
catch (SipException e)
{
logger.error("can't send the PUBLISH request", e);
return false;
}
// CONDITIONAL REQUEST FAILED (412)
}
else if (response.getStatusCode() == Response
.CONDITIONAL_REQUEST_FAILED)
{
// as recommanded in rfc3903#5, we start a totally new
// publication
this.distantPAET = null;
Request req = null;
try
{
req = createPublish(this.subscriptionDuration, true);
}
catch (OperationFailedException e)
{
logger.error("can't create the new publish request", e);
return false;
}
ClientTransaction transac = null;
try
{
transac = this.parentProvider
.getDefaultJainSipProvider()
.getNewClientTransaction(req);
}
catch (TransactionUnavailableException e)
{
logger.error("can't create the client transaction", e);
return false;
}
try
{
transac.sendRequest();
}
catch (SipException e)
{
logger.error("can't send the PUBLISH request", e);
return false;
}
// with every other error, we consider that we have to start a new
// communication.
// Enter p2p mode if the distant PA mode fails
}
else
{
logger.debug("error received from the network" + response);
this.distantPAET = null;
if (this.useDistantPA == false)
{
return true;
}
logger.debug("we enter into the peer to peer mode as the "
+ "distant PA mode fails");
this.useDistantPA = false;
if (this.republishTask != null)
{
this.republishTask.cancel();
this.republishTask = null;
}
// if we are there, we don't have any watcher so no need to
// republish our presence state
}
processed = true;
}
return processed;
}
/**
* Finalize the subscription of a contact and transform the pending contact
* into a real contact.
*
* @param contact the contact concerned
* @param dialog the dialog which will be used to communicate with this
* contact for retrieving its status
*
* @throws NullPointerException if dialog or contact is null
*/
private void finalizeSubscription(ContactSipImpl contact, Dialog dialog)
throws NullPointerException
{
// remember the dialog created to be able to send SUBSCRIBE
// refresh and to unsibscribe
if (dialog == null)
{
throw new NullPointerException("null dialog associated with a " +
"contact: " + contact);
}
if (contact == null)
{
throw new NullPointerException("null contact");
}
// set the contact client dialog
contact.setClientDialog(dialog);
contact.setResolved(true);
// inform the listeners that the contact is created
this.fireSubscriptionEvent(contact,
contact.getParentContactGroup(),
SubscriptionEvent.SUBSCRIPTION_RESOLVED);
logger.debug("contact : " + contact + " resolved");
}
/**
* Terminate the subscription to a contact presence status
*
* @param contact the contact concerned
*/
private void terminateSubscription(ContactSipImpl contact)
{
if (contact == null)
{
logger.error("null contact provided, can't terminate" +
" subscription");
return;
}
contact.setClientDialog(null);
if (contact.getResfreshTask() != null)
{
contact.getResfreshTask().cancel();
}
// we don't remove the contact
changePresenceStatusForContact(contact,
sipStatusEnum.getStatus(SipStatusEnum.UNKNOWN));
contact.setResolved(false);
}
/**
* Creates a NOTIFY request corresponding to the provided arguments.
* This request MUST be sent using <tt>dialog.sendRequest</tt>
*
* @param contact The contact to notify
* @param doc The presence document to send
* @param subscriptionState The current subscription state
* @param reason The reason of this subscription state (may be null)
*
* @return a valid <tt>ClientTransaction</tt> ready to send the request
*
* @throws OperationFailedException if something goes wrong during the
* creation of the request
*/
private ClientTransaction createNotify(ContactSipImpl contact, byte[] doc,
String subscriptionState, String reason)
throws OperationFailedException
{
Dialog dialog = contact.getServerDialog();
if (dialog == null)
{
throw new OperationFailedException("the server dialog of the " +
"contact is null", OperationFailedException.INTERNAL_ERROR);
}
Request req = null;
try
{
req = dialog.createRequest(Request.NOTIFY);
}
catch (SipException e)
{
logger.error("Can't create the NOTIFY message", e);
throw new OperationFailedException("Can't create the NOTIFY" +
" message", OperationFailedException.INTERNAL_ERROR, e);
}
// Address
Address toAddress = dialog.getRemoteTarget();
// no Contact field
if (toAddress == null)
{
toAddress = dialog.getRemoteParty();
}
ArrayList<ViaHeader> viaHeaders = null;
MaxForwardsHeader maxForwards = null;
try
{
//ViaHeaders
viaHeaders = this.parentProvider.getLocalViaHeaders(toAddress);
//MaxForwards
maxForwards = this.parentProvider
.getMaxForwardsHeader();
}
catch (OperationFailedException e)
{
logger.error("cant retrive the via headers or the max forward",
e);
throw new OperationFailedException("Can't create the NOTIFY" +
" message", OperationFailedException.INTERNAL_ERROR);
}
EventHeader evHeader = null;
try
{
evHeader = this.parentProvider.getHeaderFactory()
.createEventHeader("presence");
}
catch (ParseException e)
{
//these two should never happen.
logger.error(
"An unexpected error occurred while"
+ "constructing the EventHeader", e);
throw new OperationFailedException("Can't create the Event" +
" header", OperationFailedException.INTERNAL_ERROR, e);
}
// Contact
ContactHeader contactHeader = this.parentProvider
.getContactHeader(toAddress);
// Subscription-State
SubscriptionStateHeader sStateHeader = null;
try
{
sStateHeader = this.parentProvider
.getHeaderFactory().createSubscriptionStateHeader(
subscriptionState);
if (reason != null && reason.trim().length() != 0)
{
sStateHeader.setReasonCode(reason);
}
}
catch (ParseException e)
{
// should never happen
logger.error("can't create the Subscription-State header", e);
throw new OperationFailedException("Can't create the " +
"Subscription-State header",
OperationFailedException.INTERNAL_ERROR, e);
}
// Content-type
ContentTypeHeader cTypeHeader = null;
try
{
cTypeHeader = this.parentProvider
.getHeaderFactory().createContentTypeHeader("application",
PIDF_XML);
}
catch (ParseException e)
{
// should never happen
logger.error("can't create the Content-Type header", e);
throw new OperationFailedException("Can't create the " +
"Content-type header",
OperationFailedException.INTERNAL_ERROR, e);
}
req.setHeader(maxForwards);
req.setHeader(evHeader);
req.setHeader(sStateHeader);
req.setHeader(contactHeader);
//check whether there's a cached authorization header for this
//call id and if so - attach it to the request.
// add authorization header
CallIdHeader call = (CallIdHeader)req.getHeader(CallIdHeader.NAME);
String callid = call.getCallId();
AuthorizationHeader authorization = parentProvider
.getSipSecurityManager()
.getCachedAuthorizationHeader(callid);
if(authorization != null)
req.addHeader(authorization);
// create the transaction (then add the via header as recommended
// by the jain-sip documentation at:
// http://snad.ncsl.nist.gov/proj/iptel/jain-sip-1.2
// /javadoc/javax/sip/Dialog.html#createRequest(java.lang.String))
ClientTransaction transac = null;
try
{
transac = this.parentProvider.getDefaultJainSipProvider()
.getNewClientTransaction(req);
}
catch (TransactionUnavailableException ex)
{
logger.error(
"Failed to create subscriptionTransaction.\n"
+ "This is most probably a network connection error.",
ex);
throw new OperationFailedException("Can't create the " +
"Content-length header",
OperationFailedException.NETWORK_FAILURE, ex);
}
req.setHeader((Header) viaHeaders.get(0));
// add the content
try
{
req.setContent(doc, cTypeHeader);
}
catch (ParseException e)
{
logger.error("Failed to add the presence document", e);
throw new OperationFailedException("Can't add the presence " +
"document to the request",
OperationFailedException.INTERNAL_ERROR, e);
}
//User Agent
UserAgentHeader userAgentHeader
= parentProvider.getSipCommUserAgentHeader();
if(userAgentHeader != null)
req.addHeader(userAgentHeader);
return transac;
}
/**
* Process a request from a distant contact
*
* @param requestEvent the <tt>RequestEvent</tt> containing the newly
* received request.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processRequest(RequestEvent requestEvent)
{
if (this.presenceEnabled == false)
return false;
ServerTransaction serverTransaction = requestEvent
.getServerTransaction();
SipProvider jainSipProvider = (SipProvider) requestEvent.getSource();
Request request = requestEvent.getRequest();
if (serverTransaction == null)
{
try
{
serverTransaction = jainSipProvider.getNewServerTransaction(
request);
}
catch (TransactionAlreadyExistsException ex)
{
//let's not scare the user and only log a message
logger.error("Failed to create a new server"
+ "transaction for an incoming request\n"
+ "(Next message contains the request)"
, ex);
return false;
}
catch (TransactionUnavailableException ex)
{
//let's not scare the user and only log a message
logger.error("Failed to create a new server"
+ "transaction for an incoming request\n"
+ "(Next message contains the request)"
, ex);
return false;
}
}
EventHeader eventHeader = (EventHeader)
request.getHeader(EventHeader.NAME);
if (eventHeader == null || !eventHeader.getEventType()
.equalsIgnoreCase("presence"))
{
// we are not concerned by this request, perhaps another
// listener is ?
// don't send a 489 / Bad event answer here
return false;
}
boolean processed = false;
// NOTIFY
if (request.getMethod().equals(Request.NOTIFY))
{
Response response = null;
logger.debug("notify received");
SubscriptionStateHeader sstateHeader = (SubscriptionStateHeader)
request.getHeader(SubscriptionStateHeader.NAME);
// notify must contain one (rfc3265)
if (sstateHeader == null)
{
logger.error("no subscription state in this request");
return false;
}
// first handle the case of a contact still pending
// it's possible if the NOTIFY arrives before the OK
CallIdHeader idheader = (CallIdHeader) request.getHeader(
CallIdHeader.NAME);
ContactSipImpl contact = (ContactSipImpl) this.subscribedContacts
.get(idheader.getCallId());
if (contact != null && !sstateHeader.getState().equalsIgnoreCase(
SubscriptionStateHeader.TERMINATED) && !contact
.isResolved())
{
logger.debug("contact still pending while NOTIFY received");
// can't finalize the subscription here : the client dialog is
// null until the reception of a OK
}
// see if the notify correspond to an existing subscription
if (contact == null && !sstateHeader.getState().equalsIgnoreCase(
SubscriptionStateHeader.TERMINATED))
{
logger.debug("contact not found for callid : " +
idheader.getCallId());
// try to remove the callid from the list if we were excpeting
// this end (if it's the last notify of a subscription we just
// stopped
synchronized (this.waitedCallIds)
{
this.waitedCallIds.remove(idheader.getCallId());
}
// send a 481 response (rfc3625)
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(
Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST,
request);
}
catch (ParseException e)
{
logger.error("failed to create the 481 response", e);
return false;
}
try
{
serverTransaction.sendResponse(response);
}
catch (SipException e)
{
logger.error("failed to send the response", e);
}
catch (InvalidArgumentException e)
{
// should not happen
logger.error("invalid argument provided while trying" +
" to send the response", e);
}
return true;
}
// if we don't understand the content
ContentTypeHeader ctheader = (ContentTypeHeader) request
.getHeader(ContentTypeHeader.NAME);
if (ctheader != null && !ctheader.getContentSubType()
.equalsIgnoreCase(PIDF_XML))
{
// send a 415 response (rfc3261)
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.UNSUPPORTED_MEDIA_TYPE,
request);
}
catch (ParseException e)
{
logger.error("failed to create the OK response", e);
return false;
}
// we want PIDF
AcceptHeader acceptHeader = null;
try
{
acceptHeader = this.parentProvider
.getHeaderFactory().createAcceptHeader(
"application", PIDF_XML);
}
catch (ParseException e)
{
// should not happen
logger.error("failed to create the accept header", e);
return false;
}
response.setHeader(acceptHeader);
try
{
serverTransaction.sendResponse(response);
}
catch (SipException e)
{
logger.error("failed to send the response", e);
}
catch (InvalidArgumentException e)
{
// should not happen
logger.error("invalid argument provided while trying" +
" to send the response", e);
}
}
// if the presentity doesn't want of us anymore
if (sstateHeader.getState().equalsIgnoreCase(
SubscriptionStateHeader.TERMINATED))
{
// if we requested this end of subscription, contact == null
if (contact != null)
{
terminateSubscription(contact);
this.subscribedContacts.remove(serverTransaction.getDialog()
.getCallId().getCallId());
// if the reason is "deactivated", we immediatly resubscribe
// to the contact
if (sstateHeader.getReasonCode().equals(
SubscriptionStateHeader.DEACTIVATED))
{
forcePollContact(contact);
}
}
// try to remove the callid from the list if we were excpeting
// this end (if it's the last notify of a subscription we just
// stopped
synchronized (this.waitedCallIds)
{
this.waitedCallIds.remove(idheader.getCallId());
}
}
// send an OK response
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.OK, request);
}
catch (ParseException e)
{
logger.error("failed to create the OK response", e);
return false;
}
try
{
serverTransaction.sendResponse(response);
}
catch (SipException e)
{
logger.error("failed to send the response", e);
}
catch (InvalidArgumentException e)
{
// should not happen
logger.error("invalid argument provided while trying" +
" to send the response", e);
}
// transform the presence document in new presence status
if (request.getRawContent() != null
&& !sstateHeader.getState().equalsIgnoreCase(
SubscriptionStateHeader.TERMINATED))
{
setPidfPresenceStatus(new String(request.getRawContent()));
}
processed = true;
}
// SUBSCRIBE
else if (request.getMethod().equals(Request.SUBSCRIBE))
{
FromHeader from = (FromHeader) request.getHeader(FromHeader.NAME);
// if we received a subscribe, our network probably doesn't have
// a distant PA
if (this.useDistantPA)
{
this.useDistantPA = false;
if (this.republishTask != null)
{
this.republishTask.cancel();
this.republishTask = null;
}
}
// try to find which contact is concerned
ContactSipImpl contact = (ContactSipImpl) resolveContactID(from
.getAddress().getURI().toString());
// if we don't know him, create him
if (contact == null)
{
contact = new ContactSipImpl(
from.getAddress(), this.parentProvider);
// <tricky time>
// this ensure that we will publish our status to this contact
// without trying to subscribe to him
contact.setResolved(true);
contact.setResolvable(false);
// </tricky time>
}
logger.debug(contact.toString() + " wants to watch your presence " +
"status");
ExpiresHeader expHeader = request.getExpires();
int expires;
if (expHeader == null)
{
expires = PRESENCE_DEFAULT_EXPIRE;
}
else
{
expires = expHeader.getExpires();
}
// interval too brief
if (expires < SUBSCRIBE_MIN_EXPIRE && expires > 0 && expires < 3600)
{
// send him a 423
Response response = null;
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.INTERVAL_TOO_BRIEF, request);
}
catch (Exception e)
{
logger.error("Error while creating the response 423", e);
return false;
}
MinExpiresHeader min = null;
try
{
min = this.parentProvider.getHeaderFactory()
.createMinExpiresHeader(SUBSCRIBE_MIN_EXPIRE);
}
catch (InvalidArgumentException e)
{
// should not happen
logger.error("can't create the min expires header", e);
return false;
}
response.setHeader(min);
try
{
serverTransaction.sendResponse(response);
}
catch (Exception e)
{
logger.error("Error while sending the response 423", e);
return false;
}
return true;
}
// is it a subscription refresh ? (no need for synchronize the
// access to ourWatchers: read only operation)
if (this.ourWatchers.contains(contact) && expires != 0
&& contact.getServerDialog().equals(
serverTransaction.getDialog()))
{
contact.getTimeoutTask().cancel();
// add the new timeout task
WatcherTimeoutTask timeout = new WatcherTimeoutTask(contact);
contact.setTimeoutTask(timeout);
getTimer().schedule(timeout, expires * 1000);
// send a OK
Response response = null;
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.OK, request);
}
catch (Exception e)
{
logger.error("Error while creating the response 200", e);
return false;
}
// add the expire header
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(expires);
}
catch (InvalidArgumentException e)
{
logger.error("Can't create the expires header");
return false;
}
response.setHeader(expHeader);
try
{
serverTransaction.sendResponse(response);
}
catch (Exception e)
{
logger.error("Error while sending the response 200", e);
return false;
}
return true;
}
Dialog dialog = contact.getServerDialog();
// is it a subscription end ?
if (expires == 0)
{
logger.debug("contact " + contact + " isn't a watcher anymore");
// remove the contact from our watcher
synchronized (this.ourWatchers)
{
this.ourWatchers.remove(contact);
}
contact.getTimeoutTask().cancel();
contact.setServerDialog(null);
// send him a OK
Response response = null;
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.OK, request);
}
catch (Exception e)
{
logger.error("Error while creating the response 200", e);
return false;
}
// add the expire header
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(0);
}
catch (InvalidArgumentException e)
{
logger.error("Can't create the expires header", e);
return false;
}
response.setHeader(expHeader);
try
{
serverTransaction.sendResponse(response);
}
catch (Exception e)
{
logger.error("Error while sending the response 200", e);
return false;
}
// then terminate the subscription with an ultimate NOTIFY
ClientTransaction transac = null;
try
{
transac = createNotify(
contact,
getPidfPresenceStatus(
(ContactSipImpl)getLocalContactForDst(contact)),
SubscriptionStateHeader.TERMINATED,
SubscriptionStateHeader.TIMEOUT);
}
catch (OperationFailedException e)
{
logger.error("failed to create the new notify", e);
return false;
}
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return false;
}
return true;
}
// if the contact was already subscribed, we close the last
// subscription before accepting the new one
if (this.ourWatchers.contains(contact)
&& !contact.getServerDialog().equals(
serverTransaction.getDialog()))
{
logger.debug("contact " + contact + " try to resubscribe, "
+ "we will remove the first subscription");
// terminate the subscription with a closing NOTIFY
ClientTransaction transac = null;
try
{
transac = createNotify(contact,
getPidfPresenceStatus((ContactSipImpl)
getLocalContactForDst(contact)),
SubscriptionStateHeader.TERMINATED,
SubscriptionStateHeader.REJECTED);
}
catch (OperationFailedException e)
{
logger.error("failed to create the new notify", e);
return false;
}
contact.setServerDialog(null);
// remove the contact from our watcher
synchronized (this.ourWatchers)
{
this.ourWatchers.remove(contact);
}
if (contact.getTimeoutTask() != null)
{
contact.getTimeoutTask().cancel();
}
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return false;
}
}
// remember the dialog we will use to send the NOTIFYs
// the synchronization avoids changing the dialog while receiving
// an error for the previous subscription closed
synchronized (contact)
{
contact.setServerDialog(serverTransaction.getDialog());
}
dialog = contact.getServerDialog();
// immediately send a 200 / OK
Response response = null;
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.OK, request);
}
catch (Exception e)
{
logger.error("Error while creating the response 200", e);
return false;
}
// add the expire header
try
{
expHeader = this.parentProvider.getHeaderFactory()
.createExpiresHeader(expires);
}
catch (InvalidArgumentException e)
{
logger.error("Can't create the expires header", e);
return false;
}
response.setHeader(expHeader);
try
{
serverTransaction.sendResponse(response);
}
catch (Exception e)
{
logger.error("Error while sending the response 200", e);
return false;
}
// send a NOTIFY
ClientTransaction transac = null;
try
{
transac = createNotify(contact,
getPidfPresenceStatus((ContactSipImpl)
getLocalContactForDst(contact)),
SubscriptionStateHeader.ACTIVE,
null);
}
catch (OperationFailedException e)
{
logger.error("failed to create the new notify", e);
return false;
}
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return false;
}
// add him to our watcher list
synchronized (this.ourWatchers)
{
this.ourWatchers.add(contact);
}
// add the timeout task
WatcherTimeoutTask timeout = new WatcherTimeoutTask(contact);
contact.setTimeoutTask(timeout);
getTimer().schedule(timeout, expires * 1000);
processed = true;
}
// PUBLISH
else if (request.getMethod().equals(Request.PUBLISH))
{
// we aren't supposed to receive a publish so just say "not
// implemented". This behavior is usefull for SC to SC communication
// with the PA auto detection feature and a server which proxy the
// PUBLISH requests
Response response = null;
try
{
response = this.parentProvider.getMessageFactory()
.createResponse(Response.NOT_IMPLEMENTED, request);
}
catch (Exception e)
{
logger.error("Error while creating the response 501", e);
return false;
}
try
{
serverTransaction.sendResponse(response);
}
catch (Exception e)
{
logger.error("Error while sending the response 501", e);
return false;
}
processed = true;
}
return processed;
}
/**
* Called when a dialog is terminated
*
* @param dialogTerminatedEvent DialogTerminatedEvent
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processDialogTerminated(
DialogTerminatedEvent dialogTerminatedEvent)
{
// never fired
return false;
}
/**
* Called when an IO error occurs
*
* @param exceptionEvent IOExceptionEvent
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processIOException(IOExceptionEvent exceptionEvent)
{
// never fired
return false;
}
/**
* Called when a transaction is terminated
*
* @param transactionTerminatedEvent TransactionTerminatedEvent
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processTransactionTerminated(
TransactionTerminatedEvent transactionTerminatedEvent)
{
// nothing to do
return false;
}
/**
* Called when a timeout occur
*
* @param timeoutEvent TimeoutEvent
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
public boolean processTimeout(TimeoutEvent timeoutEvent)
{
logger.error("timeout reached, it looks really abnormal: " +
timeoutEvent.toString());
return false;
}
/**
* Attempts to re-generate the corresponding request with the proper
* credentials.
*
* @param clientTransaction the corresponding transaction
* @param response the challenge
* @param jainSipProvider the provider that received the challenge
*
* @throws OperationFailedException if processing the authentication
* challenge fails.
*/
private void processAuthenticationChallenge(
ClientTransaction clientTransaction,
Response response,
SipProvider jainSipProvider)
throws OperationFailedException
{
try
{
logger.debug("Authenticating a message request.");
ClientTransaction retryTran
= this.parentProvider.getSipSecurityManager().handleChallenge(
response,
clientTransaction,
jainSipProvider);
if(retryTran == null)
{
logger.trace("No password supplied or error occured!");
return;
}
retryTran.sendRequest();
return;
}
catch (Exception exc)
{
logger.error("We failed to authenticate a message request.",
exc);
throw new OperationFailedException("Failed to authenticate"
+ "a message request",
OperationFailedException.INTERNAL_ERROR,
exc);
}
}
/**
* 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(ContactSipImpl source,
ContactGroup parentGroup,
PresenceStatus oldValue)
{
if(oldValue.equals(source.getPresenceStatus())){
logger.debug("Ignored prov stat. change evt. old==new = "
+ oldValue);
return;
}
ContactPresenceStatusChangeEvent evt
= new ContactPresenceStatusChangeEvent(source, this.parentProvider,
parentGroup, oldValue, source.getPresenceStatus());
Iterator listeners = null;
synchronized(this.contactPresenceStatusListeners)
{
listeners = new ArrayList(this.contactPresenceStatusListeners)
.iterator();
}
while(listeners.hasNext())
{
ContactPresenceStatusListener listener
= (ContactPresenceStatusListener) listeners.next();
listener.contactPresenceStatusChanged(evt);
}
}
/**
* Sets the presence status of <tt>contact</tt> to <tt>newStatus</tt>.
*
* @param contact the <tt>ContactSipImpl</tt> whose status we'd like
* to set.
* @param newStatus the new status we'd like to set to <tt>contact</tt>.
*/
private void changePresenceStatusForContact(ContactSipImpl contact,
PresenceStatus newStatus)
{
PresenceStatus oldStatus = contact.getPresenceStatus();
contact.setPresenceStatus(newStatus);
fireContactPresenceStatusChangeEvent(
contact, contact.getParentContactGroup(), oldStatus);
}
/**
* 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 this.contactListRoot.findContactByID(contactID);
}
/**
* Returns the protocol specific contact instance representing the local
* user.
*
* @param destination the destination that we would be sending our contact
* information to.
*
* @return a ContactSipImpl instance that represents us.
*/
public ContactSipImpl getLocalContactForDst(ContactSipImpl destination)
{
return getLocalContactForDst(destination.getSipAddress());
}
/**
* Returns the protocol specific contact instance representing the local
* user.
*
* @param destination the destination that we would be sending our contact
* information to.
*
* @return a ContactSipImpl instance that represents us.
*/
public ContactSipImpl getLocalContactForDst(Address destination)
{
ContactSipImpl res;
Address sipAddress = parentProvider.getOurSipAddress(destination);
res = new ContactSipImpl(sipAddress, this.parentProvider);
res.setPresenceStatus(this.presenceStatus);
return res;
}
/**
* 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)
{
// authorizations aren't supported by this implementation
}
/**
* Adds a listener that would receive events upon changes of the provider
* presence status.
*
* @param listener the listener to register for changes in our
* PresenceStatus.
*/
public void addProviderPresenceStatusListener(
ProviderPresenceStatusListener listener)
{
synchronized(this.providerPresenceStatusListeners)
{
if (!this.providerPresenceStatusListeners.contains(listener))
this.providerPresenceStatusListeners.add(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(this.providerPresenceStatusListeners)
{
this.providerPresenceStatusListeners.remove(listener);
}
}
/**
* SIP implementation of the corresponding ProtocolProviderService
* method.
*
* @param listener a presence status listener.
*/
public void addContactPresenceStatusListener(
ContactPresenceStatusListener listener)
{
synchronized(this.contactPresenceStatusListeners)
{
if (!this.contactPresenceStatusListeners.contains(listener))
this.contactPresenceStatusListeners.add(listener);
}
}
/**
* 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(this.contactPresenceStatusListeners)
{
this.contactPresenceStatusListeners.remove(listener);
}
}
/**
* Registers a listener that would receive events upon changes in server
* stored groups.
*
* @param listener a ServerStoredGroupChangeListener impl that would
* receive events upon group changes.
*/
public void addServerStoredGroupChangeListener(ServerStoredGroupListener
listener)
{
synchronized(this.serverStoredGroupListeners)
{
if (!this.serverStoredGroupListeners.contains(listener))
this.serverStoredGroupListeners.add(listener);
}
}
/**
* 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(this.serverStoredGroupListeners)
{
this.serverStoredGroupListeners.remove(listener);
}
}
/**
* Returns the status message that was confirmed by the server
*
* @return the last status message that we have requested and the server
* has confirmed.
*/
public String getCurrentStatusMessage()
{
return this.statusMessage;
}
/**
* 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 addressStr 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 addressStr,
String persistentData,
ContactGroup parent)
{
Address contactAddress;
try
{
contactAddress = parentProvider.parseAddressString(addressStr);
}
catch (ParseException exc)
{
throw new IllegalArgumentException(
addressStr + " is not a valid string.", exc);
}
ContactSipImpl contact = new ContactSipImpl(contactAddress,
this.parentProvider);
contact.setResolved(false);
((ContactGroupSipImpl) parent).addContact(contact);
fireSubscriptionEvent(contact,
parent,
SubscriptionEvent.SUBSCRIPTION_CREATED);
return contact;
}
/**
* 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.
*
* @param contactAddress the address of the volatile contact we'd like to
* create.
* @return the newly created volatile contact.
*/
public ContactSipImpl createVolatileContact(Address contactAddress)
{
// First create the new volatile contact;
ContactSipImpl newVolatileContact
= new ContactSipImpl(contactAddress, this.parentProvider);
newVolatileContact.setDisplayName(contactAddress.getDisplayName());
newVolatileContact.setPersistent(false);
// Check whether a volatile group already exists and if not create one
ContactGroupSipImpl theVolatileGroup = getNonPersistentGroup();
// if the parent volatile group is null then we create it
if (theVolatileGroup == null)
{
theVolatileGroup = new ContactGroupSipImpl(
"NotInContactList",
this.parentProvider);
theVolatileGroup.setResolved(false);
theVolatileGroup.setPersistent(false);
this.contactListRoot.addSubgroup(theVolatileGroup);
fireServerStoredGroupEvent(theVolatileGroup
, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
}
//now add the volatile contact inside it
theVolatileGroup.addContact(newVolatileContact);
fireSubscriptionEvent(newVolatileContact
, theVolatileGroup
, SubscriptionEvent.SUBSCRIPTION_CREATED);
return newVolatileContact;
}
/**
* 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.
*/
private ContactGroupSipImpl getNonPersistentGroup()
{
for (int i = 0;
i < getServerStoredContactListRoot().countSubgroups();
i++)
{
ContactGroupSipImpl gr = (ContactGroupSipImpl)
getServerStoredContactListRoot().getGroup(i);
if(!gr.isPersistent())
{
return gr;
}
}
return null;
}
/**
* 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
*/
Contact resolveContactID(String contactID)
{
Contact res = this.findContactByID(contactID);
if (res == null)
{
// we try to resolve the conflict by removing "sip:" from the id
if (contactID.startsWith("sip:"))
{
res = this.findContactByID(contactID.substring(4));
}
if (res == null)
{
// we try to remove the part after the '@'
if (contactID.indexOf('@') > -1)
{
res = this.findContactByID(
contactID.substring(0, contactID.indexOf('@')));
if (res == null)
{
// try the same thing without sip:
if (contactID.startsWith("sip:"))
{
res = this.findContactByID(
contactID.substring(4,
contactID.indexOf('@')));
}
}
}
}
}
return res;
}
/**
* Returns contact if present in the watcher list, null else.
*
* @param contactAddress the contact to find
*
* @return the watcher or null if the contact isn't a watcher
*/
private ContactSipImpl getWatcher(String contactAddress)
{
String id1 = contactAddress;
// without sip:
String id2 = contactAddress.substring(4);
// without the domain
String id3 = contactAddress.substring(0, contactAddress.indexOf('@'));
// without sip: and the domain
String id4 = contactAddress.substring(4, contactAddress.indexOf('@'));
Iterator iter = this.ourWatchers.iterator();
while (iter.hasNext())
{
ContactSipImpl contact = (ContactSipImpl) iter.next();
// test by order of probability to be true
// will probably save 1ms :)
if (contact.getAddress().equals(id2)
|| contact.getAddress().equals(id1)
|| contact.getAddress().equals(id4)
|| contact.getAddress().equals(id3))
{
return contact;
}
}
return null;
}
/**
* Returns a new valid xml document.
*
* @return a correct xml document or null if an error occurs
*/
Document createDocument()
{
try
{
if (this.docBuilderFactory == null)
{
this.docBuilderFactory = DocumentBuilderFactory.newInstance();
}
if (this.docBuilder == null)
{
this.docBuilder = this.docBuilderFactory.newDocumentBuilder();
}
}
catch (Exception e)
{
logger.error("can't create the new xml document", e);
return null;
}
return this.docBuilder.newDocument();
}
/**
* Convert a xml document
*
* @param document the document to convert
*
* @return a string representing <tt>document</tt> or null if an error
* occur
*/
String convertDocument(Document document)
{
DOMSource source = new DOMSource(document);
StringWriter stringWriter = new StringWriter();
StreamResult result = new StreamResult(stringWriter);
try
{
if (this.transFactory == null)
{
this.transFactory = TransformerFactory.newInstance();
}
if (this.transformer == null)
{
this.transformer = this.transFactory.newTransformer();
}
this.transformer.transform(source, result);
}
catch (Exception e)
{
logger.error("can't convert the xml document into a string", e);
return null;
}
return stringWriter.toString();
}
/**
* Convert a xml document
*
* @param document the document as a String
*
* @return a <tt>Document</tt> representing the document or null if an
* error occur
*/
Document convertDocument(String document)
{
StringReader reader = new StringReader(document);
StreamSource source = new StreamSource(reader);
Document doc = createDocument();
if (doc == null)
{
return null;
}
DOMResult result = new DOMResult(doc);
try
{
if (this.transFactory == null)
{
this.transFactory = TransformerFactory.newInstance();
}
if (this.transformer == null)
{
this.transformer = this.transFactory.newTransformer();
}
this.transformer.transform(source, result);
}
catch (Exception e)
{
logger.error("can't convert the string into a xml document", e);
return null;
}
return doc;
}
/**
* Converts the <tt>PresenceStatus</tt> of <tt>contact</tt> into a PIDF
* document.
*
* @param contact The contact which interest us
*
* @return a PIDF document representing the current presence status of
* this contact or null if an error occurs.
*/
public byte[] getPidfPresenceStatus(ContactSipImpl contact)
{
Document doc = this.createDocument();
if (doc == null)
{
return null;
}
String contactUri = contact.getSipAddress().getURI().toString();
// <presence>
Element presence = doc.createElement(PRESENCE_ELEMENT);
presence.setAttribute(NS_ELEMENT, NS_VALUE);
presence.setAttribute(RPID_NS_ELEMENT, RPID_NS_VALUE);
presence.setAttribute(DM_NS_ELEMENT, DM_NS_VALUE);
presence.setAttribute(ENTITY_ATTRIBUTE, contactUri);
doc.appendChild(presence);
// <person>
Element person = doc.createElement(NS_PERSON_ELT);
person.setAttribute(ID_ATTRIBUTE, personid);
presence.appendChild(person);
// <activities>
Element activities = doc.createElement(NS_ACTIVITY_ELT);
person.appendChild(activities);
// the correct activity
if (contact.getPresenceStatus()
.equals(sipStatusEnum.getStatus(SipStatusEnum.AWAY)))
{
Element away = doc.createElement(NS_AWAY_ELT);
activities.appendChild(away);
}
else if (contact.getPresenceStatus()
.equals(sipStatusEnum.getStatus(SipStatusEnum.BUSY)))
{
Element busy = doc.createElement(NS_BUSY_ELT);
activities.appendChild(busy);
}
else if (contact.getPresenceStatus()
.equals(sipStatusEnum.getStatus(SipStatusEnum.ON_THE_PHONE)))
{
Element otp = doc.createElement(NS_OTP_ELT);
activities.appendChild(otp);
}
// <tuple>
Element tuple = doc.createElement(TUPLE_ELEMENT);
tuple.setAttribute(ID_ATTRIBUTE, tupleid);
presence.appendChild(tuple);
// <status>
Element status = doc.createElement(STATUS_ELEMENT);
tuple.appendChild(status);
// <basic>
Element basic = doc.createElement(BASIC_ELEMENT);
if (contact.getPresenceStatus()
.equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)))
{
basic.appendChild(doc.createTextNode(OFFLINE_STATUS));
}
else
{
basic.appendChild(doc.createTextNode(ONLINE_STATUS));
}
status.appendChild(basic);
// <contact>
Element contactUriEl = doc.createElement(CONTACT_ELEMENT);
Node cValue = doc.createTextNode(contactUri);
contactUriEl.appendChild(cValue);
tuple.appendChild(contactUriEl);
// <note> we write our real status here, this status SHOULD not be
// used for automatic parsing but some (bad) IM clients do this...
// we don't use xml:lang here because it's not really revelant
Element noteNodeEl = doc.createElement(NOTE_ELEMENT);
noteNodeEl.appendChild(doc.createTextNode(contact.getPresenceStatus()
.getStatusName()));
tuple.appendChild(noteNodeEl);
String res = convertDocument(doc);
if (res == null)
{
return null;
}
return res.getBytes();
}
/**
* Sets the contact's presence status using the PIDF document provided.
* In case of conflict (more than one status per contact) the last valid
* status in the document is used.
* This implementation is very tolerant to be more compatible with bad
* implementations of SIMPLE. The limit of the tolerance is defined by
* the CPU cost: as far as the tolerance costs nothing more in well
* structured documents, we do it.
*
* @param presenceDoc the pidf document to use
*/
public void setPidfPresenceStatus(String presenceDoc)
{
Document doc = convertDocument(presenceDoc);
if (doc == null)
{
return;
}
logger.debug("parsing:\n" + presenceDoc);
// <presence>
NodeList presList = doc.getElementsByTagNameNS(NS_VALUE,
PRESENCE_ELEMENT);
if (presList.getLength() == 0)
{
presList = doc.getElementsByTagNameNS(ANY_NS, PRESENCE_ELEMENT);
if (presList.getLength() == 0)
{
logger.error("no presence element in this document");
return;
}
}
if (presList.getLength() > 1)
{
logger.warn("more than one presence element in this document");
}
Node presNode = presList.item(0);
if (presNode.getNodeType() != Node.ELEMENT_NODE)
{
logger.error("the presence node is not an element");
return;
}
Element presence = (Element) presNode;
// RPID area
// due to a lot of changes in the past years to this functionality,
// the namespace used by servers and clients are often wrong so we just
// ignore namespaces here
PresenceStatus personStatus = null;
NodeList personList = presence.getElementsByTagNameNS(ANY_NS,
PERSON_ELEMENT);
//if (personList.getLength() > 1) {
// logger.error("more than one person in this document");
// return;
//}
if (personList.getLength() > 0)
{
Node personNode = personList.item(0);
if (personNode.getNodeType() != Node.ELEMENT_NODE)
{
logger.error("the person node is not an element");
return;
}
Element person = (Element) personNode;
NodeList activityList =
person.getElementsByTagNameNS(ANY_NS, ACTIVITY_ELEMENT);
if (activityList.getLength() > 0)
{
Element activity = null;
// find the first correct activity
for (int i = 0; i < activityList.getLength(); i++)
{
Node activityNode = activityList.item(i);
if (activityNode.getNodeType() != Node.ELEMENT_NODE)
{
continue;
}
activity = (Element) activityNode;
NodeList statusList = activity.getChildNodes();
for (int j = 0; j < statusList.getLength(); j++)
{
Node statusNode = statusList.item(j);
if (statusNode.getNodeType() == Node.ELEMENT_NODE)
{
String statusname = statusNode.getLocalName();
if (statusname.equals(AWAY_ELEMENT))
{
personStatus = sipStatusEnum
.getStatus(SipStatusEnum.AWAY);
break;
}
else if (statusname.equals(BUSY_ELEMENT))
{
personStatus = sipStatusEnum
.getStatus(SipStatusEnum.BUSY);
break;
}
else if (statusname.equals(OTP_ELEMENT))
{
personStatus = sipStatusEnum
.getStatus(SipStatusEnum.ON_THE_PHONE);
break;
}
}
}
if (personStatus != null)
break;
}
}
}
// Vector containing the list of status to set for each contact in
// the presence document ordered by priority (highest first).
// <SipContact, Float (priority), SipStatusEnum>
Vector newPresenceStates = new Vector(3, 2);
// <tuple>
NodeList tupleList = getPidfChilds(presence, TUPLE_ELEMENT);
for (int i = 0; i < tupleList.getLength(); i++)
{
Node tupleNode = tupleList.item(i);
if (tupleNode.getNodeType() != Node.ELEMENT_NODE)
{
continue;
}
Element tuple = (Element) tupleNode;
// <contact>
NodeList contactList = getPidfChilds(tuple, CONTACT_ELEMENT);
// we use a vector here and not an unique contact to handle an
// error case where many contacts are associated with a status
// Vector<ContactSipImpl>
Vector sipcontact = new Vector(1, 3);
String contactID = null;
if (contactList.getLength() == 0)
{
// use the entity attribute of the presence node
contactID = XMLUtils.getAttribute(
presNode, ENTITY_ATTRIBUTE);
// also accept entity URIs starting with pres: instead of sip:
if (contactID.startsWith("pres:"))
{
contactID = contactID.substring("pres:".length());
}
Contact tmpContact = resolveContactID(contactID);
if (tmpContact != null)
{
Object tab[] = {tmpContact, new Float(0f)};
sipcontact.add(tab);
}
}
else
{
// this is normally not permitted by RFC3863
for (int j = 0; j < contactList.getLength(); j++)
{
Node contactNode = contactList.item(j);
if (contactNode.getNodeType() != Node.ELEMENT_NODE)
{
continue;
}
Element contact = (Element) contactNode;
contactID = getTextContent(contact);
// also accept entity URIs starting with pres: instead
// of sip:
if (contactID.startsWith("pres:"))
{
contactID = contactID.substring("pres:".length());
}
Contact tmpContact = resolveContactID(contactID);
if (tmpContact == null)
{
continue;
}
// defines an array containing the contact and its
// priority
Object tab[] = new Object[2];
// search if the contact has a priority
String prioStr = contact.getAttribute(PRIORITY_ATTRIBUTE);
Float prio = null;
try
{
if (prioStr == null || prioStr.length() == 0)
{
prio = new Float(0f);
}
else
{
prio = Float.valueOf(prioStr);
}
}
catch (NumberFormatException e)
{
logger.debug("contact priority is not a valid float",
e);
prio = new Float(0f);
}
// 0 <= priority <= 1 according to rfc
if (prio.floatValue() < 0)
{
prio = new Float(0f);
}
if (prio.floatValue() > 1)
{
prio = new Float(1f);
}
tab[0] = tmpContact;
tab[1] = prio;
// search if the contact hasn't already been added
boolean contactAlreadyListed = false;
for (int k = 0; k < sipcontact.size(); k++)
{
Object tmp[];
tmp = ((Object[]) sipcontact.get(k));
if (((Contact) tmp[0]).equals(tmpContact))
{
contactAlreadyListed = true;
// take the highest priority
if (((Float) tmp[1]).floatValue() <
prio.floatValue())
{
sipcontact.remove(k);
sipcontact.add(tab);
}
break;
}
}
// add the contact and its priority to the list
if (!contactAlreadyListed)
{
sipcontact.add(tab);
}
}
}
if (sipcontact.isEmpty())
{
logger.debug("no contact found for id: " + contactID);
continue;
}
// if we use RPID, simply ignore the standard PIDF status
if (personStatus != null)
{
newPresenceStates = setStatusForContacts(
personStatus,
sipcontact,
newPresenceStates);
continue;
}
// <status>
NodeList statusList = getPidfChilds(tuple, STATUS_ELEMENT);
// in case of many status, just consider the last one
// this is normally not permitted by RFC3863
int index = statusList.getLength() - 1;
Node statusNode = null;
do
{
Node temp = statusList.item(index);
if (temp.getNodeType() == Node.ELEMENT_NODE)
{
statusNode = temp;
break;
}
index--;
}
while (index >= 0);
Element basic = null;
if (statusNode == null)
{
logger.debug("no valid status in this tuple");
}
else
{
Element status = (Element) statusNode;
// <basic>
NodeList basicList = getPidfChilds(status, BASIC_ELEMENT);
// in case of many basic, just consider the last one
// this is normally not permitted by RFC3863
index = basicList.getLength() - 1;
Node basicNode = null;
do
{
Node temp = basicList.item(index);
if (temp.getNodeType() == Node.ELEMENT_NODE)
{
basicNode = temp;
break;
}
index--;
}
while (index >= 0);
if (basicNode == null)
{
logger.debug("no valid <basic> in this status");
}
else
{
basic = (Element) basicNode;
}
}
// search for a <note> that can define a more precise
// status this is not recommended by RFC3863 but some im
// clients use this.
NodeList noteList = getPidfChilds(tuple, NOTE_ELEMENT);
boolean changed = false;
for (int k = 0; k < noteList.getLength() && !changed; k++)
{
Node noteNode = noteList.item(k);
if (noteNode.getNodeType() != Node.ELEMENT_NODE)
{
continue;
}
Element note = (Element) noteNode;
String state = getTextContent(note);
Iterator states = sipStatusEnum.getSupportedStatusSet();
while (states.hasNext())
{
PresenceStatus current = (PresenceStatus) states.next();
if (current.getStatusName().equalsIgnoreCase(state))
{
changed = true;
newPresenceStates = setStatusForContacts(current,
sipcontact,
newPresenceStates);
break;
}
}
}
if (changed == false && basic != null)
{
if (getTextContent(basic).equalsIgnoreCase(ONLINE_STATUS))
{
newPresenceStates = setStatusForContacts(
sipStatusEnum.getStatus(SipStatusEnum.ONLINE),
sipcontact,
newPresenceStates);
}
else if (getTextContent(basic).equalsIgnoreCase(
OFFLINE_STATUS))
{
newPresenceStates = setStatusForContacts(
sipStatusEnum.getStatus(SipStatusEnum.OFFLINE),
sipcontact,
newPresenceStates);
}
}
else
{
if (changed == false)
{
logger.debug("no suitable presence state found in this "
+ "tuple");
}
}
} // for each <tuple>
// Now really set the new presence status for the listed contacts
// newPresenceStates is ordered so priority order is respected
Iterator iter = newPresenceStates.iterator();
while (iter.hasNext())
{
Object tab[] = (Object[]) iter.next();
ContactSipImpl contact = (ContactSipImpl) tab[0];
PresenceStatus status = (PresenceStatus) tab[2];
changePresenceStatusForContact(contact, status);
}
}
/**
* Secured call to XMLUtils.getText (no null returned but an empty string)
*
* @param node the node with which call <tt>XMLUtils.getText()</tt>
*
* @return the string contained in the node or an empty string if there is
* no text information in the node.
*/
private String getTextContent(Element node)
{
String res = XMLUtils.getText(node);
if (res == null)
{
logger.warn("no text for element '" + node.getNodeName() + "'");
return "";
}
return res;
}
/**
* Gets the list of the descendant of an element in the pidf namespace.
* If the list is empty, we try to get this list in any namespace.
* This method is usefull for being able to read pidf document without any
* namespace or with a wrong namespace.
*
* @param element the base element concerned.
* @paran childName the name of the descendants to match on.
*
* @return The list of all the descendant node.
*/
private NodeList getPidfChilds(Element element, String childName)
{
NodeList res;
res = element.getElementsByTagNameNS(NS_VALUE, childName);
if (res.getLength() == 0)
{
res = element.getElementsByTagNameNS(ANY_NS, childName);
}
return res;
}
/**
* Associate the provided presence state to the contacts considering the
* current presence states and priorities.
*
* @param presenceState The presence state to associate to the contacts
* @param contacts A list of <contact, priority> concerned by the
* presence status.
* @param curStatus The list of the current presence status ordered by
* priority (highest priority first).
*
* @return a Vector containing a list of <contact, priority, status>
* ordered by priority (highest first). Null if a parameter is null.
*/
private Vector setStatusForContacts(PresenceStatus presenceState,
Vector contacts, Vector curStatus)
{
// test parameters
if (presenceState == null || contacts == null || curStatus == null)
{
return null;
}
// for each contact in the list
Iterator iter = contacts.iterator();
while (iter.hasNext())
{
Object tab[] = (Object[]) iter.next();
Contact contact = (Contact) tab[0];
float priority = ((Float) tab[1]).floatValue();
// for each existing contact
int pos = 0;
boolean skip = false;
for (int i = 0; i < curStatus.size(); i++)
{
Object tab2[] = (Object[]) curStatus.get(i);
Contact curContact = (Contact) tab2[0];
float curPriority = ((Float) tab2[1]).floatValue();
// save the place where to add this contact in the list
if (pos == 0 && curPriority <= priority)
{
pos = i;
}
if (curContact.equals(contact))
{
// same contact but with an higher priority
// simply ignore this new status affectation
if (curPriority > priority)
{
skip = true;
break;
// same contact but with a lower priority
// replace the old status with this one
}
else if (curPriority < priority)
{
curStatus.remove(i);
// same contact and same priority
// consider the reachability of the status
}
else
{
PresenceStatus curPresence = (PresenceStatus) tab2[2];
if (curPresence.getStatus() >=
presenceState.getStatus())
{
skip = true;
break;
}
curStatus.remove(i);
}
i--;
}
}
if (skip)
{
continue;
}
// insert the new entry
Object tabRes[] = {contact, new Float(priority), presenceState};
curStatus.insertElementAt(tabRes, pos);
}
return curStatus;
}
/**
* Forces the poll of a contact to update its current state.
*
* @param contact the contact to poll
*/
public void forcePollContact(ContactSipImpl contact)
{
if (this.presenceEnabled == false || !contact.isResolvable())
return;
// if we are already subscribed to this contact, just return
if (contact.getClientDialog() != null)
return;
// create the subscription
Request subscription;
try
{
subscription = createSubscription(contact,
this.subscriptionDuration);
}
catch (OperationFailedException ex)
{
logger.error(
"Failed to create the subcription", ex);
return;
}
//Transaction
ClientTransaction subscribeTransaction;
SipProvider jainSipProvider
= this.parentProvider.getDefaultJainSipProvider();
try
{
subscribeTransaction = jainSipProvider
.getNewClientTransaction(subscription);
}
catch (TransactionUnavailableException ex)
{
logger.error(
"Failed to create subscriptionTransaction.\n"
+ "This is most probably a network"
+ " connection error.",
ex);
return;
}
// we register the contact to find him when the OK
// will arrive
CallIdHeader idheader = (CallIdHeader)
subscription.getHeader(CallIdHeader.NAME);
this.subscribedContacts.put(idheader.getCallId(), contact);
logger.debug("added a contact at :"
+ idheader.getCallId());
// send the message
try
{
subscribeTransaction.sendRequest();
}
catch (SipException ex)
{
logger.error(
"Failed to send the message.",
ex);
// this contact will never been accepted or
// rejected
this.subscribedContacts.remove(idheader.getCallId());
return;
}
}
/**
* Unsubscribe to every contact.
*/
public void unsubscribeToAllContact()
{
logger.debug("trying to unsubscribe to every contact");
// send event notifications saying that all our buddies are
// offline. SIMPLE 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())
{
ContactGroupSipImpl group = (ContactGroupSipImpl)
groupsIter.next();
Iterator contactsIter = group.contacts();
while (contactsIter.hasNext())
{
ContactSipImpl contact = (ContactSipImpl)
contactsIter.next();
// if it's needed, we send an unsubcsription message
if (contact.getClientDialog() != null && contact.isResolved())
{
//assertConnected(); will fail because the parent provider
// is already unregistered at this point
Dialog dialog = contact.getClientDialog();
ClientTransaction transac = null;
try
{
transac = createSubscription(0, dialog);
}
catch (OperationFailedException e)
{
logger.error(
"Failed to create subscriptionTransaction.", e);
return;
}
// we are not anymore subscribed to this contact
// this ensure that the response of this request will be
// handled as an unsubscription response
this.subscribedContacts.remove(
dialog.getCallId().getCallId());
// remember the callId to be sure to end the subscription
// before unregistering
synchronized (this.waitedCallIds)
{
this.waitedCallIds.add(dialog.getCallId().getCallId());
}
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return;
}
logger.debug("unsubscribed to " + contact);
}
else
{
logger.debug("contact " + contact
+ " doesn't insteress us");
}
terminateSubscription(contact);
}
}
}
/**
* Gets the timer which handles all scheduled tasks. If it still doesn't
* exists, a new <tt>Timer</tt> is created.
*
* @return the <tt>Timer</tt> which handles all scheduled tasks
*/
private Timer getTimer()
{
synchronized (timerLock)
{
if (timer == null)
timer = new Timer(true);
return timer;
}
}
/**
* Cancels the timer which handles all scheduled tasks and disposes of the
* currently existing tasks scheduled with it.
*/
private void cancelTimer()
{
/*
* The timer is being canceled so the tasks schedules with it are being
* made obsolete.
*/
if (republishTask != null)
republishTask = null;
if (pollingTask != null)
pollingTask = null;
synchronized (timerLock)
{
if (timer != null)
{
timer.cancel();
timer = null;
}
}
}
protected class RefreshSubscriptionTask extends TimerTask
{
/**
* The contact associated with the subscription refresh
*/
ContactSipImpl contact;
/**
* Default constructor
*
* @param contact The contact associated with this task
*/
public RefreshSubscriptionTask(ContactSipImpl contact)
{
this.contact = contact;
}
/**
* Send a subscription refresh message to the contact
*/
public void run()
{
Dialog dialog = this.contact.getClientDialog();
if (dialog == null)
{
logger.warn("null clientDialog associated with " +
this.contact + ", can't refresh the subscription");
return;
}
ClientTransaction transac = null;
try
{
transac = createSubscription(subscriptionDuration, dialog);
}
catch (OperationFailedException e)
{
logger.error("Failed to create subscriptionTransaction.", e);
return;
}
// this avoid to have a refresh response during the unregistration
// process (which could make it fail)
synchronized (waitedCallIds)
{
waitedCallIds.add(dialog.getCallId().getCallId());
}
try
{
dialog.sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return;
}
}
}
protected class WatcherTimeoutTask extends TimerTask
{
/**
* The watcher associated with this task
*/
ContactSipImpl contact;
/**
* Default constructor
*
* @param contact The watcher concerned by this timeout
*/
public WatcherTimeoutTask(ContactSipImpl contact)
{
this.contact = contact;
}
/**
* Send a closing NOTIFY to the watcher
*/
public void run()
{
if (contact.getServerDialog() == null)
{
logger.warn("serverdialog null, we won't send the closing"
+ " NOTIFY");
return;
}
ClientTransaction transac;
try
{
transac = createNotify(this.contact,
getPidfPresenceStatus((ContactSipImpl)
getLocalContactForDst(contact)),
SubscriptionStateHeader.TERMINATED,
SubscriptionStateHeader.TIMEOUT);
}
catch (OperationFailedException e)
{
logger.error("failed to create the new notify", e);
return;
}
try
{
contact.getServerDialog().sendRequest(transac);
}
catch (Exception e)
{
logger.error("Can't send the request", e);
return;
}
synchronized (ourWatchers)
{
ourWatchers.remove(this.contact);
}
this.contact.setServerDialog(null);
}
}
protected class RePublishTask extends TimerTask
{
/**
* Send a new PUBLISH request to refresh the publication
*/
public void run()
{
Request req = null;
try
{
if (distantPAET != null)
{
req = createPublish(subscriptionDuration, false);
}
else
{
// if the last publication failed for any reason, send a
// new publication, not a refresh
req = createPublish(subscriptionDuration, true);
}
}
catch (OperationFailedException e)
{
logger.error("can't create a new PUBLISH message", e);
return;
}
ClientTransaction transac = null;
try
{
transac = parentProvider
.getDefaultJainSipProvider().getNewClientTransaction(req);
}
catch (TransactionUnavailableException e)
{
logger.error("can't create the client transaction", e);
return;
}
try
{
transac.sendRequest();
}
catch (SipException e)
{
logger.error("can't send the PUBLISH request", e);
return;
}
}
}
protected class PollOfflineContactsTask extends TimerTask
{
/**
* Check if we can't subscribe to this contact now
*/
public void run()
{
// send a subscription for every contact
Iterator groupsIter = getServerStoredContactListRoot()
.subgroups();
while (groupsIter.hasNext())
{
ContactGroupSipImpl group = (ContactGroupSipImpl)
groupsIter.next();
Iterator contactsIter = group.contacts();
while (contactsIter.hasNext())
{
ContactSipImpl contact = (ContactSipImpl)
contactsIter.next();
// poll this contact
forcePollContact(contact);
}
}
}
}
protected class RegistrationListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenever
* a change in the registration state of the corresponding provider had
* occurred. The method is particularly interested in events stating
* that the SIP 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)
{
if(evt.getNewState() == RegistrationState.UNREGISTERING)
{
// stop any task associated with the timer
cancelTimer();
// this will not be called by anyone else, so call it
// the method will terminate every active subscription
try
{
publishPresenceStatus(
sipStatusEnum.getStatus(SipStatusEnum.OFFLINE), "");
}
catch (OperationFailedException e)
{
logger.error("can't set the offline mode", e);
}
// we wait for every SUBSCRIBE, NOTIFY and PUBLISH transaction
// to finish before continuing the unsubscription
for (byte i = 0; i < 10; i++)
{ // wait 5 s. max
synchronized (waitedCallIds)
{
if (waitedCallIds.size() == 0)
{
break;
}
}
synchronized (this)
{
try
{
wait(500);
}
catch (InterruptedException e)
{
logger.debug("abnormal behavior, may cause " +
"unnecessary CPU use", e);
}
}
}
}
else if (evt.getNewState().equals(
RegistrationState.REGISTERED))
{
logger.debug("enter registered state");
/*
* If presence support is enabled and the keep-alive method
* is REGISTER, we'll get RegistrationState.REGISTERED more
* than one though we're already registered. If we're
* receiving such subsequent REGISTERED, we don't have to do
* anything because we've already set it up in response to
* the first REGISTERED.
*/
if ((presenceEnabled == false) || (pollingTask != null))
{
return;
}
// send a subscription for every contact
Iterator<ContactGroup> groupsIter
= getServerStoredContactListRoot().subgroups();
while (groupsIter.hasNext())
{
ContactGroupSipImpl group = (ContactGroupSipImpl)
groupsIter.next();
Iterator contactsIter = group.contacts();
while (contactsIter.hasNext())
{
ContactSipImpl contact = (ContactSipImpl)
contactsIter.next();
if (contact.isResolved())
{
logger.debug("contact " + contact
+ " already resolved");
continue;
}
// try to subscribe to this contact
forcePollContact(contact);
}
}
// create the new polling task
pollingTask = new PollOfflineContactsTask();
// start polling the offline contacts
getTimer().schedule(pollingTask, pollingTaskPeriod,
pollingTaskPeriod);
}
else if(evt.getNewState()
== RegistrationState.CONNECTION_FAILED)
{
// if connection failed we have lost network connectivity
// we must fire that all contacts has gone offline
Iterator groupsIter = getServerStoredContactListRoot()
.subgroups();
while(groupsIter.hasNext())
{
ContactGroupSipImpl group
= (ContactGroupSipImpl)groupsIter.next();
Iterator<Contact> contactsIter = group.contacts();
while(contactsIter.hasNext())
{
ContactSipImpl contact
= (ContactSipImpl)contactsIter.next();
PresenceStatus oldContactStatus
= contact.getPresenceStatus();
contact.setResolved(false);
contact.setClientDialog(null);
if(!oldContactStatus.isOnline())
continue;
contact.setPresenceStatus(
sipStatusEnum.getStatus(SipStatusEnum.OFFLINE));
fireContactPresenceStatusChangeEvent(
contact
, contact.getParentContactGroup()
, oldContactStatus);
}
}
// stop any task associated with the timer
cancelTimer();
waitedCallIds.clear();
}
}
}
}