/*
* Created on Jul 19, 2005
*
* Copyright 2005 CafeSip.org
*
* 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 org.cafesip.jiplet.sip;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Vector;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipProvider;
import javax.sip.TimeoutEvent;
import javax.sip.Transaction;
import javax.sip.TransactionAlreadyExistsException;
import javax.sip.TransactionState;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.ContactHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.RouteHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import org.cafesip.jiplet.Jiplet;
import org.cafesip.jiplet.JipletDialog;
import org.cafesip.jiplet.JipletException;
import org.cafesip.jiplet.JipletLogger;
/**
* This class enables SIP-related operations from within a jiplet class. It has
* methods to proxy requests, proxy responses and handle proxy timeouts. In
* addition, it can cancel proxy requests and other operations.
*
* @author Amit Chatterjee (copied heavily from JAIN-SIP proxy code)
*
*/
public class SipCommunicator
{
private Jiplet jiplet;
private boolean reset = true;
ServerTransaction serverTransaction;
private RequestEvent request;
private ResponseEvent response;
private TimeoutEvent timeout;
private boolean addRecordRoute;
private boolean stateful;
private boolean presenceServer;
/**
* A constructor for this class.
*
*/
public SipCommunicator(Jiplet jiplet, RequestEvent request)
{
this.jiplet = jiplet;
this.request = request;
initStackInfo(jiplet);
}
public SipCommunicator(Jiplet jiplet, ResponseEvent response)
{
this.jiplet = jiplet;
this.response = response;
initStackInfo(jiplet);
}
public SipCommunicator(Jiplet jiplet, TimeoutEvent timeout)
{
this.jiplet = jiplet;
this.timeout = timeout;
initStackInfo(jiplet);
}
private void initStackInfo(Jiplet jiplet)
{
}
/**
* Cancels a proxy request. For this to work, the proxy object must be
* obtained by calling the method requestEvent.getProxy() from the
* processRequest() method of the jiplet class (or a class that extends
* jiplet). If stateless, this method does nothing. Otherwise, it cancels the
* previous operation by sending a CANCEL request to all the dialogs (call
* legs). If the parameter statusCode is greater than 0, it also sends a
* response with the provided status code to the leg that sent the original
* request.
*
* @param statusCode
* A value greater than zero will result in a response to the
* user agent that started the dialog. If this is not desired,
* use -1 as this parameter.
* @param reason
* A reason phrase along with the status code. If null, the
* reason phrase is not added.
*
*/
public void cancelRequest(int statusCode, String reason)
{
reset = true;
if (stateful == false)
{
// we do not need to do anything for stateless proxying
return;
}
if (request == null)
{
// we only process the cancel for a proxied request
JipletLogger
.warn("This proxy object was not obtained by calling the"
+ " method requestEvent.getProxy() from the processRequest() method of the jiplet class."
+ " Therefore this proxy object cannot perform the cancel operation");
return;
}
Dialog dialog = serverTransaction.getDialog();
if (dialog == null)
{
JipletLogger
.warn("Could not obtain the SIP dialog while canceling a proxy."
+ " Cannot cancel the proxy");
return;
}
JipletDialog jdialog = jiplet.getDialog(serverTransaction.getDialog(),
false);
if (jdialog == null)
{
JipletLogger
.warn("Could not obtain the SIP dialog while canceling a proxy."
+ " Cannot cancel the proxy");
return;
}
TransactionsMapping transactionsMapping = jdialog
.getTransactionsMapping();
if (transactionsMapping == null)
{
JipletLogger
.warn("Could not obtain the jiplet dialog mapping while canceling a proxy."
+ " This should not happen");
return;
}
Transaction t = (Transaction) jdialog.getAttribute("firstTransaction");
if (t == null)
{
JipletLogger
.warn("Could not find the first transaction while canceling a proxy."
+ " Cannot cancel the proxy");
return;
}
if (t instanceof ClientTransaction)
{
JipletLogger
.warn("Found the first transaction in the dialog to be a client transaction while canceling a proxy."
+ " Cannot cancel the proxy");
return;
}
ServerTransaction firstServerTransaction = (ServerTransaction) t;
Vector transactions = transactionsMapping
.getClientTransactions(firstServerTransaction);
if (transactions == null || transactions.isEmpty())
{
// this is an error condition and should not happen
JipletLogger
.error("While canceling a proxy request, found empty client transaction in the transaction map for the dialog. ");
return;
}
for (Enumeration en = transactions.elements(); en.hasMoreElements();)
{
ClientTransaction ct = (ClientTransaction) en.nextElement();
// check if the client transaction can be canceled.
if (ct.getState().equals(TransactionState.COMPLETED)
|| ct.getState().equals(TransactionState.TERMINATED))
{
continue;
}
try
{
JipletDialog clientJipletDialog = (JipletDialog) ct.getDialog()
.getApplicationData();
if (clientJipletDialog != null)
{
ClientTransaction client = clientJipletDialog
.getSipProvider().getNewClientTransaction(
ct.createCancel());
client.sendRequest();
}
else
{
ListeningPoint defaultLP = jiplet
.getListeningPointDefault();
ClientTransaction client = jiplet.getSipProvider(defaultLP)
.getNewClientTransaction(ct.createCancel());
client.sendRequest();
}
}
catch (Exception e)
{
jiplet.error("Couldn't cancel client transaction : Exception "
+ e.getClass().getName() + " : " + e.getMessage()
+ "\n" + JipletLogger.getStackTrace(e));
}
}
transactions.clear();
try
{
if (statusCode >= 0)
{
if (firstServerTransaction.getState().equals(
TransactionState.COMPLETED)
|| firstServerTransaction.getState().equals(
TransactionState.TERMINATED))
{
// nothing to do
return;
}
Response response = jiplet.getMessageFactory().createResponse(
statusCode, firstServerTransaction.getRequest());
if (reason != null)
{
response.setReasonPhrase(reason);
}
firstServerTransaction.sendResponse(response);
}
}
catch (Exception e)
{
jiplet.error("Exception " + e.getClass().getName() + " : "
+ e.getMessage() + "\n" + JipletLogger.getStackTrace(e));
return;
}
}
/**
* This method is similar to the cancelRequest(int statusCode) method except
* that no response is sent to the originating request.
*/
public void cancelRequest()
{
cancelRequest(-1, null);
}
/**
* Call this method to drop an outbound leg for the case where other
* outbound legs may be present and the event on this leg shouldn't drop
* everything.
*
* @param clientTransaction for this leg
* @return true if this leg was dropped, false otherwise
*/
public boolean dropOutboundLeg(ClientTransaction clientTransaction)
{
if (clientTransaction == null || clientTransaction.getDialog() == null)
{
return false;
}
JipletDialog jd = jiplet
.getDialog(clientTransaction.getDialog(), false);
if (jd == null)
{
return false;
}
TransactionsMapping transactionsMapping = jd.getTransactionsMapping();
ServerTransaction serverTransaction = transactionsMapping
.getServerTransaction(clientTransaction);
if (serverTransaction != null)
{
// if other outbound legs present, simply drop this leg out
Vector clientsTransactionList = transactionsMapping
.getClientTransactions(serverTransaction);
if (clientsTransactionList != null
&& clientsTransactionList.size() > 1)
{
transactionsMapping.removeMapping(clientTransaction);
return true;
}
}
return false;
}
/**
* This method is used to proxy a received SIP request message.
*
* @param uris
* list of URIs to proxy a SIP request message to. For ACK, CANCEL
* and BYE messages for a stateful proxy, the contact list can be
* an empty array list. The proxy will automatically proxy the
* request based on the dialog
* @param addRecordRoute
* true if record route is to be added.
* @param true
* if stateful proxy is required.
* @param true
* if the proxy server is a presence server
* @throws SipException
* @throws ParseException
* @throws JipletException
* @throws InvalidArgumentException
*
*/
public void proxyRequest(ArrayList uris, boolean addRecordRoute,
boolean stateful, boolean presenceServer)
throws InvalidArgumentException, JipletException, ParseException,
SipException
{
try
{
if (request == null)
{
throw new JipletException(
"The request cannot be proxied from a non-request event");
}
this.addRecordRoute = addRecordRoute;
this.stateful = stateful;
this.presenceServer = presenceServer;
SipProvider provider = (SipProvider) request.getSource();
Request msg = request.getRequest();
if (serverTransaction == null)
{
if (jiplet.isDebugEnabled() == true)
{
jiplet
.debug("The server transaction is being obtained first time for this proxy object");
}
serverTransaction = request.getServerTransaction();
}
else
{
if (jiplet.isDebugEnabled() == true)
{
jiplet
.debug("The server transaction already exists. This proxy object is being re-used to send another request");
}
}
// PROXY BEHAVIOR
/*
* RFC 3261: 16.2: For all new requests, including any with unknown
* methods, an element intending to proxy the request MUST:
*
* 1. Validate the request (Section 16.3)
*
* 2. Preprocess routing information (Section 16.4)
*
* 3. Determine target(s) for the request (Section 16.5)
*
* 4. Forward the request to each target (Section 16.6)
*
* 5. Process all responses (Section 16.7)
*/
// 1. Validate the request (Section 16.3)
RequestValidation valid = new RequestValidation(jiplet, presenceServer);
if (valid.validateRequest(provider, request.getRequest(),
serverTransaction) == false)
{
jiplet
.warn("A request message that is to be proxied failed validation."
+ " Not going to proxy it. An appropriate response has been sent");
return;
}
// Let's check if the ACK is for the proxy: if there is no Route
// header: it is mandatory for the ACK to be forwarded
if (msg.getMethod().equals(Request.ACK))
{
ListIterator routes = msg.getHeaders(RouteHeader.NAME);
if (routes == null || !routes.hasNext())
{
if (jiplet.isDebugEnabled() == true)
{
jiplet.debug("Proxying an ACK "
+ " targeted for the proxy, we ignore it");
}
return;
}
}
if (stateful == true)
{
String method = msg.getMethod();
// Methods that creates dialogs, so that can
// generate transactions
if (method.equals(Request.INVITE)
|| method.equals(Request.SUBSCRIBE))
{
try
{
if (serverTransaction == null)
{
serverTransaction = provider
.getNewServerTransaction(msg);
}
TransactionsMapping transactionsMapping = jiplet
.getDialog(serverTransaction.getDialog(), true)
.getTransactionsMapping();
if (transactionsMapping == null)
{
transactionsMapping = new TransactionsMapping(
jiplet, serverTransaction);
// save server transaction side SipProvider in JipletDialog
Dialog serverDialog = serverTransaction.getDialog();
JipletDialog jd = jiplet.getDialog(serverDialog, true);
jd.setSipProvider((SipProvider) request.getSource());
}
}
catch (TransactionAlreadyExistsException e)
{
if (jiplet.isDebugEnabled() == true)
{
jiplet.debug("The request message to be proxied"
+ " is a retransmission, we drop it!");
}
}
}
}
// 2. Preprocess routing information (Section 16.4)
/*
* The proxy MUST inspect the Request-URI of the request. If the
* Request-URI of the request contains a value this proxy previously
* placed into a Record-Route header field (see Section 16.6 item
* 4), the proxy MUST replace the Request-URI in the request with
* the last value from the Route header field, and remove that value
* from the Route header field. The proxy MUST then proceed as if it
* received this modified request. ..... (idem to below:) 16.12. The
* proxy will inspect the URI in the topmost Route header field
* value. If it indicates this proxy, the proxy removes it from the
* Route header field (this route node has been reached).
*/
ListIterator routes = msg.getHeaders(RouteHeader.NAME);
if (routes != null)
{
if (routes.hasNext())
{
RouteHeader routeHeader = (RouteHeader) routes.next();
Address routeAddress = routeHeader.getAddress();
SipURI routeSipURI = (SipURI) routeAddress.getURI();
String h = routeSipURI.getHost();
int port = routeSipURI.getPort();
if (jiplet.hasAddress(h, port) == true)
{
if (jiplet.isDebugEnabled() == true)
{
jiplet
.debug("A request message to be proxied has this proxy in the route header. "
+ " We are going to remove the first route from "
+ " the RouteHeader");
}
routes.remove();
}
}
}
// 3. Determine target(s) for the request (Section 16.5)
/*
* The set of targets will either be predetermined by the contents
* of the request or will be obtained from an abstract location
* service. Each target in the set is represented as a URI.
*/
/*
* If the Request-URI of the request contains an maddr parameter,
* the Request-URI MUST be placed into the target set as the only
* target URI, and the proxy MUST proceed to Section 16.6.
*/
URI requestURI = msg.getRequestURI();
if (requestURI.isSipURI())
{
SipURI requestSipURI = (SipURI) requestURI;
if (requestSipURI.getMAddrParam() != null)
{
uris.clear();
uris.add(requestURI);
if (jiplet.isDebugEnabled() == true)
jiplet
.debug("While proxying a request, "
+ " the only target is the Request-URI (mAddr parameter)");
// 4. Forward the request
RequestForwarding forwarder = new RequestForwarding(jiplet,
this, request,
serverTransaction, stateful, addRecordRoute);
forwarder.forwardRequest(uris);
return;
}
}
if (stateful == true)
{
// Forward to next hop but dont reply OK right away for the
// BYE. Bye is end-to-end not hop by hop!
if (msg.getMethod().equals(Request.BYE))
{
if (serverTransaction == null)
{
if (jiplet.isDebugEnabled() == true)
jiplet
.debug("While proxying a request, null server transaction for BYE");
return;
}
Dialog d = serverTransaction.getDialog();
TransactionsMapping transactionsMapping = jiplet.getDialog(
d, true).getTransactionsMapping();
Dialog peerDialog = transactionsMapping
.getPeerDialog(serverTransaction);
Request clonedRequest = (Request) msg.clone();
FromHeader from = (FromHeader) clonedRequest
.getHeader(FromHeader.NAME);
from.removeParameter("tag");
ToHeader to = (ToHeader) clonedRequest
.getHeader(ToHeader.NAME);
to.removeParameter("tag");
if (peerDialog.getState() != null)
{
JipletDialog clientDialog = jiplet.getDialog(peerDialog, false);
SipProvider clientProvider = clientDialog.getSipProvider();
ListeningPoint lp = clientProvider.getListeningPoints()[0];
// TODO, need to save the right transport - save LP instead of SipProvider?
// (in JipletDialog)
ViaHeader via = jiplet.getHeaderFactory().createViaHeader(lp.getIPAddress(),
lp.getPort(), lp.getTransport(), null);
clonedRequest.addHeader(via);
ClientTransaction newct = clientProvider
.getNewClientTransaction(clonedRequest);
transactionsMapping
.addMapping(serverTransaction, newct);
peerDialog.sendRequest(newct);
jiplet.registerForResponse(clonedRequest, 60000L);
return;
}
else
{
// the peer dialog is not yet established so bail out.
// this is a client error - client is sending BYE
// before dialog establishment.
jiplet
.warn("While proxying a SIP request, bad dialog state - BYE dropped");
return;
}
}
// NEW CODE added by Amit to handle ACK and CANCEL
else if (msg.getMethod().equals(Request.ACK))
{
if (serverTransaction == null)
{
if (jiplet.isDebugEnabled() == true)
jiplet
.debug("While proxying an ACK request, null server transaction");
return;
}
Dialog d = serverTransaction.getDialog();
TransactionsMapping transactionsMapping = jiplet.getDialog(
d, true).getTransactionsMapping();
Dialog peerDialog = transactionsMapping
.getPeerDialog(serverTransaction);
Request clonedRequest = (Request) msg.clone();
if (peerDialog.getState() != null)
{
peerDialog.sendAck(clonedRequest);
}
return;
}
else if (msg.getMethod().equals(Request.CANCEL))
{
if (serverTransaction == null)
{
if (jiplet.isDebugEnabled() == true)
jiplet
.debug("While proxying a CANCEL request, null server transaction for BYE");
return;
}
Dialog d = serverTransaction.getDialog();
JipletDialog jd = jiplet.getDialog(d, true);
TransactionsMapping transactionsMapping = jd
.getTransactionsMapping();
Vector transactions = transactionsMapping
.getClientTransactions((ServerTransaction) jd
.getAttribute("firstTransaction"));
if (transactions == null || transactions.isEmpty())
{
return;
}
for (Enumeration en = transactions.elements(); en
.hasMoreElements();)
{
ClientTransaction ct = (ClientTransaction) en
.nextElement();
// check if the client transaction can be canceled.
if (ct.getState().equals(TransactionState.COMPLETED)
|| ct.getState().equals(
TransactionState.TERMINATED))
{
continue;
}
JipletDialog clientJipletDialog = (JipletDialog) ct.getDialog().getApplicationData();
if (clientJipletDialog != null)
{
ClientTransaction client = clientJipletDialog.getSipProvider().getNewClientTransaction(ct.createCancel());
client.sendRequest();
}
}
}
// END NEW CODE
}
/*
* If the target set for the request has not been predetermined as
* described above, this implies that the element is responsible for
* the domain in the Request-URI, and the element MAY use whatever
* mechanism it desires to determine where to send the request. ...
* When accessing the location service constructed by a registrar,
* the Request-URI MUST first be canonicalized as described in
* Section 10.3 before being used as an index.
*/
if (requestURI.isSipURI())
{
SipURI requestSipURI = (SipURI) requestURI;
Iterator iterator = requestSipURI.getParameterNames();
if (jiplet.isDebugEnabled() == true)
jiplet.debug("While proxying a request, we canonicalized"
+ " the request-URI");
while (iterator != null && iterator.hasNext())
{
String name = (String) iterator.next();
requestSipURI.removeParameter(name);
}
}
// We fork only INVITE
if (uris.size() > 1 && !msg.getMethod().equals(Request.INVITE))
{
if (jiplet.isDebugEnabled() == true)
jiplet.debug("While proxying a request, the request "
+ " to fork is not an INVITE, so we will process"
+ " it with the first target as the only target.");
URI uri = (URI) uris.get(0);
uris.clear();
uris.add(uri);
// 4. Forward the request to the target:
RequestForwarding forwarder = new RequestForwarding(jiplet,
this, request,
serverTransaction, stateful, addRecordRoute);
forwarder.forwardRequest(uris);
return;
}
if (!uris.isEmpty())
{
if (jiplet.isDebugEnabled() == true)
jiplet.debug("While proxying a request, the target set"
+ " is the set of the contacts URI from the "
+ " location service");
// 4. Forward the request statefully to each target Section
// 16.6.:
RequestForwarding forwarder = new RequestForwarding(jiplet,
this, request,
serverTransaction, stateful, addRecordRoute);
forwarder.forwardRequest(uris);
return;
}
/*
* If the target set remains empty after applying all of the above,
* the proxy MUST return an error response, which SHOULD be the 480
* (Temporarily Unavailable) response.
*/
Response response = jiplet.getMessageFactory().createResponse(
Response.TEMPORARILY_UNAVAILABLE, msg);
if (serverTransaction != null)
serverTransaction.sendResponse(response);
else
provider.sendResponse(response);
if (jiplet.isDebugEnabled() == true)
jiplet
.debug("While proxying a request, unable to set "
+ " the targets, 480 (Temporarily Unavailable) replied");
}
finally
{
reset = false;
}
}
public ViaHeader getStackViaHeader() throws ParseException,
InvalidArgumentException
{
ListeningPoint lp = jiplet.getListeningPointDefault();
return jiplet.getHeaderFactory().createViaHeader(lp.getIPAddress(),
lp.getPort(), lp.getTransport(), null);
}
public ContactHeader getStackContactHeader() throws ParseException
{
ListeningPoint lp = jiplet.getListeningPointDefault();
SipURI sipURI = jiplet.getAddressFactory().createSipURI(null, lp.getIPAddress());
sipURI.setPort(lp.getPort());
sipURI.setTransportParam(lp.getTransport());
Address contactAddress = jiplet.getAddressFactory().createAddress(
sipURI);
return jiplet.getHeaderFactory().createContactHeader(contactAddress);
}
/**
* This method is used to proxy a received SIP request message. This method
* is similar to the other proxyRequest method except that it proxies the
* request to a single location.
*/
public void proxyRequest(URI uri, boolean addRecordRoute, boolean stateful,
boolean presenceServer) throws InvalidArgumentException,
JipletException, ParseException, SipException
{
ArrayList list = new ArrayList();
list.add(uri);
proxyRequest(list, addRecordRoute, stateful, presenceServer);
}
/**
* This method is used to proxy the received SIP response message.
*
* @param true
* if the proxy server is a presence server
* @throws JipletException
* @throws ParseException
* @throws SipException
*/
public void proxyResponse(boolean presenceServer) throws JipletException,
SipException, ParseException
{
if (response == null)
{
throw new JipletException(
"The response cannot be proxied from a non-response event");
}
ResponseForwarding forwarder = new ResponseForwarding(jiplet,
(SipProvider) response.getSource(), presenceServer);
forwarder.forwardResponse(response.getResponse(), response
.getClientTransaction());
}
/**
* This method is used to handle proxy processing when a SIP timeout-occurs.
* The jiplet is notified of timeout by the container by invoking the
* processTimeout() method. The jiplet should call this method for handling
* the timeout-related to proxy forwarding.
*
* @param stateful
* true if the proxy is stateful
* @throws ParseException
* @throws SipException
* @throws JipletException
*/
public void handleProxyTimeout(boolean stateful) throws ParseException,
SipException, JipletException
{
if (timeout == null)
{
throw new JipletException(
"The timout cannot be handled from a non-timeout event");
}
if (stateful == false)
{
// TODO this needs to be looked into in some details. For now, just
// return.
return;
}
SipProvider provider = (SipProvider) timeout.getSource();
TransactionsMapping transactionsMapping = null;
if (timeout.isServerTransaction())
{
ServerTransaction serverTransaction = timeout
.getServerTransaction();
Dialog dialog = serverTransaction.getDialog();
if (dialog != null)
{
transactionsMapping = jiplet.getDialog(dialog, true)
.getTransactionsMapping();
transactionsMapping.removeMapping(serverTransaction);
}
}
else
{
ClientTransaction clientTransaction = timeout
.getClientTransaction();
Dialog dialog = clientTransaction.getDialog();
ServerTransaction st = null;
if (dialog != null)
{
transactionsMapping = jiplet.getDialog(dialog, true)
.getTransactionsMapping();
if (transactionsMapping != null)
{
st = transactionsMapping
.getServerTransaction(clientTransaction);
}
if (st == null)
{
jiplet.error("Unable to retrieve the server transaction,"
+ " cannot process timeout!");
return;
}
}
else
{
jiplet.error("Unable to retrieve the transaction Mapping,"
+ " cannot process timeout!");
return;
}
Request request = st.getRequest();
// This removes the given mapping from the table but not
// necessarily the whole thing.
transactionsMapping.removeMapping(clientTransaction);
if (!transactionsMapping.hasMapping(st))
{
// No more mappings left in the transaction table.
Response response = jiplet.getMessageFactory().createResponse(
Response.REQUEST_TIMEOUT, request);
try
{
st.sendResponse(response);
}
catch (InvalidArgumentException e)
{
// inapplicable - it only happens if the Response was created
// by Dialog.createReliableProvisionalResponse(int) and the
// application calls ServerTransaction.sendResponse() to send it
JipletLogger
.error("Timeout response sending failed - invalid send method used. Failed response = \n"
+ response.toString());
}
}
}
}
/**
* @return the server transaction. Must be called after a proxyRequest() or
* a proxyResponse() has been called.
*/
public ServerTransaction getServerTransaction()
{
return serverTransaction;
}
protected boolean isReset()
{
return reset;
}
protected void setReset(boolean reset)
{
this.reset = reset;
}
/**
* @return returns the request event.
*/
public RequestEvent getRequest()
{
return request;
}
/**
* @return returns the response event
*/
public ResponseEvent getResponse()
{
return response;
}
/**
* @return the timeout event
*/
public TimeoutEvent getTimeout()
{
return timeout;
}
}