/*
* Conditions Of Use
*
* This software was developed by employees of the National Institute of
* Standards and Technology (NIST), an agency of the Federal Government.
* Pursuant to title 15 Untied States Code Section 105, works of NIST
* employees are not subject to copyright protection in the United States
* and are considered to be in the public domain. As a result, a formal
* license is not needed to use the software.
*
* This software is provided by NIST as a service and is expressly
* provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
* OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
* AND DATA ACCURACY. NIST does not warrant or make any representations
* regarding the use of the software or the results thereof, including but
* not limited to the correctness, accuracy, reliability or usefulness of
* the software.
*
* Permission to use this software is contingent upon your acceptance
* of the terms of this agreement
*
* .
*
*/
package gov.nist.javax.sip.stack;
import gov.nist.core.InternalErrorHandler;
import gov.nist.javax.sip.SIPConstants;
import gov.nist.javax.sip.ServerTransactionExt;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.Utils;
import gov.nist.javax.sip.header.Expires;
import gov.nist.javax.sip.header.ParameterNames;
import gov.nist.javax.sip.header.RSeq;
import gov.nist.javax.sip.header.Via;
import gov.nist.javax.sip.header.ViaList;
import gov.nist.javax.sip.message.SIPMessage;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import java.io.IOException;
import java.text.ParseException;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.sip.Dialog;
import javax.sip.DialogState;
import javax.sip.DialogTerminatedEvent;
import javax.sip.ObjectInUseException;
import javax.sip.SipException;
import javax.sip.Timeout;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionState;
import javax.sip.address.Hop;
import javax.sip.header.ContactHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.RSeqHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
/*
* Bug fixes / enhancements:Emil Ivov, Antonis Karydas, Daniel J. Martinez Manzano, Daniel, Hagai
* Sela, Vazques-Illa, Bill Roome, Thomas Froment and Pierre De Rop, Christophe Anzille and Jeroen
* van Bemmel, Frank Reif.
* Carolyn Beeton ( Avaya ).
*
*/
/**
* Represents a server transaction. Implements the following state machines.
*
* <pre>
*
*
*
* |INVITE
* |pass INV to TU
* INVITE V send 100 if TU won't in 200ms
* send response+-----------+
* +--------| |--------+101-199 from TU
* | | Proceeding| |send response
* +------->| |<-------+
* | | Transport Err.
* | | Inform TU
* | |--------------->+
* +-----------+ |
* 300-699 from TU | |2xx from TU |
* send response | |send response |
* | +------------------>+
* | |
* INVITE V Timer G fires |
* send response+-----------+ send response |
* +--------| |--------+ |
* | | Completed | | |
* +------->| |<-------+ |
* +-----------+ |
* | | |
* ACK | | |
* - | +------------------>+
* | Timer H fires |
* V or Transport Err.|
* +-----------+ Inform TU |
* | | |
* | Confirmed | |
* | | |
* +-----------+ |
* | |
* |Timer I fires |
* |- |
* | |
* V |
* +-----------+ |
* | | |
* | Terminated|<---------------+
* | |
* +-----------+
*
* Figure 7: INVITE server transaction
* Request received
* |pass to TU
*
* V
* +-----------+
* | |
* | Trying |-------------+
* | | |
* +-----------+ |200-699 from TU
* | |send response
* |1xx from TU |
* |send response |
* | |
* Request V 1xx from TU |
* send response+-----------+send response|
* +--------| |--------+ |
* | | Proceeding| | |
* +------->| |<-------+ |
* +<--------------| | |
* |Trnsprt Err +-----------+ |
* |Inform TU | |
* | | |
* | |200-699 from TU |
* | |send response |
* | Request V |
* | send response+-----------+ |
* | +--------| | |
* | | | Completed |<------------+
* | +------->| |
* +<--------------| |
* |Trnsprt Err +-----------+
* |Inform TU |
* | |Timer J fires
* | |-
* | |
* | V
* | +-----------+
* | | |
* +-------------->| Terminated|
* | |
* +-----------+
*
*
*
*
*
* </pre>
*
* @version 1.2 $Revision: 1.118 $ $Date: 2010/01/10 00:13:14 $
* @author M. Ranganathan
*
*/
public class SIPServerTransaction extends SIPTransaction implements ServerRequestInterface,
javax.sip.ServerTransaction, ServerTransactionExt {
// force the listener to see transaction
private int rseqNumber;
// private LinkedList pendingRequests;
// Real RequestInterface to pass messages to
private transient ServerRequestInterface requestOf;
private SIPDialog dialog;
// the unacknowledged SIPResponse
private SIPResponse pendingReliableResponse;
// The pending reliable Response Timer
private ProvisionalResponseTask provisionalResponseTask;
private boolean retransmissionAlertEnabled;
private RetransmissionAlertTimerTask retransmissionAlertTimerTask;
protected boolean isAckSeen;
private SIPClientTransaction pendingSubscribeTransaction;
private SIPServerTransaction inviteTransaction;
private Semaphore provisionalResponseSem = new Semaphore(1);
/**
* This timer task is used for alerting the application to send retransmission alerts.
*
*
*/
class RetransmissionAlertTimerTask extends SIPStackTimerTask {
String dialogId;
int ticks;
int ticksLeft;
public RetransmissionAlertTimerTask(String dialogId) {
this.ticks = SIPTransaction.T1;
this.ticksLeft = this.ticks;
}
protected void runTask() {
SIPServerTransaction serverTransaction = SIPServerTransaction.this;
ticksLeft--;
if (ticksLeft == -1) {
serverTransaction.fireRetransmissionTimer();
this.ticksLeft = 2 * ticks;
}
}
}
class ProvisionalResponseTask extends SIPStackTimerTask {
int ticks;
int ticksLeft;
public ProvisionalResponseTask() {
this.ticks = SIPTransaction.T1;
this.ticksLeft = this.ticks;
}
protected void runTask() {
SIPServerTransaction serverTransaction = SIPServerTransaction.this;
/*
* The reliable provisional response is passed to the transaction layer periodically
* with an interval that starts at T1 seconds and doubles for each retransmission (T1
* is defined in Section 17 of RFC 3261). Once passed to the server transaction, it is
* added to an internal list of unacknowledged reliable provisional responses. The
* transaction layer will forward each retransmission passed from the UAS core.
*
* This differs from retransmissions of 2xx responses, whose intervals cap at T2
* seconds. This is because retransmissions of ACK are triggered on receipt of a 2xx,
* but retransmissions of PRACK take place independently of reception of 1xx.
*/
// If the transaction has terminated,
if (serverTransaction.isTerminated()) {
this.cancel();
} else {
ticksLeft--;
if (ticksLeft == -1) {
serverTransaction.fireReliableResponseRetransmissionTimer();
this.ticksLeft = 2 * ticks;
this.ticks = this.ticksLeft;
// timer H MUST be set to fire in 64*T1 seconds for all transports. Timer H
// determines when the server
// transaction abandons retransmitting the response
if (this.ticksLeft >= SIPTransaction.TIMER_H) {
this.cancel();
setState(TERMINATED_STATE);
fireTimeoutTimer();
}
}
}
}
}
/**
* This timer task will terminate the transaction if the listener does not respond in a
* pre-determined time period. This helps prevent buggy listeners (who fail to respond) from
* causing memory leaks. This allows a container to protect itself from buggy code ( that
* fails to respond to a server transaction).
*
*/
class ListenerExecutionMaxTimer extends SIPStackTimerTask {
SIPServerTransaction serverTransaction = SIPServerTransaction.this;
ListenerExecutionMaxTimer() {
}
protected void runTask() {
try {
if (serverTransaction.getState() == null) {
serverTransaction.terminate();
SIPTransactionStack sipStack = serverTransaction.getSIPStack();
sipStack.removePendingTransaction(serverTransaction);
sipStack.removeTransaction(serverTransaction);
}
} catch (Exception ex) {
sipStack.getStackLogger().logError("unexpected exception", ex);
}
}
}
/**
* This timer task is for INVITE server transactions. It will send a trying in 200 ms. if the
* TU does not do so.
*
*/
class SendTrying extends SIPStackTimerTask {
protected SendTrying() {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("scheduled timer for " + SIPServerTransaction.this);
}
protected void runTask() {
SIPServerTransaction serverTransaction = SIPServerTransaction.this;
TransactionState realState = serverTransaction.getRealState();
if (realState == null || TransactionState.TRYING == realState) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug(" sending Trying current state = "
+ serverTransaction.getRealState());
try {
serverTransaction.sendMessage(serverTransaction.getOriginalRequest()
.createResponse(100, "Trying"));
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug(" trying sent "
+ serverTransaction.getRealState());
} catch (IOException ex) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logError("IO error sending TRYING");
}
}
}
}
class TransactionTimer extends SIPStackTimerTask {
public TransactionTimer() {
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug("TransactionTimer() : " + getTransactionId());
}
}
protected void runTask() {
// If the transaction has terminated,
if (isTerminated()) {
// Keep the transaction hanging around in the transaction table
// to catch the incoming ACK -- this is needed for tcp only.
// Note that the transaction record is actually removed in
// the connection linger timer.
try {
this.cancel();
} catch (IllegalStateException ex) {
if (!sipStack.isAlive())
return;
}
// Oneshot timer that garbage collects the SeverTransaction
// after a scheduled amount of time. The linger timer allows
// the client side of the tx to use the same connection to
// send an ACK and prevents a race condition for creation
// of new server tx
TimerTask myTimer = new LingerTimer();
sipStack.getTimer().schedule(myTimer,
SIPTransactionStack.CONNECTION_LINGER_TIME * 1000);
} else {
// Add to the fire list -- needs to be moved
// outside the synchronized block to prevent
// deadlock.
fireTimer();
}
}
}
/**
* Send a response.
*
* @param transactionResponse -- the response to send
*
*/
private void sendResponse(SIPResponse transactionResponse) throws IOException {
try {
// RFC18.2.2. Sending Responses
// The server transport uses the value of the top Via header field
// in
// order
// to determine where to send a response.
// It MUST follow the following process:
// If the "sent-protocol" is a reliable transport
// protocol such as TCP or SCTP,
// or TLS over those, the response MUST be
// sent using the existing connection
// to the source of the original request
// that created the transaction, if that connection is still open.
if (isReliable()) {
getMessageChannel().sendMessage(transactionResponse);
// TODO If that connection attempt fails, the server SHOULD
// use SRV 3263 procedures
// for servers in order to determine the IP address
// and port to open the connection and send the response to.
} else {
Via via = transactionResponse.getTopmostVia();
String transport = via.getTransport();
if (transport == null)
throw new IOException("missing transport!");
// @@@ hagai Symmetric NAT support
int port = via.getRPort();
if (port == -1)
port = via.getPort();
if (port == -1) {
if (transport.equalsIgnoreCase("TLS"))
port = 5061;
else
port = 5060;
}
// Otherwise, if the Via header field value contains a
// "maddr" parameter, the response MUST be forwarded to
// the address listed there, using the port indicated in
// "sent-by",
// or port 5060 if none is present. If the address is a
// multicast
// address, the response SHOULD be sent using
// the TTL indicated in the "ttl" parameter, or with a
// TTL of 1 if that parameter is not present.
String host = null;
if (via.getMAddr() != null) {
host = via.getMAddr();
} else {
// Otherwise (for unreliable unicast transports),
// if the top Via has a "received" parameter, the response
// MUST
// be sent to the
// address in the "received" parameter, using the port
// indicated
// in the
// "sent-by" value, or using port 5060 if none is specified
// explicitly.
host = via.getParameter(Via.RECEIVED);
if (host == null) {
// Otherwise, if it is not receiver-tagged, the response
// MUST be
// sent to the address indicated by the "sent-by" value,
// using the procedures in Section 5
// RFC 3263 PROCEDURE TO BE DONE HERE
host = via.getHost();
}
}
Hop hop = sipStack.addressResolver.resolveAddress(new HopImpl(host, port,
transport));
MessageChannel messageChannel = ((SIPTransactionStack) getSIPStack())
.createRawMessageChannel(this.getSipProvider().getListeningPoint(
hop.getTransport()).getIPAddress(), this.getPort(), hop);
if (messageChannel != null)
messageChannel.sendMessage(transactionResponse);
else
throw new IOException("Could not create a message channel for " + hop);
}
} finally {
this.startTransactionTimer();
}
}
/**
* Creates a new server transaction.
*
* @param sipStack Transaction stack this transaction belongs to.
* @param newChannelToUse Channel to encapsulate.
*/
protected SIPServerTransaction(SIPTransactionStack sipStack, MessageChannel newChannelToUse) {
super(sipStack, newChannelToUse);
if (sipStack.maxListenerResponseTime != -1) {
sipStack.getTimer().schedule(new ListenerExecutionMaxTimer(),
sipStack.maxListenerResponseTime * 1000);
}
this.rseqNumber = (int) (Math.random() * 1000);
// Only one outstanding request for a given server tx.
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug("Creating Server Transaction" + this.getBranchId());
sipStack.getStackLogger().logStackTrace();
}
}
/**
* Sets the real RequestInterface this transaction encapsulates.
*
* @param newRequestOf RequestInterface to send messages to.
*/
public void setRequestInterface(ServerRequestInterface newRequestOf) {
requestOf = newRequestOf;
}
/**
* Returns this transaction.
*/
public MessageChannel getResponseChannel() {
return this;
}
/**
* Determines if the message is a part of this transaction.
*
* @param messageToTest Message to check if it is part of this transaction.
*
* @return True if the message is part of this transaction, false if not.
*/
public boolean isMessagePartOfTransaction(SIPMessage messageToTest) {
// List of Via headers in the message to test
ViaList viaHeaders;
// Topmost Via header in the list
Via topViaHeader;
// Branch code in the topmost Via header
String messageBranch;
// Flags whether the select message is part of this transaction
boolean transactionMatches;
transactionMatches = false;
String method = messageToTest.getCSeq().getMethod();
// Invite Server transactions linger in the terminated state in the
// transaction
// table and are matched to compensate for
// http://bugs.sipit.net/show_bug.cgi?id=769
if ((method.equals(Request.INVITE) || !isTerminated())) {
// Get the topmost Via header and its branch parameter
viaHeaders = messageToTest.getViaHeaders();
if (viaHeaders != null) {
topViaHeader = (Via) viaHeaders.getFirst();
messageBranch = topViaHeader.getBranch();
if (messageBranch != null) {
// If the branch parameter exists but
// does not start with the magic cookie,
if (!messageBranch.toLowerCase().startsWith(
SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE)) {
// Flags this as old
// (RFC2543-compatible) client
// version
messageBranch = null;
}
}
// If a new branch parameter exists,
if (messageBranch != null && this.getBranch() != null) {
if (method.equals(Request.CANCEL)) {
// Cancel is handled as a special case because it
// shares the same same branch id of the invite
// that it is trying to cancel.
transactionMatches = this.getMethod().equals(Request.CANCEL)
&& getBranch().equalsIgnoreCase(messageBranch)
&& topViaHeader.getSentBy().equals(
((Via) getOriginalRequest().getViaHeaders().getFirst())
.getSentBy());
} else {
// Matching server side transaction with only the
// branch parameter.
transactionMatches = getBranch().equalsIgnoreCase(messageBranch)
&& topViaHeader.getSentBy().equals(
((Via) getOriginalRequest().getViaHeaders().getFirst())
.getSentBy());
}
} else {
// This is an RFC2543-compliant message; this code is here
// for backwards compatibility.
// It is a weak check.
// If RequestURI, To tag, From tag, CallID, CSeq number, and
// top Via headers are the same, the
// SIPMessage matches this transaction. An exception is for
// a CANCEL request, which is not deemed
// to be part of an otherwise-matching INVITE transaction.
String originalFromTag = super.fromTag;
String thisFromTag = messageToTest.getFrom().getTag();
boolean skipFrom = (originalFromTag == null || thisFromTag == null);
String originalToTag = super.toTag;
String thisToTag = messageToTest.getTo().getTag();
boolean skipTo = (originalToTag == null || thisToTag == null);
boolean isResponse = (messageToTest instanceof SIPResponse);
// Issue #96: special case handling for a CANCEL request -
// the CSeq method of the original request must
// be CANCEL for it to have a chance at matching.
if (messageToTest.getCSeq().getMethod().equalsIgnoreCase(Request.CANCEL)
&& !getOriginalRequest().getCSeq().getMethod().equalsIgnoreCase(
Request.CANCEL)) {
transactionMatches = false;
} else if ((isResponse || getOriginalRequest().getRequestURI().equals(
((SIPRequest) messageToTest).getRequestURI()))
&& (skipFrom || originalFromTag != null && originalFromTag.equalsIgnoreCase(thisFromTag))
&& (skipTo || originalToTag != null && originalToTag.equalsIgnoreCase(thisToTag))
&& getOriginalRequest().getCallId().getCallId().equalsIgnoreCase(
messageToTest.getCallId().getCallId())
&& getOriginalRequest().getCSeq().getSeqNumber() == messageToTest
.getCSeq().getSeqNumber()
&& ((!messageToTest.getCSeq().getMethod().equals(Request.CANCEL)) || getOriginalRequest()
.getMethod().equals(messageToTest.getCSeq().getMethod()))
&& topViaHeader.equals(getOriginalRequest().getViaHeaders()
.getFirst())) {
transactionMatches = true;
}
}
}
}
return transactionMatches;
}
/**
* Send out a trying response (only happens when the transaction is mapped). Otherwise the
* transaction is not known to the stack.
*/
protected void map() {
// note that TRYING is a pseudo-state for invite transactions
TransactionState realState = getRealState();
if (realState == null || realState == TransactionState.TRYING) {
// JvB: Removed the condition 'dialog!=null'. Trying should also
// be
// sent by intermediate proxies. This fixes some TCK tests
// null check added as the stack may be stopped.
if (isInviteTransaction() && !this.isMapped && sipStack.getTimer() != null) {
this.isMapped = true;
// Schedule a timer to fire in 200 ms if the
// TU did not send a trying in that time.
sipStack.getTimer().schedule(new SendTrying(), 200);
} else {
isMapped = true;
}
}
// Pull it out of the pending transactions list.
sipStack.removePendingTransaction(this);
}
/**
* Return true if the transaction is known to stack.
*/
public boolean isTransactionMapped() {
return this.isMapped;
}
/**
* Process a new request message through this transaction. If necessary, this message will
* also be passed onto the TU.
*
* @param transactionRequest Request to process.
* @param sourceChannel Channel that received this message.
*/
public void processRequest(SIPRequest transactionRequest, MessageChannel sourceChannel) {
boolean toTu = false;
// Can only process a single request directed to the
// transaction at a time. For a given server transaction
// the listener sees only one event at a time.
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug("processRequest: " + transactionRequest.getFirstLine());
sipStack.getStackLogger().logDebug("tx state = " + this.getRealState());
}
try {
// If this is the first request for this transaction,
if (getRealState() == null) {
// Save this request as the one this
// transaction is handling
setOriginalRequest(transactionRequest);
this.setState(TransactionState.TRYING);
toTu = true;
this.setPassToListener();
// Rsends the TRYING on retransmission of the request.
if (isInviteTransaction() && this.isMapped) {
// JvB: also
// proxies need
// to do this
// Has side-effect of setting
// state to "Proceeding"
sendMessage(transactionRequest.createResponse(100, "Trying"));
}
// If an invite transaction is ACK'ed while in
// the completed state,
} else if (isInviteTransaction() && TransactionState.COMPLETED == getRealState()
&& transactionRequest.getMethod().equals(Request.ACK)) {
// @jvB bug fix
this.setState(TransactionState.CONFIRMED);
disableRetransmissionTimer();
if (!isReliable()) {
enableTimeoutTimer(TIMER_I);
} else {
this.setState(TransactionState.TERMINATED);
}
// JvB: For the purpose of testing a TI, added a property to
// pass it anyway
if (sipStack.isNon2XXAckPassedToListener()) {
// This is useful for test applications that want to see
// all messages.
requestOf.processRequest(transactionRequest, this);
} else {
// According to RFC3261 Application should not Ack in
// CONFIRMED state
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug("ACK received for server Tx "
+ this.getTransactionId() + " not delivering to application!");
}
this.semRelease();
}
return;
// If we receive a retransmission of the original
// request,
} else if (transactionRequest.getMethod().equals(getOriginalRequest().getMethod())) {
if (TransactionState.PROCEEDING == getRealState()
|| TransactionState.COMPLETED == getRealState()) {
this.semRelease();
// Resend the last response to
// the client
if (lastResponse != null) {
// Send the message to the client
super.sendMessage(lastResponse);
}
} else if (transactionRequest.getMethod().equals(Request.ACK)) {
// This is passed up to the TU to suppress
// retransmission of OK
if (requestOf != null)
requestOf.processRequest(transactionRequest, this);
else
this.semRelease();
}
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("completed processing retransmitted request : "
+ transactionRequest.getFirstLine() + this + " txState = "
+ this.getState() + " lastResponse = " + this.getLastResponse());
return;
}
// Pass message to the TU
if (TransactionState.COMPLETED != getRealState()
&& TransactionState.TERMINATED != getRealState() && requestOf != null) {
if (getOriginalRequest().getMethod().equals(transactionRequest.getMethod())) {
// Only send original request to TU once!
if (toTu) {
requestOf.processRequest(transactionRequest, this);
} else
this.semRelease();
} else {
if (requestOf != null)
requestOf.processRequest(transactionRequest, this);
else
this.semRelease();
}
} else {
// This seems like a common bug so I am allowing it through!
if (((SIPTransactionStack) getSIPStack()).isDialogCreated(getOriginalRequest()
.getMethod())
&& getRealState() == TransactionState.TERMINATED
&& transactionRequest.getMethod().equals(Request.ACK)
&& requestOf != null) {
SIPDialog thisDialog = (SIPDialog) this.dialog;
if (thisDialog == null || !thisDialog.ackProcessed) {
// Filter out duplicate acks
if (thisDialog != null) {
thisDialog.ackReceived(transactionRequest);
thisDialog.ackProcessed = true;
}
requestOf.processRequest(transactionRequest, this);
} else {
this.semRelease();
}
} else if (transactionRequest.getMethod().equals(Request.CANCEL)) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("Too late to cancel Transaction");
this.semRelease();
// send OK and just ignore the CANCEL.
try {
this.sendMessage(transactionRequest.createResponse(Response.OK));
} catch (IOException ex) {
// Transaction is already terminated
// just ignore the IOException.
}
}
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("Dropping request " + getRealState());
}
} catch (IOException e) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logError("IOException " ,e);
this.semRelease();
this.raiseIOExceptionEvent();
}
}
/**
* Send a response message through this transactionand onto the client. The response drives
* the state machine.
*
* @param messageToSend Response to process and send.
*/
public void sendMessage(SIPMessage messageToSend) throws IOException {
try {
// Message typecast as a response
SIPResponse transactionResponse;
// Status code of the response being sent to the client
int statusCode;
// Get the status code from the response
transactionResponse = (SIPResponse) messageToSend;
statusCode = transactionResponse.getStatusCode();
try {
// Provided we have set the banch id for this we set the BID for
// the
// outgoing via.
if (this.getOriginalRequest().getTopmostVia().getBranch() != null)
transactionResponse.getTopmostVia().setBranch(this.getBranch());
else
transactionResponse.getTopmostVia().removeParameter(ParameterNames.BRANCH);
// Make the topmost via headers match identically for the
// transaction rsponse.
if (!this.getOriginalRequest().getTopmostVia().hasPort())
transactionResponse.getTopmostVia().removePort();
} catch (ParseException ex) {
ex.printStackTrace();
}
// Method of the response does not match the request used to
// create the transaction - transaction state does not change.
if (!transactionResponse.getCSeq().getMethod().equals(
getOriginalRequest().getMethod())) {
sendResponse(transactionResponse);
return;
}
// If the TU sends a provisional response while in the
// trying state,
if (getRealState() == TransactionState.TRYING) {
if (statusCode / 100 == 1) {
this.setState(TransactionState.PROCEEDING);
} else if (200 <= statusCode && statusCode <= 699) {
// INVITE ST has TRYING as a Pseudo state
// (See issue 76). We are using the TRYING
// pseudo state invite Transactions
// to signal if the application
// has sent trying or not and hence this
// check is necessary.
if (!isInviteTransaction()) {
if (!isReliable()) {
// Linger in the completed state to catch
// retransmissions if the transport is not
// reliable.
this.setState(TransactionState.COMPLETED);
// Note that Timer J is only set for Unreliable
// transports -- see Issue 75.
/*
* From RFC 3261 Section 17.2.2 (non-invite server transaction)
*
* When the server transaction enters the "Completed" state, it MUST
* set Timer J to fire in 64*T1 seconds for unreliable transports, and
* zero seconds for reliable transports. While in the "Completed"
* state, the server transaction MUST pass the final response to the
* transport layer for retransmission whenever a retransmission of the
* request is received. Any other final responses passed by the TU to
* the server transaction MUST be discarded while in the "Completed"
* state. The server transaction remains in this state until Timer J
* fires, at which point it MUST transition to the "Terminated" state.
*/
enableTimeoutTimer(TIMER_J);
} else {
this.setState(TransactionState.TERMINATED);
}
} else {
// This is the case for INVITE server transactions.
// essentially, it duplicates the code in the
// PROCEEDING case below. There is no TRYING state for INVITE
// transactions in the RFC. We are using it to signal whether the
// application has sent a provisional response or not. Hence
// this is treated the same as as Proceeding.
if (statusCode / 100 == 2) {
// Status code is 2xx means that the
// transaction transitions to TERMINATED
// for both Reliable as well as unreliable
// transports. Note that the dialog layer
// takes care of retransmitting 2xx final
// responses.
/*
* RFC 3261 Section 13.3.1.4 Note, however, that the INVITE server
* transaction will be destroyed as soon as it receives this final
* response and passes it to the transport. Therefore, it is necessary
* to periodically pass the response directly to the transport until
* the ACK arrives. The 2xx response is passed to the transport with
* an interval that starts at T1 seconds and doubles for each
* retransmission until it reaches T2 seconds (T1 and T2 are defined
* in Section 17). Response retransmissions cease when an ACK request
* for the response is received. This is independent of whatever
* transport protocols are used to send the response.
*/
this.disableRetransmissionTimer();
this.disableTimeoutTimer();
this.collectionTime = TIMER_J;
this.setState(TransactionState.TERMINATED);
if (this.dialog != null)
this.dialog.setRetransmissionTicks();
} else {
// This an error final response.
this.setState(TransactionState.COMPLETED);
if (!isReliable()) {
/*
* RFC 3261
*
* While in the "Proceeding" state, if the TU passes a response
* with status code from 300 to 699 to the server transaction, the
* response MUST be passed to the transport layer for
* transmission, and the state machine MUST enter the "Completed"
* state. For unreliable transports, timer G is set to fire in T1
* seconds, and is not set to fire for reliable transports.
*/
enableRetransmissionTimer();
}
enableTimeoutTimer(TIMER_H);
}
}
}
// If the transaction is in the proceeding state,
} else if (getRealState() == TransactionState.PROCEEDING) {
if (isInviteTransaction()) {
// If the response is a failure message,
if (statusCode / 100 == 2) {
// Set up to catch returning ACKs
// The transaction lingers in the
// terminated state for some time
// to catch retransmitted INVITEs
this.disableRetransmissionTimer();
this.disableTimeoutTimer();
this.collectionTime = TIMER_J;
this.setState(TransactionState.TERMINATED);
if (this.dialog != null)
this.dialog.setRetransmissionTicks();
} else if (300 <= statusCode && statusCode <= 699) {
// Set up to catch returning ACKs
this.setState(TransactionState.COMPLETED);
if (!isReliable()) {
/*
* While in the "Proceeding" state, if the TU passes a response with
* status code from 300 to 699 to the server transaction, the response
* MUST be passed to the transport layer for transmission, and the
* state machine MUST enter the "Completed" state. For unreliable
* transports, timer G is set to fire in T1 seconds, and is not set to
* fire for reliable transports.
*/
enableRetransmissionTimer();
}
enableTimeoutTimer(TIMER_H);
}
// If the transaction is not an invite transaction
// and this is a final response,
} else if (200 <= statusCode && statusCode <= 699) {
// This is for Non-invite server transactions.
// Set up to retransmit this response,
// or terminate the transaction
this.setState(TransactionState.COMPLETED);
if (!isReliable()) {
disableRetransmissionTimer();
enableTimeoutTimer(TIMER_J);
} else {
this.setState(TransactionState.TERMINATED);
}
}
// If the transaction has already completed,
} else if (TransactionState.COMPLETED == this.getRealState()) {
return;
}
try {
// Send the message to the client.
// Record the last message sent out.
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug(
"sendMessage : tx = " + this + " getState = " + this.getState());
}
lastResponse = transactionResponse;
this.sendResponse(transactionResponse);
} catch (IOException e) {
this.setState(TransactionState.TERMINATED);
this.collectionTime = 0;
throw e;
}
} finally {
this.startTransactionTimer();
}
}
public String getViaHost() {
return getMessageChannel().getViaHost();
}
public int getViaPort() {
return getMessageChannel().getViaPort();
}
/**
* Called by the transaction stack when a retransmission timer fires. This retransmits the
* last response when the retransmission filter is enabled.
*/
protected void fireRetransmissionTimer() {
try {
if (sipStack.isLoggingEnabled()) {
sipStack.getStackLogger().logDebug("fireRetransmissionTimer() -- ");
}
// Resend the last response sent by this transaction
if (isInviteTransaction() && lastResponse != null) {
// null can happen if this is terminating when the timer fires.
if (!this.retransmissionAlertEnabled || sipStack.isTransactionPendingAck(this) ) {
// Retransmit last response until ack.
if (lastResponse.getStatusCode() / 100 > 2 && !this.isAckSeen)
super.sendMessage(lastResponse);
} else {
// alert the application to retransmit the last response
SipProviderImpl sipProvider = (SipProviderImpl) this.getSipProvider();
TimeoutEvent txTimeout = new TimeoutEvent(sipProvider, this,
Timeout.RETRANSMIT);
sipProvider.handleEvent(txTimeout, this);
}
}
} catch (IOException e) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logException(e);
raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR);
}
}
private void fireReliableResponseRetransmissionTimer() {
try {
super.sendMessage(this.pendingReliableResponse);
} catch (IOException e) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logException(e);
this.setState(TransactionState.TERMINATED);
raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR);
}
}
/**
* Called by the transaction stack when a timeout timer fires.
*/
protected void fireTimeoutTimer() {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("SIPServerTransaction.fireTimeoutTimer this = " + this
+ " current state = " + this.getRealState() + " method = "
+ this.getOriginalRequest().getMethod());
if ( this.getMethod().equals(Request.INVITE) && sipStack.removeTransactionPendingAck(this) ) {
if ( sipStack.isLoggingEnabled() ) {
sipStack.getStackLogger().logDebug("Found tx pending ACK - returning");
}
return;
}
SIPDialog dialog = (SIPDialog) this.dialog;
if (((SIPTransactionStack) getSIPStack()).isDialogCreated(this.getOriginalRequest()
.getMethod())
&& (TransactionState.CALLING == this.getRealState() || TransactionState.TRYING == this
.getRealState())) {
dialog.setState(SIPDialog.TERMINATED_STATE);
} else if (getOriginalRequest().getMethod().equals(Request.BYE)) {
if (dialog != null && dialog.isTerminatedOnBye())
dialog.setState(SIPDialog.TERMINATED_STATE);
}
if (TransactionState.COMPLETED == this.getRealState() && isInviteTransaction()) {
raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR);
this.setState(TransactionState.TERMINATED);
sipStack.removeTransaction(this);
} else if (TransactionState.COMPLETED == this.getRealState() && !isInviteTransaction()) {
this.setState(TransactionState.TERMINATED);
sipStack.removeTransaction(this);
} else if (TransactionState.CONFIRMED == this.getRealState() && isInviteTransaction()) {
// TIMER_I should not generate a timeout
// exception to the application when the
// Invite transaction is in Confirmed state.
// Just transition to Terminated state.
this.setState(TransactionState.TERMINATED);
sipStack.removeTransaction(this);
} else if (!isInviteTransaction()
&& (TransactionState.COMPLETED == this.getRealState() || TransactionState.CONFIRMED == this
.getRealState())) {
this.setState(TransactionState.TERMINATED);
} else if (isInviteTransaction() && TransactionState.TERMINATED == this.getRealState()) {
// This state could be reached when retransmitting
raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR);
if (dialog != null)
dialog.setState(SIPDialog.TERMINATED_STATE);
}
}
/**
* Get the last response.
*/
public SIPResponse getLastResponse() {
return this.lastResponse;
}
/**
* Set the original request.
*/
public void setOriginalRequest(SIPRequest originalRequest) {
super.setOriginalRequest(originalRequest);
}
/*
* (non-Javadoc)
*
* @see javax.sip.ServerTransaction#sendResponse(javax.sip.message.Response)
*/
public void sendResponse(Response response) throws SipException {
SIPResponse sipResponse = (SIPResponse) response;
SIPDialog dialog = this.dialog;
if (response == null)
throw new NullPointerException("null response");
try {
sipResponse.checkHeaders();
} catch (ParseException ex) {
throw new SipException(ex.getMessage());
}
// check for meaningful response.
if (!sipResponse.getCSeq().getMethod().equals(this.getMethod())) {
throw new SipException(
"CSeq method does not match Request method of request that created the tx.");
}
/*
* 200-class responses to SUBSCRIBE requests also MUST contain an "Expires" header. The
* period of time in the response MAY be shorter but MUST NOT be longer than specified in
* the request.
*/
if (this.getMethod().equals(Request.SUBSCRIBE) && response.getStatusCode() / 100 == 2) {
if (response.getHeader(ExpiresHeader.NAME) == null) {
throw new SipException("Expires header is mandatory in 2xx response of SUBSCRIBE");
} else {
Expires requestExpires = (Expires) this.getOriginalRequest().getExpires();
Expires responseExpires = (Expires) response.getExpires();
/*
* If no "Expires" header is present in a SUBSCRIBE request, the implied default
* is defined by the event package being used.
*/
if (requestExpires != null
&& responseExpires.getExpires() > requestExpires.getExpires()) {
throw new SipException(
"Response Expires time exceeds request Expires time : See RFC 3265 3.1.1");
}
}
}
// Check for mandatory header.
if (sipResponse.getStatusCode() == 200
&& sipResponse.getCSeq().getMethod().equals(Request.INVITE)
&& sipResponse.getHeader(ContactHeader.NAME) == null)
throw new SipException("Contact Header is mandatory for the OK to the INVITE");
if (!this.isMessagePartOfTransaction((SIPMessage) response)) {
throw new SipException("Response does not belong to this transaction.");
}
// Fix up the response if the dialog has already been established.
try {
/*
* The UAS MAY send a final response to the initial request before
* having received PRACKs for all unacknowledged reliable provisional responses,
* unless the final response is 2xx and any of the unacknowledged reliable provisional
* responses contained a session description. In that case, it MUST NOT send a final
* response until those provisional responses are acknowledged.
*/
if (this.pendingReliableResponse != null
&& this.getDialog() != null
&& this.getState() != TransactionState.TERMINATED
&& ((SIPResponse)response).getContentTypeHeader() != null
&& response.getStatusCode() / 100 == 2
&& ((SIPResponse)response).getContentTypeHeader().getContentType()
.equalsIgnoreCase("application")
&& ((SIPResponse)response).getContentTypeHeader().getContentSubType()
.equalsIgnoreCase("sdp")) {
try {
boolean acquired = this.provisionalResponseSem.tryAcquire(1,TimeUnit.SECONDS);
if (!acquired ) {
throw new SipException("cannot send response -- unacked povisional");
}
} catch (Exception ex) {
this.sipStack.getStackLogger().logError("Could not acquire PRACK sem ", ex);
}
} else {
// Sending the final response cancels the
// pending response task.
if (this.pendingReliableResponse != null && sipResponse.isFinalResponse()) {
this.provisionalResponseTask.cancel();
this.provisionalResponseTask = null;
}
}
// Dialog checks. These make sure that the response
// being sent makes sense.
if (dialog != null) {
if (sipResponse.getStatusCode() / 100 == 2
&& sipStack.isDialogCreated(sipResponse.getCSeq().getMethod())) {
if (dialog.getLocalTag() == null && sipResponse.getTo().getTag() == null) {
// Trying to send final response and user forgot to set
// to
// tag on the response -- be nice and assign the tag for
// the user.
sipResponse.getTo().setTag(Utils.getInstance().generateTag());
} else if (dialog.getLocalTag() != null && sipResponse.getToTag() == null) {
sipResponse.setToTag(dialog.getLocalTag());
} else if (dialog.getLocalTag() != null && sipResponse.getToTag() != null
&& !dialog.getLocalTag().equals(sipResponse.getToTag())) {
throw new SipException("Tag mismatch dialogTag is "
+ dialog.getLocalTag() + " responseTag is "
+ sipResponse.getToTag());
}
}
if (!sipResponse.getCallId().getCallId().equals(dialog.getCallId().getCallId())) {
throw new SipException("Dialog mismatch!");
}
}
// Backward compatibility slippery slope....
// Only set the from tag in the response when the
// incoming request has a from tag.
String fromTag = ((SIPRequest) this.getRequest()).getFrom().getTag();
if (fromTag != null && sipResponse.getFromTag() != null
&& !sipResponse.getFromTag().equals(fromTag)) {
throw new SipException("From tag of request does not match response from tag");
} else if (fromTag != null) {
sipResponse.getFrom().setTag(fromTag);
} else {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("WARNING -- Null From tag in request!!");
}
// See if the dialog needs to be inserted into the dialog table
// or if the state of the dialog needs to be changed.
if (dialog != null && response.getStatusCode() != 100) {
dialog.setResponseTags(sipResponse);
DialogState oldState = dialog.getState();
dialog.setLastResponse(this, (SIPResponse) response);
if (oldState == null && dialog.getState() == DialogState.TERMINATED) {
DialogTerminatedEvent event = new DialogTerminatedEvent(dialog
.getSipProvider(), dialog);
// Provide notification to the listener that the dialog has
// ended.
dialog.getSipProvider().handleEvent(event, this);
}
} else if (dialog == null && this.getMethod().equals(Request.INVITE)
&& this.retransmissionAlertEnabled
&& this.retransmissionAlertTimerTask == null
&& response.getStatusCode() / 100 == 2) {
String dialogId = ((SIPResponse) response).getDialogId(true);
this.retransmissionAlertTimerTask = new RetransmissionAlertTimerTask(dialogId);
sipStack.retransmissionAlertTransactions.put(dialogId, this);
sipStack.getTimer().schedule(this.retransmissionAlertTimerTask, 0,
SIPTransactionStack.BASE_TIMER_INTERVAL);
}
// Send message after possibly inserting the Dialog
// into the dialog table to avoid a possible race condition.
this.sendMessage((SIPResponse) response);
if ( dialog != null ) {
dialog.startRetransmitTimer(this, (SIPResponse)response);
}
} catch (IOException ex) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logException(ex);
this.setState(TransactionState.TERMINATED);
raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR);
throw new SipException(ex.getMessage());
} catch (java.text.ParseException ex1) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logException(ex1);
this.setState(TransactionState.TERMINATED);
throw new SipException(ex1.getMessage());
}
}
/**
* Return the book-keeping information that we actually use.
*/
private TransactionState getRealState() {
return super.getState();
}
/**
* Return the current transaction state according to the RFC 3261 transaction state machine.
* Invite transactions do not have a trying state. We just use this as a pseudo state for
* processing requests.
*
* @return the state of the transaction.
*/
public TransactionState getState() {
// Trying is a pseudo state for INVITE transactions.
if (this.isInviteTransaction() && TransactionState.TRYING == super.getState())
return TransactionState.PROCEEDING;
else
return super.getState();
}
/**
* Sets a timeout after which the connection is closed (provided the server does not use the
* connection for outgoing requests in this time period) and calls the superclass to set
* state.
*/
public void setState(TransactionState newState) {
// Set this timer for connection caching
// of incoming connections.
if (newState == TransactionState.TERMINATED && this.isReliable()
&& (!getSIPStack().cacheServerConnections)) {
// Set a time after which the connection
// is closed.
this.collectionTime = TIMER_J;
}
super.setState(newState);
}
/**
* Start the timer task.
*/
protected void startTransactionTimer() {
if (this.transactionTimerStarted.compareAndSet(false, true)) {
if (sipStack.getTimer() != null) {
// The timer is set to null when the Stack is
// shutting down.
TimerTask myTimer = new TransactionTimer();
sipStack.getTimer().schedule(myTimer, BASE_TIMER_INTERVAL, BASE_TIMER_INTERVAL);
}
}
}
public boolean equals(Object other) {
if (!other.getClass().equals(this.getClass())) {
return false;
}
SIPServerTransaction sst = (SIPServerTransaction) other;
return this.getBranch().equalsIgnoreCase(sst.getBranch());
}
/*
* (non-Javadoc)
*
* @see gov.nist.javax.sip.stack.SIPTransaction#getDialog()
*/
public Dialog getDialog() {
return this.dialog;
}
/*
* (non-Javadoc)
*
* @see gov.nist.javax.sip.stack.SIPTransaction#setDialog(gov.nist.javax.sip.stack.SIPDialog,
* gov.nist.javax.sip.message.SIPMessage)
*/
public void setDialog(SIPDialog sipDialog, String dialogId) {
if (sipStack.isLoggingEnabled())
sipStack.getStackLogger().logDebug("setDialog " + this + " dialog = " + sipDialog);
this.dialog = sipDialog;
if (dialogId != null)
this.dialog.setAssigned();
if (this.retransmissionAlertEnabled && this.retransmissionAlertTimerTask != null) {
this.retransmissionAlertTimerTask.cancel();
if (this.retransmissionAlertTimerTask.dialogId != null) {
sipStack.retransmissionAlertTransactions
.remove(this.retransmissionAlertTimerTask.dialogId);
}
this.retransmissionAlertTimerTask = null;
}
this.retransmissionAlertEnabled = false;
}
/*
* (non-Javadoc)
*
* @see javax.sip.Transaction#terminate()
*/
public void terminate() throws ObjectInUseException {
this.setState(TransactionState.TERMINATED);
if (this.retransmissionAlertTimerTask != null) {
this.retransmissionAlertTimerTask.cancel();
if (retransmissionAlertTimerTask.dialogId != null) {
this.sipStack.retransmissionAlertTransactions
.remove(retransmissionAlertTimerTask.dialogId);
}
this.retransmissionAlertTimerTask = null;
}
}
protected void sendReliableProvisionalResponse(Response relResponse) throws SipException {
/*
* After the first reliable provisional response for a request has been acknowledged, the
* UAS MAY send additional reliable provisional responses. The UAS MUST NOT send a second
* reliable provisional response until the first is acknowledged.
*/
if (this.pendingReliableResponse != null) {
throw new SipException("Unacknowledged response");
} else
this.pendingReliableResponse = (SIPResponse) relResponse;
/*
* In addition, it MUST contain a Require header field containing the option tag 100rel,
* and MUST include an RSeq header field.
*/
RSeq rseq = (RSeq) relResponse.getHeader(RSeqHeader.NAME);
if (relResponse.getHeader(RSeqHeader.NAME) == null) {
rseq = new RSeq();
relResponse.setHeader(rseq);
}
try {
this.rseqNumber++;
rseq.setSeqNumber(this.rseqNumber);
// start the timer task which will retransmit the reliable response
// until the PRACK is received
this.lastResponse = (SIPResponse) relResponse;
if ( this.getDialog() != null ) {
boolean acquired = this.provisionalResponseSem.tryAcquire(1, TimeUnit.SECONDS);
if (!acquired) {
throw new SipException("Unacknowledged response");
}
}
this.sendMessage((SIPMessage) relResponse);
this.provisionalResponseTask = new ProvisionalResponseTask();
this.sipStack.getTimer().schedule(provisionalResponseTask, 0,
SIPTransactionStack.BASE_TIMER_INTERVAL);
} catch (Exception ex) {
InternalErrorHandler.handleException(ex);
}
}
public SIPResponse getReliableProvisionalResponse() {
return this.pendingReliableResponse;
}
/**
* Cancel the retransmit timer for the provisional response task.
*
* @return true if the tx has seen the prack for the first time and false otherwise.
*
*/
public boolean prackRecieved() {
if (this.pendingReliableResponse == null)
return false;
if(provisionalResponseTask != null)
this.provisionalResponseTask.cancel();
this.pendingReliableResponse = null;
this.provisionalResponseSem.release();
return true;
}
/*
* (non-Javadoc)
*
* @see javax.sip.ServerTransaction#enableRetransmissionAlerts()
*/
public void enableRetransmissionAlerts() throws SipException {
if (this.getDialog() != null)
throw new SipException("Dialog associated with tx");
else if (!this.getMethod().equals(Request.INVITE))
throw new SipException("Request Method must be INVITE");
this.retransmissionAlertEnabled = true;
}
public boolean isRetransmissionAlertEnabled() {
return this.retransmissionAlertEnabled;
}
/**
* Disable retransmission Alerts and cancel associated timers.
*
*/
public void disableRetransmissionAlerts() {
if (this.retransmissionAlertTimerTask != null && this.retransmissionAlertEnabled) {
this.retransmissionAlertTimerTask.cancel();
this.retransmissionAlertEnabled = false;
String dialogId = this.retransmissionAlertTimerTask.dialogId;
if (dialogId != null) {
sipStack.retransmissionAlertTransactions.remove(dialogId);
}
this.retransmissionAlertTimerTask = null;
}
}
/**
* This is book-keeping for retransmission filter management.
*/
public void setAckSeen() {
this.isAckSeen = true;
}
/**
* This is book-keeping for retransmission filter management.
*/
public boolean ackSeen() {
return this.isAckSeen;
}
public void setMapped(boolean b) {
this.isMapped = true;
}
public void setPendingSubscribe(SIPClientTransaction pendingSubscribeClientTx) {
this.pendingSubscribeTransaction = pendingSubscribeClientTx;
}
public void releaseSem() {
if (this.pendingSubscribeTransaction != null) {
/*
* When a notify is being processed we take a lock on the subscribe to avoid racing
* with the OK of the subscribe.
*/
pendingSubscribeTransaction.releaseSem();
} else if (this.inviteTransaction != null && this.getMethod().equals(Request.CANCEL)) {
/*
* When a CANCEL is being processed we take a nested lock on the associated INVITE
* server tx.
*/
this.inviteTransaction.releaseSem();
}
super.releaseSem();
}
/**
* The INVITE Server Transaction corresponding to a CANCEL Server Transaction.
*
* @param st -- the invite server tx corresponding to the cancel server transaction.
*/
public void setInviteTransaction(SIPServerTransaction st) {
this.inviteTransaction = st;
}
/**
* TODO -- this method has to be added to the api.
*
* @return
*/
public SIPServerTransaction getCanceledInviteTransaction() {
return this.inviteTransaction;
}
public void scheduleAckRemoval() throws IllegalStateException {
if (this.getMethod() == null || !this.getMethod().equals(Request.ACK)) {
throw new IllegalStateException("Method is null[" + (getMethod() == null)
+ "] or method is not ACK[" + this.getMethod() + "]");
}
this.startTransactionTimer();
}
}