/*
* Portions Copyright 2000-2007 Sun Microsystems, Inc. All Rights
* Reserved. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package gov.nist.siplite.stack;
import gov.nist.siplite.parser.*;
import gov.nist.siplite.header.*;
import gov.nist.siplite.message.*;
import gov.nist.siplite.*;
import gov.nist.siplite.address.*;
import gov.nist.core.*;
import java.util.*;
import java.io.IOException;
import javax.microedition.sip.SipException;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
/**
* Abstract class to support both client and server transactions.
* Provides an encapsulation of a message channel, handles timer events,
* and creation of the Via header for a message.
*
* @version JAIN-SIP-1.1
*
*/
public abstract class Transaction
extends MessageChannel {
/** Transaction timer interval. */
protected static final int BASE_TIMER_INTERVAL =
SIPTransactionStack.BASE_TIMER_INTERVAL;
/** RTT Estimate. 500ms default. */
protected static final int T1 = 500/BASE_TIMER_INTERVAL;
/** 5 sec Maximum duration a message will remain in the network */
protected static final int T4 = 5000/BASE_TIMER_INTERVAL;
/**
* The maximum retransmit interval for non-INVITE
* requests and INVITE responses
*/
protected static final int T2 = 4000/BASE_TIMER_INTERVAL;
/** INVITE request retransmit interval, for UDP only */
protected static final int TIMER_A = 1*T1;
/** INVITE transaction timeout timer */
protected static final int TIMER_B = 64*T1;
/** INVITE transaction timeout timer */
protected static final int TIMER_J = 64*T1;
/** INVITE transaction timeout timer */
protected static final int TIMER_F = 64*T1;
/** INVITE transaction timeout timer */
protected static final int TIMER_H = 64*T1;
/** INVITE transaction timeout timer */
protected static final int TIMER_I = T4;
/** INVITE transaction timeout timer */
protected static final int TIMER_K = T4;
/** INVITE transaction timeout timer */
protected static final int TIMER_D = 32000/BASE_TIMER_INTERVAL;
/** INVITE transaction timeout timer */
protected static final int TIMER_C = 3*60*1000/BASE_TIMER_INTERVAL;
/** Last response message. */
protected Response lastResponse;
/** Current SIP dialog. */
protected Dialog dialog;
/** Flag indicating an ACK was received. */
protected boolean ackSeenFlag;
/** Flag indicating listener waiting. */
protected boolean toListener;
/**
* Initialized but no state assigned.
*/
public static final int INITIAL_STATE = -1;
/**
* Trying state.
*/
public static final int TRYING_STATE = 1;
/**
* CALLING State.
*/
public static final int CALLING_STATE = 2;
/**
* Proceeding state.
*/
public static final int PROCEEDING_STATE = 3;
/**
* Completed state.
*/
public static final int COMPLETED_STATE = 4;
/**
* Confirmed state.
*/
public static final int CONFIRMED_STATE = 5;
/**
* Terminated state.
*/
public static final int TERMINATED_STATE = 6;
/**
* Maximum number of ticks between retransmissions.
*/
protected static final int MAXIMUM_RETRANSMISSION_TICK_COUNT =
4000/BASE_TIMER_INTERVAL;
/** Parent stack for this transaction. */
protected SIPTransactionStack parentStack;
/** Original request that is being handled by this transaction. */
private Request originalRequest;
/**
* Underlying channel being used to send messages for this
* transaction.
*/
protected MessageChannel encapsulatedChannel;
/** Transaction branch ID. */
private String branch;
/** Current transaction state. */
private int currentState;
/** Number of ticks the retransmission timer was set to last. */
private int retransmissionTimerLastTickCount;
/** Number of ticks before the message is retransmitted. */
private int retransmissionTimerTicksLeft;
/** Number of ticks before the transaction times out. */
private int timeoutTimerTicksLeft;
/** List of event listeners for this transaction. */
private Vector eventListeners;
/** Flag to indcate that this has been cancelled. */
protected boolean isCancelled;
/**
* Object representing the connection being held by the JSR180
* Implementation It can be either a SipClientConnection in case
* of a ClientTransaction or a SipConnectionNotifier in case of a
*ServerTransaction.
*/
protected Object applicationData;
/**
* Retrieves the application data.
* @return Object representing the connection being held by the JSR180
* Implementation. It can be either a SipClientConnection in case of a
* ClientTransaction or a SipConnectionNotifier in case of a
* ServerTransaction
*/
public Object getApplicationData() {
return applicationData;
}
/**
* Sets the application data.
* @param newApplicationData Object representing the connection being held
* by the JSR180
* Implementation. It can be either a SipClientConnection in case of a
* ClientTransaction or a SipConnectionNotifier in case of a
* ServerTransaction
*/
public void setApplicationData(Object newApplicationData) {
applicationData = newApplicationData;
}
/**
* Gets the branch identifier.
* @return the current branch id
*/
public String getBranchId() {
return branch;
}
/**
* Transaction constructor.
*
* @param newParentStack Parent stack for this transaction.
* @param newEncapsulatedChannel
* Underlying channel for this transaction.
*/
protected Transaction(
SIPTransactionStack newParentStack,
MessageChannel newEncapsulatedChannel) {
parentStack = newParentStack;
encapsulatedChannel = newEncapsulatedChannel;
currentState = INITIAL_STATE;
disableRetransmissionTimer();
disableTimeoutTimer();
eventListeners = new Vector();
// Always add the parent stack as a listener
// of this transaction
addEventListener(newParentStack);
}
/**
* Sets the request message that this transaction handles.
*
* @param newOriginalRequest Request being handled.
*/
public void setOriginalRequest(
Request newOriginalRequest) {
// Branch value of topmost Via header
String newBranch;
originalRequest = newOriginalRequest;
originalRequest.setTransaction(this);
// If the message has an explicit branch value set,
newBranch = ((ViaHeader)newOriginalRequest.getViaHeaders().
getFirst()).getBranch();
if (newBranch != null) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"Setting Branch id : " + newBranch);
}
// Override the default branch with the one
// set by the message
setBranch(newBranch);
} else {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"Branch id is null!" + newOriginalRequest.encode());
}
}
}
/**
* Gets the request being handled by this transaction.
* @return Request being handled.
*/
public Request getOriginalRequest() {
return originalRequest;
}
/**
* Gets the original request but cast to a Request structure.
* @return the request that generated this transaction.
*/
public Request getRequest() {
return (Request) originalRequest;
}
/**
* Returns a flag stating whether this transaction is for an
* INVITE request or not.
* @return True if this is an INVITE request, false if not.
*/
protected final boolean isInviteTransaction() {
return originalRequest.getMethod().equals(Request.INVITE);
}
/**
* Returns true if the transaction corresponds to a CANCEL message.
* @return true if the transaciton is a CANCEL transaction.
*/
protected final boolean isCancelTransaction() {
return originalRequest.getMethod().equals(Request.CANCEL);
}
/**
* Returns a flag that states if this is a BYE transaction.
* @return true if the transaciton is a BYE transaction.
*/
protected final boolean isByeTransaction() {
return originalRequest.getMethod().equals(Request.BYE);
}
/**
* Returns the message channel used for
* transmitting/receiving messages
* for this transaction. Made public in support of JAIN dual
* transaction model.
* @return Encapsulated MessageChannel.
*/
public MessageChannel getMessageChannel() {
return encapsulatedChannel;
}
/**
* Sets the Via header branch parameter used to identify
* this transaction.
* @param newBranch New string used as the branch
* for this transaction.
*/
public final void setBranch(String newBranch) {
branch = newBranch;
}
/**
* Gets the current setting for the branch parameter of this transaction.
* @return Branch parameter for this transaction.
*/
public final String getBranch() {
if (branch == null) {
branch = getOriginalRequest().getTopmostVia().getBranch();
}
return branch;
}
/**
* Changes the state of this transaction.
* @param newState New state of this transaction.
*/
public void setState(int newState) {
currentState = newState;
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"setState " + this + " " + newState);
}
// If this transaction is being terminated,
if (newState == TERMINATED_STATE) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"Transaction is being terminated!");
// new Exception().printStackTrace();
}
}
}
/**
* Gets the current state of this transaction.
* @return Current state of this transaction.
*/
public final int getState() {
return currentState;
}
/**
* Enables retransmission timer events for this transaction to begin in
* one tick.
*/
protected final void enableRetransmissionTimer() {
enableRetransmissionTimer(1);
}
/**
* Enables retransmission timer events for this
* transaction to begin after the number of ticks passed to
* this routine.
* @param tickCount Number of ticks before the
* next retransmission timer
* event occurs.
*/
protected final void enableRetransmissionTimer(int tickCount) {
retransmissionTimerTicksLeft =
Math.min(tickCount, MAXIMUM_RETRANSMISSION_TICK_COUNT);
retransmissionTimerLastTickCount =
retransmissionTimerTicksLeft;
}
/**
* Turns off retransmission events for this transaction.
*/
protected final void disableRetransmissionTimer() {
retransmissionTimerTicksLeft = -1;
}
/**
* Enables a timeout event to occur for this transaction after the number
* of ticks passed to this method.
* @param tickCount Number of ticks before this transaction times out.
*/
protected final void enableTimeoutTimer(int tickCount) {
timeoutTimerTicksLeft = tickCount;
}
/**
* Disabled the timeout timer.
*/
protected final void disableTimeoutTimer() {
timeoutTimerTicksLeft = -1;
}
/**
* Fired after each timer tick.
* Checks the retransmission and timeout
* timers of this transaction, and fired these events
* if necessary.
*/
synchronized final void fireTimer() {
// If the timeout timer is enabled,
if (timeoutTimerTicksLeft != -1) {
// Count down the timer, and if it has run out,
if (--timeoutTimerTicksLeft == 0) {
// Fire the timeout timer
fireTimeoutTimer();
}
}
// If the retransmission timer is enabled,
if (retransmissionTimerTicksLeft != -1) {
// Count down the timer, and if it has run out,
if (--retransmissionTimerTicksLeft == 0) {
// Enable this timer to fire again after
// twice the original time
enableRetransmissionTimer
(retransmissionTimerLastTickCount * 2);
// Fire the timeout timer
fireRetransmissionTimer();
}
}
}
/**
* Tests a message to see if it is part of this transaction.
* @param messageToHeaderTest message to be processed
* @return True if the message is part of this
* transaction, false if not.
*/
public abstract boolean
isMessagePartOfTransaction(Message messageToHeaderTest);
/**
* This method is called when this transaction's
* retransmission timer has fired.
*/
protected abstract void fireRetransmissionTimer();
/**
* This method is called when this transaction's
* timeout timer has fired.
*/
protected abstract void fireTimeoutTimer();
/**
* Tests if this transaction has terminated.
* @return Trus if this transaction is terminated, false if not.
*/
protected final boolean isTerminated() {
return (getState() == TERMINATED_STATE);
}
/**
* Gets the host.
* @return the host
*/
public String getHost() {
return encapsulatedChannel.getHost();
}
/**
* Gets the key.
* @return the key
*/
public String getKey() {
return encapsulatedChannel.getKey();
}
/**
* Gets the port.
* @return the port
*/
public int getPort() {
return encapsulatedChannel.getPort();
}
/**
* Gets the SIP stack context.
* @return the SIP Stack
*/
public SIPMessageStack getSIPStack() {
return parentStack;
}
/**
* Gets the remote address.
* @return the remote address
*/
public String getPeerAddress() {
return encapsulatedChannel.getPeerAddress();
}
/**
* Gets the remote port number.
* @return the remote port number
*/
public int getPeerPort() {
return encapsulatedChannel.getPeerPort();
}
/**
* Gets the connection transport.
* @return the connection transport
*/
public String getTransport() {
return encapsulatedChannel.getTransport();
}
/**
* Checks if the connection is reliable
* @return true if channel is on a stream connection
*/
public boolean isReliable() {
return encapsulatedChannel.isReliable();
}
/**
* Returns the Via header for this channel. Gets the Via header of the
* underlying message channel, and adds a branch parameter to it for this
* transaction.
* @return the via header
*/
public ViaHeader getViaHeader() {
// Via header of the encapulated channel
ViaHeader channelViaHeader;
// Add the branch parameter to the underlying
// channel's Via header
channelViaHeader = super.getViaHeader();
channelViaHeader.setBranch(branch);
return channelViaHeader;
}
/**
* Process an exception.
* @param ex the exception to handle
*/
public void handleException(SIPServerException ex) {
encapsulatedChannel.handleException(ex);
}
/**
* Processes the message through the transaction and sends it to the SIP
* peer.
* @param messageToHeaderSend Message to send to the SIP peer.
*/
abstract public void sendMessage(Message messageToHeaderSend)
throws IOException;
/**
* Parses the byte array as a message, process it through the
* transaction, and send it to the SIP peer.
*
* @param messageBytes Bytes of the message to send.
* @param receiverAddress Address of the target peer.
* @param receiverPort Network port of the target peer.
*
* @throws IOException If there is an error parsing
* the byte array into an object.
*/
protected void sendMessage(
byte[] messageBytes,
String receiverAddress,
int receiverPort)
throws IOException {
// Object representation of the SIP message
Message messageToHeaderSend;
try {
StringMsgParser messageParser = new StringMsgParser();
messageToHeaderSend =
messageParser.parseSIPMessage(messageBytes);
sendMessage(messageToHeaderSend);
} catch (ParseException e) {
throw new IOException(e.getMessage());
}
}
/**
* Adds a new event listener to this transaction.
* @param newListener Listener to add.
*/
public void addEventListener(
SIPTransactionEventListener newListener) {
eventListeners.addElement(newListener);
}
/**
* Removes an event listener from this transaction.
* @param oldListener Listener to remove.
*/
public void removeEventListener(
SIPTransactionEventListener oldListener) {
eventListeners.removeElement(oldListener);
}
/**
* Creates a SIPTransactionErrorEvent and sends it
* to all of the listeners of this transaction.
* This method also flags the transaction as
* terminated.
* @param errorEventID ID of the error to raise.
*/
protected void raiseErrorEvent(
int errorEventID) {
// Error event to send to all listeners
SIPTransactionErrorEvent newErrorEvent;
// Iterator through the list of listeners
Enumeration listenerIterator;
// Next listener in the list
SIPTransactionEventListener nextListener;
// Create the error event
newErrorEvent = new SIPTransactionErrorEvent(this,
errorEventID);
// Loop through all listeners of this transaction
synchronized (eventListeners) {
listenerIterator = eventListeners.elements();
while (listenerIterator.hasMoreElements()) {
// Send the event to the next listener
nextListener = (SIPTransactionEventListener)
listenerIterator.nextElement();
nextListener.transactionErrorEvent
(newErrorEvent);
}
}
// Clear the event listeners after propagating the error.
eventListeners.removeAllElements();
// Errors always terminate a transaction
setState(TERMINATED_STATE);
if (this instanceof ServerTransaction &&
this.isByeTransaction() && this.dialog != null)
this.dialog.setState(Dialog.TERMINATED_STATE);
}
/**
* A shortcut way of telling if we are a server transaction.
* @return true if this is a servertransaction
*/
protected boolean IsServerTransaction() {
return this instanceof ServerTransaction;
}
/**
* Gets the dialog object of this Transaction object. This object
* returns null if no dialog exists. A dialog only exists for a
* transaction when a session is setup between a User Agent Client and a
* User Agent Server, either by a 1xx Provisional Response for an early
* dialog or a 200OK Response for a committed dialog.
* @return the Dialog Object of this Transaction object.
* @see Dialog
*/
public Dialog getDialog() {
return dialog;
}
/**
* Sets the dialog object.
* @param newDialog the dialog to set.
*/
public void setDialog(Dialog newDialog) {
dialog = newDialog;
}
/**
* Returns the current value of the retransmit timer in
* milliseconds used to retransmit messages over unreliable transports.
* @return the integer value of the retransmit timer in milliseconds.
*/
public int getRetransmitTimer() {
return SIPTransactionStack.BASE_TIMER_INTERVAL;
}
/**
* Gets the host to assign for an outgoing Request via header.
* @return the via host
*/
public String getViaHost() {
return getViaHeader().getHost();
}
/**
* Gets the last response.
* @return the last response
*/
public Response getLastResponse() { return this.lastResponse; }
/**
* Gets the transaction Id.
* @return the transaction id
*/
public String getTransactionId() {
return getOriginalRequest().getTransactionId();
}
/**
* Gets the port to assign for the via header of an outgoing message.
* @return the via port number
*/
public int getViaPort() {
return getViaHeader().getPort();
}
/**
* A method that can be used to test if an incoming request
* belongs to this transction. This does not take the transaction
* state into account when doing the check otherwise it is identical
* to isMessagePartOfTransaction. This is useful for checking if
* a CANCEL belongs to this transaction.
* @param requestToHeaderTest is the request to test.
* @return true if the the request belongs to the transaction.
*/
public boolean doesCancelMatchTransaction(
Request requestToHeaderTest) {
// List of Via headers in the message to test
ViaList viaHeaders;
// ToHeaderpmost Via header in the list
ViaHeader topViaHeader;
// Branch code in the topmost Via header
String messageBranch;
// Flags whether the select message is part of this transaction
boolean transactionMatches;
transactionMatches = false;
if (getOriginalRequest() == null ||
getOriginalRequest().getMethod().equals(Request.CANCEL)) {
return false;
}
// Get the topmost Via header and its branch parameter
viaHeaders = requestToHeaderTest.getViaHeaders();
if (viaHeaders != null) {
topViaHeader = (ViaHeader)viaHeaders.getFirst();
messageBranch = topViaHeader.getBranch();
if (messageBranch != null) {
// If the branch parameter exists but
// does not start with the magic cookie,
if (!messageBranch.toUpperCase().startsWith(SIPConstants.
GENERAL_BRANCH_MAGIC_COOKIE.toUpperCase())) {
// Flags this as old
// (RFC2543-compatible) client
// version
messageBranch = null;
}
}
// If a new branch parameter exists,
if (messageBranch != null &&
this.getBranch() != null) {
// If the branch equals the branch in
// this message,
if (getBranch().equals(messageBranch)
&& topViaHeader.getSentBy().
equals(((ViaHeader)getOriginalRequest().
getViaHeaders().getFirst()).
getSentBy())) {
transactionMatches = true;
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION,
LogChannels.LC_JSR180, "returning true");
}
}
} else {
// If this is an RFC2543-compliant message,
// If RequestURI, ToHeader tag, FromHeader tag,
// CallIdHeader, CSeqHeader number, and top Via
// headers are the same,
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"testing against " + getOriginalRequest());
}
if (
getOriginalRequest().getRequestURI().equals
(requestToHeaderTest.getRequestURI()) &&
getOriginalRequest().getTo().equals
(requestToHeaderTest.getTo()) &&
getOriginalRequest().getFromHeader().equals
(requestToHeaderTest.getFromHeader()) &&
getOriginalRequest().getCallId().
getCallId().equals
(requestToHeaderTest.getCallId() .getCallId()) &&
getOriginalRequest().
getCSeqHeader().getSequenceNumber() ==
requestToHeaderTest.getCSeqHeader().
getSequenceNumber() &&
topViaHeader.equals
(getOriginalRequest().
getViaHeaders().getFirst())) {
transactionMatches = true;
}
}
}
return transactionMatches;
}
/**
* Checks if transaction has been sent to the listener.
* @return true if transaction has been sent
*/
public boolean passToListener() {
return toListener;
}
/**
* Closes the encapsulated channel.
*/
public void close() {
encapsulatedChannel.close();
}
/**
* Check if this connection is secure.
* @return true if this is a secure channel
*/
public boolean isSecure() {
return encapsulatedChannel.isSecure();
}
/**
* Gets the message processor handling this transaction.
* @return the mesage processor for this transaction
*/
public MessageProcessor getMessageProcessor() {
return encapsulatedChannel.getMessageProcessor();
}
/**
* Sets the ACK has been seen flag.
*/
public void setAckSeen() {
ackSeenFlag = true;
}
/**
* Checks if the ACK has been seen flag is set.
* @return true if the ACK has been seen
*/
public boolean isAckSeen() {
return ackSeenFlag;
}
/**
* Create route set for request.
*
* @param request the input request
* @throws SipException if any occurs
*/
protected void buildRouteSet(Request request) throws SipException {
Message origMsg = lastResponse;
boolean isServer = false;
Dialog dlg = getDialog();
if (dlg != null) { // inside of dialog
Transaction firstTransaction = dlg.getFirstTransaction();
if (dlg.isServer()) { // dialog was created on server side
origMsg = firstTransaction.getOriginalRequest();
isServer = true;
} else { // dialog was created on client side
origMsg = firstTransaction.getLastResponse();
}
} else { // out of dialog
// IMPL_NOTE: implement a support for the case when Contact
// header is not present.
if (this instanceof ServerTransaction) {
origMsg = getOriginalRequest();
isServer = true;
}
}
if (origMsg == null) {
// Avoid NPE in case when sending ACK from the part that
// initiated a Re-INVITE.
return;
}
RecordRouteList recordRouteList = origMsg.getRecordRouteHeaders();
ContactList cl = origMsg.getContactHeaders();
if (cl == null) {
// Prevent NPE and log an error: Contact header is absent.
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Transaction.buildRouteSet(): Contact must be present!");
}
return;
}
ContactHeader contact = (ContactHeader)cl.getFirst();
if (recordRouteList == null) {
URI remoteTarget = contact.getAddress().getURI();
// RFC 3261, 12.2.1.1 Generating the Request
// If the route set is empty, the UAC MUST place the remote
// target URI into the Request-URI. The UAC MUST NOT add
// a Route header field to the request.
request.setRequestURI(remoteTarget);
} else {
request.removeHeader(Header.ROUTE);
RouteList routeList = new RouteList();
// start at the end of the list and walk backwards
Vector li = recordRouteList.getHeaders();
int recSize = li.size();
for (int i = 0; i < recSize; i++) {
int j = i;
if (!isServer) { // on client side the order is reversed
j = recSize - i - 1;
}
RecordRouteHeader rr = (RecordRouteHeader) li.elementAt(j);
Address addr = rr.getAddress();
RouteHeader route = new RouteHeader();
route.setAddress((Address)rr.getAddress().clone());
route.setParameters((NameValueList)rr.getParameters().clone());
routeList.add(route);
}
RouteHeader firstRoute = (RouteHeader) routeList.getFirst();
if (!((SipURI)firstRoute.getAddress().getURI()).hasLrParam()) {
RouteHeader route = new RouteHeader();
route.setAddress((Address)contact.getAddress().clone());
routeList.removeFirst();
// IMPL_NOTE: is clone() need?
URI uri = firstRoute.getAddress().getURI();
request.setRequestURI(uri);
routeList.add(route);
request.addHeader(routeList);
} else {
URI uri = (URI) contact.getAddress().getURI().clone();
request.setRequestURI(uri);
request.addHeader(routeList);
}
}
}
}
|