/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.sip;
import java.text.*;
import java.util.*;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;
/**
* Implements the subscriber part of RFC 3265
* "Session Initiation Protocol (SIP)-Specific Event Notification" and thus
* eases the creation of event package-specific implementations.
*
* @author Lubomir Marinov
* @author Benoit Pradelle
* @author Emil Ivov
*/
public class EventPackageSubscriber
extends EventPackageSupport
{
/**
* Out class logger.
*/
private static final Logger logger
= Logger.getLogger(EventPackageSubscriber.class);
/**
* The number of seconds before a subscription managed by this instance
* expires that this subscriber should attempt to refresh it.
*/
private final int refreshMargin;
/**
* A reference to the SipMessageFactory instance that we should
* use when creating requests.
*/
private final SipMessageFactory messageFactory;
/**
* Initializes a new EventPackageSubscriber instance which is
* to provide subscriber support according to RFC 3265 to a specific SIP
* ProtocolProviderService implementation for a specific event
* package.
*
* @param protocolProvider
* the SIP ProtocolProviderService implementation
* for which the new instance is to provide subscriber support
* for a specific event package
* @param eventPackage
* the name of the event package the new instance is to implement
* and carry in the Event and Allow-Events headers
* @param subscriptionDuration
* the duration of each subscription to be managed by the new
* instance and to be carried in the Expires headers
* @param contentSubType
* the sub-type of the content type of the NOTIFY bodies to be
* announced, expected and supported by the subscriptions to be
* managed by the new instance
* @param timer
* the Timer support which is to refresh the
* subscriptions to be managed by the new instance
* @param refreshMargin
* the number of seconds before a subscription to be managed by
* the new instance expires that the new instance should attempt
* to refresh it
*/
public EventPackageSubscriber(
ProtocolProviderServiceSipImpl protocolProvider,
String eventPackage,
int subscriptionDuration,
String contentSubType,
TimerScheduler timer,
int refreshMargin)
{
super(
protocolProvider,
eventPackage,
subscriptionDuration,
contentSubType,
timer);
this.refreshMargin = refreshMargin;
this.messageFactory = protocolProvider.getMessageFactory();
}
/**
* Creates a new SUBSCRIBE request in the form of a
* ClientTransaction with the parameters of a specific
* Subscription.
*
* @param subscription
* the Subscription to be described in a SUBSCRIBE
* request
* @param dialog
* the Dialog with which this request should be
* associated
* @param expires
* the subscription duration of the SUBSCRIBE request to be
* created
* @return a new ClientTransaction initialized with a new
* SUBSCRIBE request which matches the parameters of the specified
* Subscription and is associated with the specified
* Dialog
* @throws OperationFailedException
* if the message could not be generated
*/
private ClientTransaction createSubscription(
Subscription subscription,
Dialog dialog,
int expires)
throws OperationFailedException
{
Request req = messageFactory.createRequest(dialog, Request.SUBSCRIBE);
// Address
Address toAddress = dialog.getRemoteTarget();
// no Contact field
if (toAddress == null)
toAddress = dialog.getRemoteParty();
//MaxForwards
MaxForwardsHeader maxForwards = protocolProvider.getMaxForwardsHeader();
req.setHeader(maxForwards);
/*
* Create the transaction and 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(String).
*/
ClientTransaction transac = null;
try
{
transac = protocolProvider
.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);
}
populateSubscribeRequest(req, subscription, expires);
return transac;
}
/**
* Creates a new SUBSCRIBE request in the form of a
* ClientTransaction with the parameters of a specific
* Subscription.
*
* @param subscription
* the Subscription to be described in a SUBSCRIBE
* request
* @param expires
* the subscription duration of the SUBSCRIBE request to be
* created
* @return a new ClientTransaction initialized with a new
* SUBSCRIBE request which matches the parameters of the specified
* Subscription
* @throws OperationFailedException
* if the request could not be generated
*/
private ClientTransaction createSubscription(
Subscription subscription,
int expires)
throws OperationFailedException
{
Address toAddress = subscription.getAddress();
HeaderFactory headerFactory = protocolProvider.getHeaderFactory();
// Call ID
CallIdHeader callIdHeader
= protocolProvider.getDefaultJainSipProvider().getNewCallId();
//CSeq
CSeqHeader cSeqHeader;
try
{
cSeqHeader
= headerFactory.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 = SipMessageFactory.generateLocalTag();
FromHeader fromHeader;
ToHeader toHeader;
try
{
//FromHeader
fromHeader
= headerFactory
.createFromHeader(
protocolProvider.getOurSipAddress(toAddress),
localTag);
//ToHeader
toHeader = headerFactory.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 viaHeaders
= protocolProvider.getLocalViaHeaders(toAddress);
//MaxForwards
MaxForwardsHeader maxForwards = protocolProvider.getMaxForwardsHeader();
Request req;
try
{
req
= protocolProvider
.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);
}
populateSubscribeRequest(req, subscription, expires);
//Transaction
ClientTransaction subscribeTransaction;
try
{
subscribeTransaction
= protocolProvider
.getDefaultJainSipProvider()
.getNewClientTransaction(req);
}
catch (TransactionUnavailableException ex)
{
logger.error(
"Failed to create subscribe transaction.\n"
+ "This is most probably a network connection error.",
ex);
throw new OperationFailedException(
"Failed to create the subscription transaction",
OperationFailedException.NETWORK_FAILURE);
}
return subscribeTransaction;
}
/**
* Gets the Subscription from the list of subscriptions managed by
* this instance which is associated with a specific subscription
* Address/Request URI and has a specific id tag in its Event
* header.
*
* @param toAddress the subscription Address/Request URI of the
* Subscription to be retrieved
* @param eventId the id tag placed in the Event header of the
* Subscription to be retrieved if there is one or null if
* the Subscription should have no id tag in its Event header
* @return an existing Subscription from the list of subscriptions
* managed by this instance with the specified subscription
* Address/Request URI and the specified id tag in its Event
* header; null if no such Subscription exists in the list
* of subscriptions managed by this instance
*/
@Override
protected Subscription getSubscription(Address toAddress, String eventId)
{
return (Subscription) super.getSubscription(toAddress, eventId);
}
/**
* Gets the Subscription from the list of subscriptions managed by
* this instance which is associated with a specific CallId.
*
* @param callId the CallId associated with the Subscription to be
* retrieved
* @return an existing Subscription from the list of subscriptions
* managed by this instance which is associated with the specified CallId;
* null if no such Subscription exists in the list of
* subscriptions managed by this instance
*/
@Override
protected Subscription getSubscription(String callId)
{
return (Subscription) super.getSubscription(callId);
}
/**
* Adds a specific Subscription to the list of subscriptions
* managed by this instance only if another Subscription with the
* same subscription Address/Request URI and id tag of its
* associated Event header does not exist in the list.
*
* @param subscription the new Subscription to be added to the list
* of subscriptions managed by this instance if there is no other
* Subscription in the list which has the same subscription
* Address/Request URI and id tag of its Event header
* @throws OperationFailedException if we fail constructing or sending the
* subscription request
*/
public void poll(Subscription subscription)
throws OperationFailedException
{
if (getSubscription(
subscription.getAddress(),
subscription.getEventId())
== null)
subscribe(subscription);
}
/**
* Populates a specific Request instance with the headers
* common to dialog-creating Requests and ones sent inside
* existing dialogs and specific to the general event package subscription
* functionality that this instance and a specific Subscription
* represent.
*
* @param req
* the Request instance to be populated with common
* headers and ones specific to the event package of a specific
* Subscription
* @param subscription
* the Subscription which is to be described in the
* specified Request i.e. its properties are to be
* used to populate the specified Request
* @param expires
* the subscription duration to be set into the Expires header of
* the specified SUBSCRIBE Request
* @throws OperationFailedException if we fail parsing or populating the
* subscription request.
*/
protected void populateSubscribeRequest(
Request req,
Subscription subscription,
int expires)
throws OperationFailedException
{
HeaderFactory headerFactory = protocolProvider.getHeaderFactory();
// Event
EventHeader evHeader;
try
{
evHeader = headerFactory.createEventHeader(eventPackage);
String eventId = subscription.getEventId();
if (eventId != null)
evHeader.setEventId(eventId);
}
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);
}
req.setHeader(evHeader);
// Accept
AcceptHeader accept;
try
{
accept
= headerFactory
.createAcceptHeader("application", contentSubType);
}
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;
try
{
expHeader = headerFactory.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);
}
/**
* Implements {@link MethodProcessor#processRequest(RequestEvent)}. Handles
* only NOTIFY requests because they are the only requests concerning event
* package subscribers and if the processing of a given request requires
* event package-specific handling, delivers the request to the matching
* Subscription instance. Examples of such event package-specific handling
* include handling the termination of an existing Subscription and
* processing the bodies of the NOTIFY requests for active Subscriptions.
*
* @param requestEvent a RequestEvent specifying the SIP
* Request to be processed
* @return true if the SIP Request specified by
* requestEvent was processed; otherwise, false
*/
@Override
public boolean processRequest(RequestEvent requestEvent)
{
Request request = requestEvent.getRequest();
EventHeader eventHeader
= (EventHeader) request.getHeader(EventHeader.NAME);
if ((eventHeader == null)
|| !eventPackage.equalsIgnoreCase(eventHeader.getEventType()))
{
/*
* We are not concerned by this request, perhaps another listener
* is. So don't send a 489 / Bad event answer here.
*/
return false;
}
if (!Request.NOTIFY.equals(request.getMethod()))
return false;
if (logger.isDebugEnabled())
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;
}
String sstate = sstateHeader.getState();
ServerTransaction serverTransaction
= getOrCreateServerTransaction(requestEvent);
// first handle the case of a contact still pending
// it's possible if the NOTIFY arrives before the OK
CallIdHeader callIdHeader
= (CallIdHeader) request.getHeader(CallIdHeader.NAME);
String callId = callIdHeader.getCallId();
Subscription subscription = getSubscription(callId);
// see if the notify correspond to an existing subscription
if ((subscription == null)
&& !SubscriptionStateHeader.TERMINATED.equalsIgnoreCase(sstate))
{
if (logger.isDebugEnabled())
logger.debug("subscription not found for callId " + callId);
// send a 481 response (rfc3625)
Response response;
try
{
response
= protocolProvider
.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(contentSubType))
{
// send a 415 response (rfc3261)
Response response;
try
{
response
= protocolProvider
.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;
try
{
acceptHeader
= protocolProvider
.getHeaderFactory()
.createAcceptHeader("application", contentSubType);
}
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 (SubscriptionStateHeader.TERMINATED.equalsIgnoreCase(sstate))
{
// if we requested this end of subscription, subscription == null
if (subscription != null)
{
removeSubscription(callId, subscription);
subscription
.processTerminatedRequest(
requestEvent,
sstateHeader.getReasonCode());
}
}
// send an OK response
Response response;
try
{
response
= protocolProvider
.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 (subscription != null)
subscription
.processActiveRequest(requestEvent, request.getRawContent());
return true;
}
/**
* Implements {@link MethodProcessor#processResponse(ResponseEvent)}.
* Handles only responses to SUBSCRIBE requests because they are the only
* requests concerning event package subscribers (and the only requests sent
* by them, for that matter) and if the processing of a given response
* requires event package-specific handling, delivers the response to the
* matching Subscription instance. Examples of such event
* package-specific handling include letting the respective
* Subscription handle the success or failure in the establishment
* of a subscription.
*
* @param responseEvent a ResponseEvent specifying the SIP
* Response to be processed
* @return true if the SIP Response specified by
* responseEvent was processed; otherwise, false
*/
@Override
public boolean processResponse(ResponseEvent responseEvent)
{
Response response = responseEvent.getResponse();
CSeqHeader cseqHeader
= (CSeqHeader) response.getHeader(CSeqHeader.NAME);
if (cseqHeader == null)
{
logger.error("An incoming response did not contain a CSeq header");
return false;
}
if (!Request.SUBSCRIBE.equals(cseqHeader.getMethod()))
return false;
ClientTransaction clientTransaction
= responseEvent.getClientTransaction();
Request request = clientTransaction.getRequest();
/*
* Don't handle responses to requests not coming from this event
* package.
*/
if (request != null)
{
EventHeader eventHeader
= (EventHeader) request.getHeader(EventHeader.NAME);
if ((eventHeader == null)
|| !eventPackage
.equalsIgnoreCase(eventHeader.getEventType()))
return false;
}
// Find the subscription.
CallIdHeader callIdHeader
= (CallIdHeader) response.getHeader(CallIdHeader.NAME);
String callId = callIdHeader.getCallId();
Subscription subscription = getSubscription(callId);
// 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();
int statusCode = response.getStatusCode();
SipProvider sourceProvider = (SipProvider) responseEvent.getSource();
if (((expHeader != null) && (expHeader.getExpires() == 0))
|| (subscription == null)) // this handle the unsubscription
// case where we removed the contact
// from subscribedContacts
{
boolean processed = false;
if ((statusCode == Response.UNAUTHORIZED)
|| (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED))
{
try
{
processAuthenticationChallenge(
clientTransaction,
response,
sourceProvider);
processed = true;
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
}
}
else if ((statusCode != Response.OK)
&& (statusCode != Response.ACCEPTED))
processed = true;
// any other cases (200/202) will imply a NOTIFY, so we will
// handle the end of a subscription there
return processed;
}
if ((statusCode >= Response.OK)
&& (statusCode < Response.MULTIPLE_CHOICES))
{
// OK (200/202)
if ((statusCode == Response.OK)
|| (statusCode == Response.ACCEPTED))
{
if (expHeader == null)
{
// not conform to rfc3265
logger.error("no Expires header in this response");
return false;
}
SubscriptionRefreshTask refreshTask
= new SubscriptionRefreshTask(subscription);
subscription.setTimerTask(refreshTask);
int refreshDelay = expHeader.getExpires();
// try to keep a margin if the refresh delay allows it
if (refreshDelay >= (2*refreshMargin))
refreshDelay -= refreshMargin;
timer.schedule(refreshTask, refreshDelay * 1000);
// do it to remember the dialog in case of a polling
// subscription (which means no call to finalizeSubscription)
subscription.setDialog(clientTransaction.getDialog());
subscription.processSuccessResponse(responseEvent, statusCode);
}
}
else if((statusCode >= Response.MULTIPLE_CHOICES)
&& (statusCode < Response.BAD_REQUEST))
{
if (logger.isInfoEnabled())
logger.info(
"Response to subscribe to " + subscription.getAddress()
+ ": " + response.getReasonPhrase());
}
else if(statusCode >= Response.BAD_REQUEST)
{
// if the response is a 423 response, just re-send the request
// with a valid expires value
if (statusCode == 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;
}
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
= protocolProvider
.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 ((statusCode == Response.UNAUTHORIZED)
|| (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED))
{
try
{
processAuthenticationChallenge(
clientTransaction,
response,
sourceProvider);
}
catch (OperationFailedException e)
{
logger.error("can't handle the challenge", e);
removeSubscription(callId, subscription);
subscription
.processFailureResponse(responseEvent, statusCode);
}
// 408 480 486 600 603 : non definitive reject
// others: definitive reject (or not implemented)
}
else
{
if (logger.isDebugEnabled())
logger.debug("error received from the network:\n" + response);
removeSubscription(callId, subscription);
subscription.processFailureResponse(responseEvent, statusCode);
}
}
return true;
}
/**
* If we got timeout we there is a problem with the connection, lets
* inform the provider.
* Implements MethodProcessor#processTimeout(TimeoutEvent).
*/
@Override
public boolean processTimeout(TimeoutEvent timeoutEvent)
{
protocolProvider.notifyConnectionFailed();
return true;
}
/**
* Creates and sends a SUBSCRIBE request to the subscription
* Address/Request URI of a specific Subscription
* in order to request receiving event notifications and adds the specified
* Subscription to the list of subscriptions managed by this
* instance. The added Subscription may later receive
* notifications to process the Requests and/or
* Responses which constitute the signaling session associated
* with it. If the attempt to create and send the SUBSCRIBE request fails,
* the specified Subscription is not added to the list of
* subscriptions managed by this instance.
*
* @param subscription
* a Subscription which specifies the properties of
* the SUBSCRIBE request to be created and sent, to be added to
* the list of subscriptions managed by this instance
* @throws OperationFailedException if we fail constructing or sending the
* subscription request.
*/
public void subscribe(Subscription subscription)
throws OperationFailedException
{
Dialog dialog = subscription.getDialog();
if ((dialog != null)
&& DialogState.TERMINATED.equals(dialog.getState()))
dialog = null;
//create the subscription
ClientTransaction subscribeTransaction = null;
try
{
subscribeTransaction
= (dialog == null)
? createSubscription(subscription, subscriptionDuration)
: createSubscription(subscription, dialog, subscriptionDuration);
}
catch (OperationFailedException ex)
{
ProtocolProviderServiceSipImpl.throwOperationFailedException(
"Failed to create the subscription",
OperationFailedException.INTERNAL_ERROR, ex, logger);
}
// we register the contact to find him when the OK will arrive
CallIdHeader callIdHeader
= (CallIdHeader)
subscribeTransaction.getRequest().getHeader(CallIdHeader.NAME);
String callId = callIdHeader.getCallId();
addSubscription(callId, subscription);
// send the message
try
{
if (dialog == null)
subscribeTransaction.sendRequest();
else
dialog.sendRequest(subscribeTransaction);
}
catch (SipException ex)
{
// this contact will never been accepted or rejected
removeSubscription(callId, subscription);
ProtocolProviderServiceSipImpl.throwOperationFailedException(
"Failed to send the subscription",
OperationFailedException.NETWORK_FAILURE, ex, logger);
}
}
/**
* Creates and sends a SUBSCRIBE request to a specific subscription
* Address/Request URI if it matches a
* Subscription with an id tag of its Event header of
* null in the list of subscriptions managed by this instance with
* an Expires header value of zero in order to terminate receiving event
* notifications and removes the specified Subscription from
* the list of subscriptions managed by this instance. The removed
* Subscription may receive notifications to process the
* Requests and/or Responses which constitute the
* signaling session associated with it. If the attempt to create the
* SUBSCRIBE request fails, the associated Subscription is not
* removed from the list of subscriptions managed by this instance. If the
* specified Address does not identify an existing
* Subscription in the list of subscriptions managed by this
* instance, an assertion may optionally be performed or no reaction can be
* taken.
*
* @param toAddress
* a subscription Address/Request URI which
* identifies a Subscription to be removed from the
* list of subscriptions managed by this instance
* @param assertSubscribed
* true to assert if the specified subscription
* Address/Request URI does not identify an existing
* Subscription in the list of subscriptions managed
* by this instance; false to not assert if the
* mentioned condition is met
* @throws IllegalArgumentException
* if assertSubscribed is true and
* toAddress does not identify an existing
* Subscription in the list of subscriptions
* managed by this instance
* @throws OperationFailedException if we fail constructing or sending the
* unSUBSCRIBE request.
*/
public void unsubscribe(Address toAddress, boolean assertSubscribed)
throws IllegalArgumentException,
OperationFailedException
{
unsubscribe(toAddress, null, assertSubscribed);
}
/**
* Creates and sends a SUBSCRIBE request to a specific subscription
* Address/Request URI if it matches a
* Subscription with an id tag of its Event header of a
* specific value in the list of subscriptions managed by this instance with
* an Expires header value of zero in order to terminate receiving event
* notifications and removes the specified Subscription from
* the list of subscriptions managed by this instance. The removed
* Subscription may receive notifications to process the
* Requests and/or Responses which constitute the
* signaling session associated with it. If the attempt to create the
* SUBSCRIBE request fails, the associated Subscription is not
* removed from the list of subscriptions managed by this instance. If the
* specified Address does not identify an existing
* Subscription in the list of subscriptions managed by this
* instance, an assertion may optionally be performed or no reaction can be
* taken.
*
* @param toAddress
* a subscription Address/Request URI which
* identifies a Subscription to be removed from the
* list of subscriptions managed by this instance
* @param eventId
* the id tag placed in the Event header of the
* Subscription to be matched if there is one or
* null if the Subscription should have no
* id tag in its Event header
* @param assertSubscribed
* true to assert if the specified subscription
* Address/Request URI does not identify an existing
* Subscription in the list of subscriptions managed
* by this instance; false to not assert if the
* mentioned condition is met
* @throws IllegalArgumentException
* if assertSubscribed is true and
* toAddress and eventId do not
* identify an existing Subscription in the list of
* subscriptions managed by this instance
* @throws OperationFailedException if we fail constructing or sending the
* unSUBSCRIBE request.
*/
public void unsubscribe(
Address toAddress,
String eventId,
boolean assertSubscribed)
throws IllegalArgumentException,
OperationFailedException
{
Subscription subscription = getSubscription(toAddress, eventId);
if (subscription == null)
if (assertSubscribed)
throw
new IllegalArgumentException(
"trying to unregister a not registered contact");
else
return;
Dialog dialog = subscription.getDialog();
// we stop the subscription if we're subscribed to this contact
if (dialog != null)
{
String callId = dialog.getCallId().getCallId();
ClientTransaction subscribeTransaction;
try
{
subscribeTransaction
= createSubscription(subscription, dialog, 0);
}
catch (OperationFailedException e)
{
if (logger.isDebugEnabled())
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
removeSubscription(callId, subscription);
try
{
dialog.sendRequest(subscribeTransaction);
}
catch (SipException e)
{
if (logger.isDebugEnabled())
logger.debug("Can't send the request", e);
throw
new OperationFailedException(
"Failed to send the subscription message",
OperationFailedException.NETWORK_FAILURE,
e);
}
}
}
/**
* Represents a general event package subscription in the sense of RFC 3265
* "Session Initiation Protocol (SIP)-Specific Event Notification" from the
* point of view of the subscriber and its signaling characteristics such as
* Request URI, id tag value of its Event header, the Dialog
* which has been created by the associated SUBSCRIBE request or through
* which it was sent. Additionally, represents the subscription-specific
* processing of the related Requests and Response
* s thus allowing implementers to tap into the general event package
* subscription operations and provide the event package-specific
* processing.
*
* @author Lubomir Marinov
*/
public static abstract class Subscription
extends EventPackageSupport.Subscription
{
/**
* Initializes a new Subscription instance with a specific
* subscription Address/Request URI and an id tag of the
* associated Event headers of value null.
*
* @param toAddress
* the subscription Address/Request URI which is
* to be the target of the SUBSCRIBE requests associated with
* the new instance
*/
public Subscription(Address toAddress)
{
this(toAddress, null);
}
/**
* Initializes a new Subscription instance with a specific
* subscription Address/Request URI and a specific id tag
* of the associated Event headers.
*
* @param toAddress
* the subscription Address/Request URI which is
* to be the target of the SUBSCRIBE requests associated with
* the new instance
* @param eventId
* the value of the id tag to be placed in the Event headers
* of the SUBSCRIBE requests created for the new instance and
* to be present in the received Event headers in order to
* have the new instance associated with them
*/
public Subscription(Address toAddress, String eventId)
{
super(toAddress, eventId);
}
/**
* Notifies this Subscription that an active NOTIFY
* Request has been received and it may process the
* specified raw content carried in it.
*
* @param requestEvent
* the RequestEvent carrying the full details of
* the received NOTIFY Request including the raw
* content which may be processed by this
* Subscription
* @param rawContent
* an array of bytes which represents the raw content carried
* in the body of the received NOTIFY Request
* and extracted from the specified RequestEvent
* for the convenience of the implementers
*/
protected abstract void processActiveRequest(
RequestEvent requestEvent,
byte[] rawContent);
/**
* Notifies this Subscription that a Response
* to a previous SUBSCRIBE Request has been received with a
* status code in the failure range and it may process the status code
* carried in it.
*
* @param responseEvent
* the ResponseEvent carrying the full details
* of the received Response including the status
* code which may be processed by this
* Subscription
* @param statusCode
* the status code carried in the Response and
* extracted from the specified ResponseEvent
* for the convenience of the implementers
*/
protected abstract void processFailureResponse(
ResponseEvent responseEvent,
int statusCode);
/**
* Notifies this Subscription that a Response
* to a previous SUBSCRIBE Request has been received with a
* status code in the success range and it may process the status code
* carried in it.
*
* @param responseEvent
* the ResponseEvent carrying the full details
* of the received Response including the status
* code which may be processed by this
* Subscription
* @param statusCode
* the status code carried in the Response and
* extracted from the specified ResponseEvent
* for the convenience of the implementers
*/
protected abstract void processSuccessResponse(
ResponseEvent responseEvent,
int statusCode);
/**
* Notifies this Subscription that a terminating NOTIFY
* Request has been received and it may process the reason
* code carried in it.
*
* @param requestEvent
* the RequestEvent carrying the full details of
* the received NOTIFY Request including the
* reason code which may be processed by this
* Subscription
* @param reasonCode
* the code of the reason for the termination carried in the
* NOTIFY Request and extracted from the
* specified RequestEvent for the convenience of
* the implementers
*/
protected abstract void processTerminatedRequest(
RequestEvent requestEvent,
String reasonCode);
}
/**
* Represents a TimerTask which refreshes a specific
* Subscription.
*/
private class SubscriptionRefreshTask
extends TimerTask
{
/**
* The Subscription to be refreshed by this
* TimerTask.
*/
private final Subscription subscription;
/**
* Initializes a new SubscriptionRefreshTask which is to
* refresh a specific Subscription.
*
* @param subscription
* the Subscription to be refreshed by the new
* instance
*/
public SubscriptionRefreshTask(Subscription subscription)
{
this.subscription = subscription;
}
/**
* Refreshes the Subscription associated with this
* TimerTask.
*/
@Override
public void run()
{
Dialog dialog = subscription.getDialog();
if (dialog == null)
{
logger.warn(
"null dialog associated with "
+ subscription
+ ", can't refresh the subscription");
return;
}
ClientTransaction transac = null;
try
{
transac
= createSubscription(
subscription,
dialog,
subscriptionDuration);
}
catch (OperationFailedException e)
{
logger.error("Failed to create subscriptionTransaction.", e);
return;
}
try
{
dialog.sendRequest(transac);
}
catch (SipException e)
{
logger.error("Can't send the request", e);
}
}
}
}