/* * File Name : TransactionHandle.java * * The JAIN MGCP API implementaion. * * The source code contained in this file is in in the public domain. * It can be used in any project or product without prior permission, * license or royalty payments. There is NO WARRANTY OF ANY KIND, * EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, * THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, * AND DATA ACCURACY. We do 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. */ package org.mobicents.mgcp.stack; import jain.protocol.ip.mgcp.JainMgcpCommandEvent; import jain.protocol.ip.mgcp.JainMgcpResponseEvent; import jain.protocol.ip.mgcp.message.Constants; import jain.protocol.ip.mgcp.message.Notify; import jain.protocol.ip.mgcp.message.parms.EndpointIdentifier; import jain.protocol.ip.mgcp.message.parms.NotifiedEntity; import jain.protocol.ip.mgcp.message.parms.ReturnCode; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.ParseException; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Timer; import java.util.TimerTask; import org.apache.log4j.Logger; import org.mobicents.mgcp.stack.handlers.TransactionHandlerManagement; import org.mobicents.mgcp.stack.parser.UtilsFactory; /** * Implements the base gateway control interface. * * The MGCP implements the media gateway control interface as a set of * transactions. The transactions are composed of a command and a mandatory * response. There are eight types of command: * * <li> CreateConnection ModifyConnection DeleteConnection NotificationRequest * Notify AuditEndpoint AuditConnection RestartInProgress </li> * * The first four commands are sent by the Call Agent to a gateway. The Notify * command is sent by the gateway to the Call Agent. The gateway may also send a * DeleteConnection. The Call Agent may send either of the Audit commands to the * gateway. The Gateway may send a RestartInProgress command to the Call Agent. * * All commands are composed of a Command header, optionally followed by a * session description. * * All responses are composed of a Response header, optionally followed by a * session description. * * Headers and session descriptions are encoded as a set of text lines, * separated by a line feed character. The headers are separated from the * session description by an empty line. * * MGCP uses a transaction identifier to correlate commands and responses. The * transaction identifier is encoded as a component of the command header and * repeated as a component of the response header. * * Transaction identifiers have values between 1 and 999999999. An MGCP entity * shall not reuse a transaction identifier sooner than 3 minutes after * completion of the previous command in which the identifier was used. * * @author Oleg Kulikov * @author Pavel Mitrenko * @author Amit Bhayani */ public abstract class TransactionHandler implements Runnable, TransactionHandlerManagement { /** Logger instance */ private static final Logger logger = Logger.getLogger(TransactionHandler.class); private static int GENERATOR = 1; public static final String NEW_LINE = "\n"; public static final String SINGLE_CHAR_SPACE = " "; public static final String MGCP_VERSION = " MGCP 1.0"; // let the single // char space prefix // the version public final static int LONGTRAN_TIMER_TIMEOUT = 5000; // 5secs public static final int THIST_TIMER_TIMEOUT = 30000; // 30 sec /** Is this a transaction on a command sent or received? */ protected boolean sent; /** Transaction handle sent from application to the MGCP provider. */ protected int remoteTID; /** Transaction handle sent from MGCP provider to MGCP listener */ private int localTID; protected JainMgcpStackImpl stack; /** Holds the address from wich request was originaly received by provider */ private InetAddress remoteAddress; /** * Holds the port number from wich request was originaly received by * provider */ private int remotePort; /** Used to hold parsed command event */ protected JainMgcpCommandEvent commandEvent; /** Used to hold parsed response event * */ protected JainMgcpResponseEvent responseEvent; /** Expiration timer */ protected static Timer transactionHandlerTimer = new Timer("TransactionHandlerTimer"); private LongtranTimerTask longtranTimerTask; /** Flag to check if this is Command or Response event * */ private boolean isCommand = false; private ReTransmissionTimerTask reTransmissionTimer; private THISTTimerTask tHISTTimerTask; private int A = 0; private int D = 2; private int N = 2; private DatagramPacket sendComandDatagram = null; private int countOfCommandRetransmitted = 0; protected UtilsFactory utilsFactory = null; protected EndpointHandler endpointHandler = null; protected boolean retransmision; protected Object source = null; private String msgTemp = null; protected LinkedList<ActionPerform> actionToPerform = new LinkedList<ActionPerform>(); protected EndpointIdentifier endpoint = null; /** * Creates a new instance of TransactionHandle * * Used by provider to prepare origination transaction for sending command * message from an application to the stack. * * @param stack * the reference to the MGCP stack. */ public TransactionHandler(JainMgcpStackImpl stack) { this.stack = stack; this.localTID = GENERATOR++; // utils = new Utils(); utilsFactory = stack.getUtilsFactory(); // XXX:stack.addLocalTransaction(Integer.valueOf(localTID), this); stack.getLocalTransactions().put(Integer.valueOf(localTID), this); // if (logger.isDebugEnabled()) { // logger.debug("New mgcp transaction with id localID=" + localTID); // } } /** * Creates a new instance of TransactionHandle. * * Used by stack to prepare transaction for transmitting message from * provider to the application. * * @param stack * the reference to the MGCP stack. * @remoteAddress the address from wich command message was received. * @port the number of the port from wich command received. */ public TransactionHandler(JainMgcpStackImpl stack, InetAddress remoteAddress, int port) { this(stack); this.remoteAddress = remoteAddress; this.remotePort = port; if (this.stack.provider.getNotifiedEntity() == null) { NotifiedEntity notifiedEntity = new NotifiedEntity(this.remoteAddress.getHostName(), this.remoteAddress .getHostAddress(), this.remotePort); this.stack.provider.setNotifiedEntity(notifiedEntity); } } public void setEndpointHandler(EndpointHandler handler) { this.endpointHandler = handler; } public EndpointHandler getEndpointHandler() { return this.endpointHandler; } private void processTxTimeout() { try { // releases the tx release(false); // the try ensures the static timer will not get a runtime // exception process tx timeout if (sent) { try { stack.provider.processTxTimeout(commandEvent); } finally { getEndpointHandler().processTxTimeout(commandEvent, this); } } else { // TODO : Send back 406 TxTimedOut to NotifiedEntity try { stack.provider.processRxTimeout(commandEvent); } finally { getEndpointHandler().processRxTimeout(commandEvent, this); } } } catch (Exception e) { logger.error("Failed to release mgcp transaction localID=" + localTID, e); } } private class LongtranTimerTask extends TimerTask { public void run() { if (logger.isDebugEnabled()) { logger.debug("Transaction localID=" + localTID + " timeout"); processTxTimeout(); } } } private class ReTransmissionTimerTask extends TimerTask { public void run() { try { // Sending the command countOfCommandRetransmitted++; logger.warn("message = \n" + msgTemp + "\n local Tx ID = " + localTID + " Remote Tx ID = " + remoteTID + " Sending the Command " + countOfCommandRetransmitted); stack.send(sendComandDatagram); resetReTransmissionTimer(); } catch (Exception e) { logger.error("Failed to release mgcp transaction localID=" + localTID, e); } } } private class THISTTimerTask extends TimerTask { boolean responseSent = false; THISTTimerTask(boolean responseSent) { this.responseSent = responseSent; } public void run() { if (!responseSent) { if (logger.isDebugEnabled()) { logger.debug("T-HIST timeout processTxTimeout "); } try { processTxTimeout(); } catch (Exception e) { logger.error("Failed to delete the jainMgcpResponseEvent for txId", e); } } else { Integer key = new Integer(remoteTID); TransactionHandler obj = stack.getCompletedTransactions().remove(key); // XXX:TransactionHandler obj = stack.removeCompletedTx(key); obj.clearEndpointHandler(); if (logger.isDebugEnabled()) { logger.debug("T-HIST timeout deleting Response for Tx = " + remoteTID + " Response = " + obj); } obj = null; } } } /** * Check whether the given return code is a provisional response. * * @param rc * the return code * @return true when the code is provisional */ private boolean isProvisional(ReturnCode rc) { final int rval = rc.getValue(); return ((99 < rval) && (rval < 200)); } /** Release this transaction and frees all allocated resources. */ protected void release(boolean removeEndpointHandler) { // if (logger.isDebugEnabled()) { // logger.debug("Released transaction (local id=" + localTID + "), stop // timer"); // } // XXX:stack.removeLocalTransaction(Integer.valueOf(localTID)); // XXX:stack.removeRemoteTransaction(Integer.valueOf(remoteTID)); stack.getLocalTransactions().remove(Integer.valueOf(localTID)); stack.getRemoteTxToLocalTxMap().remove(Integer.valueOf(remoteTID)); cancelTHISTTimerTask(); cancelLongtranTimer(); cancelReTransmissionTimer(); if (removeEndpointHandler) { this.clearEndpointHandler(); } } public void clearEndpointHandler() { this.endpointHandler.transactionHandlerDeleted(this); this.endpointHandler = null; } /** * Returns the transaction handle sent from application to the MGCP * provider. * * @return the int value wich identifiers the transaction handle. */ public int getRemoteTID() { return remoteTID; } /** * Returns the transaction handle sent from MGCP provider to listener. * * @return the int value wich identifiers the transaction handle. */ public int getLocalTID() { return localTID; } /** * Encodes command event object into MGCP command message. * * All descendant classes should implement this method with accordance of * the command type. * * @param event * the command event object. * @return the encoded MGCP message. */ public abstract String encode(JainMgcpCommandEvent event); /** * Encodes response event object into MGCP response message. * * All descendant classes should implement this method with accordance of * the response type. * * @param event * the response event object. * @return the encoded MGCP message. */ public abstract String encode(JainMgcpResponseEvent event); /** * Decodes MGCP command message into jain mgcp command event object. * * All descendant classes should implement this method with accordance of * the command type. * * @param MGCP * message * @return jain mgcp command event object. */ public abstract JainMgcpCommandEvent decodeCommand(final String msg) throws ParseException; /** * Decodes MGCP response message into jain mgcp response event object. * * All descendant classes should implement this method with accordance of * the command type. * * @param MGCP * message * @return jain mgcp response event object. */ public abstract JainMgcpResponseEvent decodeResponse(String message) throws ParseException; public abstract JainMgcpResponseEvent getProvisionalResponse(); public void run() { try { ActionPerform ap = this.actionToPerform.remove(); ap.perform(); } catch (NoSuchElementException nsee) { logger.error("Received NoSuchElementException for remoteeTx = " + remoteTID); } } protected void sendProvisionalResponse() { this.send(getProvisionalResponse()); } /** * Sends MGCP command from the application to the endpoint specified in the * message. * * @param event * the jain mgcp command event object. */ private void send(JainMgcpCommandEvent event) { sent = true; String host = ""; int port = 0; switch (event.getObjectIdentifier()) { case Constants.CMD_NOTIFY: Notify notifyCommand = (Notify) event; NotifiedEntity notifiedEntity = notifyCommand.getNotifiedEntity(); if (notifiedEntity == null) { notifiedEntity = this.stack.provider.getNotifiedEntity(); } port = notifiedEntity.getPortNumber(); // if (notifiedEntity.getLocalName() != null) { // host = notifiedEntity.getLocalName() + "@"; // } host += notifiedEntity.getDomainName(); break; default: // determite destination address and port to send request to // from endpoint identifier parameter. String domainName = event.getEndpointIdentifier().getDomainName(); // now checks does port number is specified in the domain name // if port number is not specified use 2427 by default int pos = domainName.indexOf(':'); if (pos > 0) { port = Integer.parseInt(domainName.substring(pos + 1)); host = domainName.substring(0, pos); } else { port = 2427; host = domainName; } break; } // construct the destination as InetAddress object InetAddress address = null; try { address = InetAddress.getByName(host); } catch (UnknownHostException e) { throw new IllegalArgumentException("Unknown endpoint " + host); } // save this tx in stack and start timer remoteTID = event.getTransactionHandle(); source = event.getSource(); event.setTransactionHandle(localTID); // encode event object as MGCP command and send over UDP. String msg = encode(event); msgTemp = msg; byte[] data = msg.getBytes(); sendComandDatagram = new DatagramPacket(data, data.length, address, port); resetReTransmissionTimer(); resetTHISTTimerTask(false); if (logger.isDebugEnabled()) { logger.debug("Send command event to " + address + ", message\n" + msg); } countOfCommandRetransmitted++; stack.send(sendComandDatagram); try { getEndpointHandler().commandDelivered(event, this); } catch (Exception e) { e.printStackTrace(); } } /** * Sends MGCP response message from the application to the host from wich * origination command was received. * * @param event * the jain mgcp response event object. */ private void send(JainMgcpResponseEvent event) { cancelLongtranTimer(); // to send response we already should know the address and port // number from which the original request was received if (remoteAddress == null) { throw new IllegalArgumentException("Unknown orinator address"); } // restore the original transaction handle parameter // and encode event objet into MGCP response message event.setTransactionHandle(remoteTID); // encode event object into MGCP response message String msg = encode(event); // send response message to the originator byte[] data = msg.getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length, remoteAddress, remotePort); if (logger.isDebugEnabled()) { logger.debug("--- TransactionHandler:" + this + " :LocalID=" + localTID + ", Send response event to " + remoteAddress + ":" + remotePort + ", message\n" + msg); } stack.send(packet); /* * Just reset timer in case of provisional response. Otherwise, release * tx. */ if (isProvisional(event.getReturnCode())) { // reset timer. resetLongtranTimer(); } else { try { getEndpointHandler().commandDelivered(commandEvent, event, this); } catch (Exception e) { e.printStackTrace(); } release(false); stack.getCompletedTransactions().put(Integer.valueOf(event.getTransactionHandle()), this); // XXX:stack.addCompletedTransaction(Integer.valueOf(event.getTransactionHandle()), // this); resetTHISTTimerTask(true); } } private void cancelLongtranTimer() { if (longtranTimerTask != null) { longtranTimerTask.cancel(); longtranTimerTask = null; } } private void resetLongtranTimer() { longtranTimerTask = new LongtranTimerTask(); transactionHandlerTimer.schedule(longtranTimerTask, LONGTRAN_TIMER_TIMEOUT); } private void cancelReTransmissionTimer() { if (reTransmissionTimer != null) { reTransmissionTimer.cancel(); reTransmissionTimer = null; } } private void resetReTransmissionTimer() { cancelReTransmissionTimer(); reTransmissionTimer = new ReTransmissionTimerTask(); transactionHandlerTimer.schedule(reTransmissionTimer, calculateReTransmissionTimeout()); } // TODO : Implement the AAD and ADEV from TCP private int calculateReTransmissionTimeout() { int reTransmissionTimeoutSec = A + N * D; N = N * 2; return reTransmissionTimeoutSec * 1000; } private void cancelTHISTTimerTask() { if (tHISTTimerTask != null) { tHISTTimerTask.cancel(); tHISTTimerTask = null; } } private void resetTHISTTimerTask(boolean responseSent) { cancelTHISTTimerTask(); tHISTTimerTask = new THISTTimerTask(responseSent); transactionHandlerTimer.schedule(tHISTTimerTask, THIST_TIMER_TIMEOUT); } /** * constructs the object source for a command * * @param tid * @return */ protected Object getObjectSource(int tid) { if (sent) { return stack; } else { return new ReceivedTransactionID(tid, this.remoteAddress, remotePort); } } public boolean isCommand() { return isCommand; } public void setCommand(boolean isCommand) { this.isCommand = isCommand; } private JainMgcpCommandEvent getCommandEvent() { return commandEvent; } public void setCommandEvent(JainMgcpCommandEvent commandEvent) { this.commandEvent = commandEvent; this.actionToPerform.add(new ScheduleCommandSend()); } private JainMgcpResponseEvent getResponseEvent() { return responseEvent; } public void setResponseEvent(JainMgcpResponseEvent responseEvent) { this.responseEvent = responseEvent; this.actionToPerform.add(new ScheduleCommandSend()); } public void markRetransmision() { this.retransmision = true; } public void receiveRequest(final EndpointIdentifier endpoint, final String msg, final Integer remoteTID) { this.remoteTID = remoteTID; this.endpoint = endpoint; try { commandEvent = decodeCommand(msg); // if (logger.isDebugEnabled()) { // logger.debug("Event decoded: \n" + event); // } } catch (ParseException e) { logger.error("Coud not parse message: ", e); return; } sent = false; // store original transaction handle parameter // and populate with local value stack.getRemoteTxToLocalTxMap().put(remoteTID, new Integer(localTID)); commandEvent.setTransactionHandle(localTID); resetLongtranTimer(); this.actionToPerform.add(new ScheduleRequestReceival(this)); // we shoudl be scheduled by message handler } /** * Used by stack for relaying received MGCP response messages to the * application. * * @param message * receive MGCP response message. */ public void receiveResponse(String message) { cancelReTransmissionTimer(); cancelLongtranTimer(); JainMgcpResponseEvent event = null; try { event = decodeResponse(message); } catch (Exception e) { logger.error("Could not decode message: ", e); } // restore original transaction handle parameter event.setTransactionHandle(remoteTID); /* * Just reset timer in case of provisional response. Otherwise, release * tx. */ if (this.isProvisional(event.getReturnCode())) { resetLongtranTimer(); } // fire event only if non provisional response // This is done in MessageHandler // endpointHandler.scheduleTransactionHandler(this); this.actionToPerform.add(new ScheduleResponseReceival(event, this)); } public String getEndpointId() { return this.commandEvent.getEndpointIdentifier().toString(); } protected abstract class ActionPerform { public abstract void perform(); } protected class ScheduleRequestReceival extends ActionPerform { protected TransactionHandler th = null; public ScheduleRequestReceival(TransactionHandler th) { super(); this.th = th; } @Override public void perform() { try { stack.provider.processMgcpCommandEvent(commandEvent); } catch (Exception e) { e.printStackTrace(); } finally { getEndpointHandler().commandDelivered(commandEvent, th); } } } protected class ScheduleResponseReceival extends ActionPerform { protected JainMgcpResponseEvent event = null; protected TransactionHandler th = null; public ScheduleResponseReceival(JainMgcpResponseEvent event, TransactionHandler th) { super(); this.th = th; this.event = event; } @Override public void perform() { try { stack.provider.processMgcpResponseEvent(event, commandEvent); } catch (Exception e) { e.printStackTrace(); } finally { int responseCode = event.getReturnCode().getValue(); MgcpResponseType type = MgcpResponseType.getResponseTypeFromCode(responseCode); if (type.equals(MgcpResponseType.ProvisionalResponse)) { return; } else { try { getEndpointHandler().commandDelivered(commandEvent, event, th); } finally { release(true); } } } } } protected class ScheduleCommandSend extends ActionPerform { @Override public void perform() { try { if (isCommand) { send(getCommandEvent()); } else { send(getResponseEvent()); } } catch (Exception e) { logger.error(e); } } } }