package jade.imtp.leap.nio;
//#J2ME_EXCLUDE_FILE
import jade.core.FrontEnd;
import jade.core.BackEnd;
import jade.core.BackEndContainer;
import jade.core.BEConnectionManager;
import jade.core.Profile;
import jade.core.ProfileException;
import jade.core.IMTPException;
import jade.imtp.leap.BackEndSkel;
import jade.imtp.leap.FrontEndStub;
import jade.imtp.leap.Dispatcher;
import jade.imtp.leap.ICPException;
import jade.imtp.leap.JICP.JICPProtocol;
import jade.imtp.leap.JICP.JICPMediatorManager;
import jade.imtp.leap.JICP.JICPPacket;
import jade.imtp.leap.JICP.Connection;
import jade.util.leap.Properties;
import jade.util.Logger;
import java.io.IOException;
import java.net.InetAddress;
import java.util.logging.Level;
/**
This class implements the BIFEDispatcher related BackEnd dispatcher
managable by an asynchronous JICPMediatorManager
@author Giovanni Caire - Telecom Italia LAB S.p.A.
*/
public class NIOBEDispatcher implements NIOMediator, BEConnectionManager, Dispatcher {
private static final long RESPONSE_TIMEOUT = 60000;
private long keepAliveTime;
private boolean enableServerKeepAlive;
private long lastReceivedTime;
private boolean active = true;
private boolean peerActive = true;
private boolean connectionDropped = false;
private JICPMediatorManager myMediatorManager;
private String myID;
private Properties myProperties;
private BackEndContainer myContainer = null;
protected InputManager inpManager;
protected OutputManager outManager;
private Logger myLogger = Logger.getMyLogger(getClass().getName());
/**
Retrieve the ID of this mediator. Returns null if this mediator
is not active
*/
public String getID() {
return (active ? myID : null);
}
/**
Retrieve the startup Properties for this NIOBEDispatcher.
*/
public Properties getProperties() {
return myProperties;
}
/**
Initialize this JICPMediator
*/
public void init(JICPMediatorManager mgr, String id, Properties props) throws ICPException {
myMediatorManager = mgr;
myID = id;
myProperties = props;
// Max disconnection time
long maxDisconnectionTime = JICPProtocol.DEFAULT_MAX_DISCONNECTION_TIME;
try {
maxDisconnectionTime = Long.parseLong(props.getProperty(JICPProtocol.MAX_DISCONNECTION_TIME_KEY));
} catch (Exception e) {
// Keep default
}
// Keep-alive time
keepAliveTime = JICPProtocol.DEFAULT_KEEP_ALIVE_TIME;
try {
keepAliveTime = Long.parseLong(props.getProperty(JICPProtocol.KEEP_ALIVE_TIME_KEY));
} catch (Exception e) {
// Keep default
}
// Server-keep-alive time
enableServerKeepAlive = false;
try {
enableServerKeepAlive = Boolean.valueOf(props.getProperty("enable-server-keep-alive")).booleanValue();
} catch (Exception e) {
// Keep default
}
// inpCnt
int inpCnt = 0;
try {
inpCnt = (Integer.parseInt(props.getProperty("lastsid")) + 1) & 0x0f;
} catch (Exception e) {
// Keep default
}
System.out.println("Next command for FE will have sessionID " + inpCnt);
// lastSid
int lastSid = 0x0f;
try {
lastSid = (byte) (Integer.parseInt(props.getProperty("outcnt")) - 1);
if (lastSid < 0) {
lastSid = 0x0f;
}
} catch (Exception e) {
// Keep default
}
FrontEndStub st = new FrontEndStub(this);
inpManager = new InputManager(inpCnt, st);
BackEndSkel sk = startBackEndContainer(props);
outManager = new OutputManager(lastSid, sk, maxDisconnectionTime);
}
protected final BackEndSkel startBackEndContainer(Properties props) throws ICPException {
try {
String nodeName = myID.replace(':', '_');
props.setProperty(Profile.CONTAINER_NAME, nodeName);
myContainer = new BackEndContainer(props, this);
if (!myContainer.connect()) {
throw new ICPException("BackEnd container failed to join the platform");
}
// Possibly the node name was re-assigned by the main
myID = myContainer.here().getName();
if (myLogger.isLoggable(Logger.CONFIG)) {
myLogger.log(Logger.CONFIG, "BackEndContainer " + myID + " successfully joined the platform");
}
return new BackEndSkel(myContainer);
} catch (ProfileException pe) {
// should never happen
pe.printStackTrace();
throw new ICPException("Error creating profile");
}
}
// Local variable only used in the kill() method
private Object shutdownLock = new Object();
/**
Kill the above container.
This may be called by the JICPMediatorManager or when
a peer termination notification is received.
*/
public void kill() {
// Avoid killing the above container two times
synchronized (shutdownLock) {
if (active) {
active = false;
myContainer.shutDown();
}
}
}
/**
* Passes to this JICPMediator the connection opened by the mediated
* entity.
* This is called by the JICPMediatorManager this Mediator is attached to
* as soon as the mediated entity (re)connects.
* @param c the connection to the mediated entity
* @param pkt the packet that was sent by the mediated entity when
* opening this connection
* @param addr the address of the mediated entity
* @param port the local port used by the mediated entity
* @return an indication to the JICPMediatorManager to keep the
* connection open.
*/
public boolean handleIncomingConnection(Connection c, JICPPacket pkt, InetAddress addr, int port) {
checkTerminatedInfo(pkt);
if (peerActive) {
if (connectionDropped) {
droppedToDisconnected();
}
// Update keep-alive info
lastReceivedTime = System.currentTimeMillis();
boolean inp = false;
byte[] data = pkt.getData();
if (data.length == 1) {
inp = (data[0] == 1);
}
if (inp) {
inpManager.setConnection((NIOJICPConnection) c);
if (myLogger.isLoggable(Logger.CONFIG)) {
myLogger.log(Logger.CONFIG, myID + ": New INP Connection establishd");
}
} else {
outManager.setConnection(c);
if (myLogger.isLoggable(Logger.CONFIG)) {
myLogger.log(Logger.CONFIG, myID + ": New OUT Connection establishd");
}
}
return true;
} else {
// The remote FrontEnd has terminated spontaneously -->
// Kill the above container (this will also kill this NIOBEDispatcher).
kill();
return false;
}
}
/**
Notify this NIOMediator that an error occurred on one of the
Connections it was using. This information is important since,
unlike normal mediators, a NIOMediator typically does not read
packets from
connections on its own (the JICPMediatorManager does that in general).
*/
public void handleConnectionError(Connection c, Exception e) {
myLogger.log(Level.WARNING, "connection error", e);
if (active && peerActive) {
// Try assuming it is the input connection
try {
inpManager.checkConnection(c);
myLogger.log(Logger.WARNING, myID + ": IC Disconnection detected");
inpManager.resetConnection();
} catch (ICPException icpe) {
// Then try assuming it is the output connection
try {
outManager.checkConnection(c);
myLogger.log(Logger.WARNING, myID + ": OC Disconnection detected");
outManager.resetConnection();
} catch (ICPException icpe2) {
// Ignore it
}
}
}
}
/**
Passes to this mediator a JICPPacket received by the
JICPMediatorManager this mediator is attached to.
In a NIOMediator this should never be called.
*/
public JICPPacket handleJICPPacket(JICPPacket p, InetAddress addr, int port) throws ICPException {
throw new ICPException("Unexpected call");
}
/**
Overloaded version of the handleJICPPacket() method including
the <code>Connection</code> the incoming JICPPacket was received
from. This information is important since, unlike normal mediators,
a NIOMediator may not read packets from connections on its own (the
JICPMediatorManager does that in general).
*/
public JICPPacket handleJICPPacket(Connection c, JICPPacket pkt, InetAddress addr, int port) throws ICPException {
if (pkt.getType() == JICPProtocol.DROP_DOWN_TYPE) {
// Note that the return packet is written inside the handleDropDown()
// method since the connection must be closed after the response has
// been sent back.
handleDropDown(c, pkt, addr, port);
return null;
}
checkTerminatedInfo(pkt);
// Update keep-alive info
lastReceivedTime = System.currentTimeMillis();
byte type = pkt.getType();
if (type == JICPProtocol.COMMAND_TYPE) {
if (peerActive) {
return outManager.handleCommand(c, pkt);
} else {
// The remote FrontEnd has terminated spontaneously -->
// Kill the above container (this will also kill this NIOBEDispatcher).
kill();
return null;
}
} else if (type == JICPProtocol.KEEP_ALIVE_TYPE) {
if (enableServerKeepAlive) {
inpManager.sendServerKeepAlive();
}
return outManager.handleKeepAlive(c, pkt);
} /* Asynch-reply
else if (type == JICPProtocol.RESPONSE_TYPE || type == JICPProtocol.ERROR_TYPE) {
inpManager.handleResponse(c, pkt);
return null;
}*/ else {
throw new ICPException("Unexpected packet type " + type);
}
}
/**
This is periodically called by the JICPMediatorManager and is
used by this NIOMediator to evaluate the elapsed time without
the need of a dedicated thread or timer.
*/
public final void tick(long currentTime) {
if (active && !connectionDropped) {
// 1) If there is a blocking read operation in place check the
// response timeout
inpManager.checkResponseTime(currentTime);
// 2) Evaluate the keep alive
if (keepAliveTime > 0) {
if ((currentTime - lastReceivedTime) > (keepAliveTime + RESPONSE_TIMEOUT)) {
// Missing keep-alive.
// The OUT connection is no longer valid
if (outManager.isConnected()) {
myLogger.log(Logger.WARNING, myID + ": Missing keep-alive");
outManager.resetConnection();
}
// Check the INP connection. Since this method must return
// asap, does it in a separated Thread
if (inpManager.isConnected()) {
Thread t = new Thread() {
public void run() {
try {
//JICPPacket pkt = new JICPPacket(JICPProtocol.KEEP_ALIVE_TYPE, JICPProtocol.DEFAULT_INFO, null);
//inpManager.dispatch(pkt, false);
inpManager.sendServerKeepAlive();
if (myLogger.isLoggable(Logger.CONFIG)) {
myLogger.log(Logger.CONFIG, myID + ": IC valid");
}
} catch (Exception e) {
// Just do nothing: the INP connection has been reset
}
}
};
t.start();
}
}
}
// 3) Evaluate the max disconnection time
if (outManager.checkMaxDisconnectionTime(currentTime)) {
myLogger.log(Logger.SEVERE, myID + ": Max disconnection time expired.");
// Consider as if the FrontEnd has terminated spontaneously -->
// Kill the above container (this will also kill this NIOBEDispatcher).
kill();
}
}
}
////////////////////////////////////////////////
// BEConnectionManager interface implementation
////////////////////////////////////////////////
/**
Return a stub of the remote FrontEnd that is connected to the
local BackEnd.
@param be The local BackEnd
@param props Additional (implementation dependent) connection
configuration properties.
@return A stub of the remote FrontEnd.
*/
public FrontEnd getFrontEnd(BackEnd be, Properties props) throws IMTPException {
return inpManager.getStub();
}
/**
Make this NIOBEDispatcher terminate.
*/
public void shutdown() {
active = false;
if (myLogger.isLoggable(Logger.INFO)) {
myLogger.log(Logger.INFO, myID + ": shutting down");
}
// Deregister from the JICPServer
if (myID != null) {
myMediatorManager.deregisterMediator(myID);
}
inpManager.shutdown();
outManager.shutdown();
}
//////////////////////////////////////////
// Dispatcher interface implementation
//////////////////////////////////////////
public byte[] dispatch(byte[] payload, boolean flush) throws ICPException {
if (connectionDropped) {
// Move from DROPPED state to DISCONNECTED state and wait
// for the FE to reconnect
droppedToDisconnected();
requestRefresh();
throw new ICPException("Connection dropped");
} else {
// Normal dispatch
JICPPacket pkt = new JICPPacket(JICPProtocol.COMMAND_TYPE, JICPProtocol.DEFAULT_INFO, payload);
pkt = inpManager.dispatch(pkt, flush);
return pkt.getData();
}
}
//////////////////////////////////////////////////////
// Methods related to connection drop-down management
//////////////////////////////////////////////////////
/**
Handle a connection DROP_DOWN request from the FE.
*/
protected void handleDropDown(Connection c, JICPPacket pkt, InetAddress addr, int port) {
if (myLogger.isLoggable(Logger.INFO)) {
myLogger.log(Logger.INFO, myID + ": DROP_DOWN request received.");
}
try {
// If the INP connection is down or we have some postponed command
// to flush, refuse dropping the connection
if (inpManager.isConnected() && inpManager.isEmpty()) {
JICPPacket rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, null);
c.writePacket(rsp);
inpManager.resetConnection();
outManager.resetConnection();
connectionDropped = true;
} else {
myLogger.log(Logger.WARNING, myID + ": DROP_DOWN request refused.");
JICPPacket rsp = new JICPPacket(JICPProtocol.ERROR_TYPE, getReconnectInfo(), null);
c.writePacket(rsp);
}
} catch (Exception e) {
myLogger.log(Logger.WARNING, myID + ": Error writing DROP_DOWN response. " + e);
}
}
/**
Move from the connectionDropped state to the Disconnected state.
This may happen when
- a packet must be dispatched to the FE.
- an incoming connection is detected
*/
private void droppedToDisconnected() {
connectionDropped = false;
outManager.setExpirationDeadline();
}
/**
Request the FE to refresh the connection.
*/
protected void requestRefresh() {
}
public boolean isConnected() {
return inpManager.isConnected() && outManager.isConnected();
}
private void updateConnectedState() {
myProperties.put(BEManagementHelper.CONNECTED, (isConnected() ? "true" : "false"));
}
/**
Inner class InputManager.
This class manages the delivery of commands to the FrontEnd
*/
protected class InputManager {
private NIOJICPConnection myConnection;
private boolean dispatching = false;
private boolean connectionRefreshed;
private boolean waitingForFlush;
private long readStartTime = -1;
private JICPPacket currentReply;
private Object dispatchLock = new Object();
private int inpCnt;
private FrontEndStub myStub;
InputManager(int c, FrontEndStub s) {
inpCnt = c;
myStub = s;
}
FrontEndStub getStub() {
return myStub;
}
synchronized void setConnection(NIOJICPConnection c) {
// Reset the old connection
resetConnection();
// Set the new connection
myConnection = c;
connectionRefreshed = true;
waitingForFlush = myStub.flush();
//myContainer.notifyInputConnectionReady();
updateConnectedState();
}
synchronized void resetConnection() {
// Close the connection if it was in place
if (myConnection != null) {
close(myConnection);
myConnection = null;
}
// Asynch-reply
// If there was someone waiting for a response on the
// connection notify it.
//notifyAll();
updateConnectedState();
}
final void checkConnection(Connection c) throws ICPException {
if (c != myConnection) {
throw new ICPException("Wrong connection");
}
}
final boolean isConnected() {
return myConnection != null;
}
final boolean isEmpty() {
// We are empty if we are not dispatching a JICPPacket and our stub
// has no postponed commands waiting to be delivered.
return (!dispatching) && myStub.isEmpty();
}
void shutdown() {
resetConnection();
}
/**
Dispatch a JICP command to the FE and get back a reply.
This method must NOT be executed in mutual exclusion with
setConnection() and resetConnection() since it performs a
blocking read operation --> It can't just be declared synchronized.
*/
final JICPPacket dispatch(JICPPacket pkt, boolean flush) throws ICPException {
synchronized (dispatchLock) {
dispatching = true;
try {
synchronized (this) {
if ((!active) || (myConnection == null) || (waitingForFlush && (!flush))) {
// If we are waiting for flushed packets and the current packet
// is a normal (i.e. non-flushed) one, then throw an exception -->
// The packet will be put in the queue of packets to be flushed
throw new ICPException("Unreachable");
}
waitingForFlush = false;
connectionRefreshed = false;
}
try {
pkt.setSessionID((byte) inpCnt);
if (myLogger.isLoggable(Logger.FINE)) {
myLogger.log(Logger.FINE, myID + ": Sending command " + inpCnt + " to FE");
}
long start = System.currentTimeMillis();
myConnection.writePacket(pkt);
// Asynch-reply: JICPPacket reply = waitForReply(RESPONSE_TIMEOUT);
readStartTime = System.currentTimeMillis();
JICPPacket reply = myConnection.readPacket();
readStartTime = -1;
checkTerminatedInfo(reply);
lastReceivedTime = System.currentTimeMillis();
long end = lastReceivedTime;
System.out.println("INP Session " + inpCnt + ". Dispatching time = " + (end - start));
if (myLogger.isLoggable(Logger.FINER)) {
myLogger.log(Logger.FINER, myID + ": Received response " + inpCnt + " from FE");
}
if (reply.getType() == JICPProtocol.ERROR_TYPE) {
// Communication OK, but there was a JICP error on the peer
throw new ICPException(new String(pkt.getData()));
}
if (!peerActive) {
// This is the response to an exit command --> Suicide, without
// killing the above container since it is already dying.
NIOBEDispatcher.this.shutdown();
}
inpCnt = (inpCnt + 1) & 0x0f;
return reply;
} catch (NullPointerException npe) {
// This can happen if a resetConnection() occurs just before
// myConnection.writePacket()/readPacket() is called.
throw new ICPException("Connection reset.");
} catch (IOException ioe) {
synchronized (this) {
if (myConnection != null && !connectionRefreshed) {
// There was an IO exception writing data to the connection
// --> reset the connection.
myLogger.log(Logger.WARNING, myID + ": IOException IC. " + ioe);
resetConnection();
}
}
readStartTime = -1;
throw new ICPException("Dispatching error.", ioe);
}
} finally {
dispatching = false;
}
}
}
public final void checkResponseTime(long currentTime) {
if (readStartTime > 0 && (currentTime - readStartTime) > RESPONSE_TIMEOUT) {
myLogger.log(Logger.WARNING, myID + ": Response timeout expired.");
resetConnection();
}
}
public void sendServerKeepAlive() throws ICPException {
JICPPacket pkt = new JICPPacket(JICPProtocol.KEEP_ALIVE_TYPE, JICPProtocol.DEFAULT_INFO, null);
dispatch(pkt, false);
}
/* Asynch-reply
final synchronized void handleResponse(Connection c, JICPPacket reply) throws ICPException {
checkConnection(c);
currentReply = reply;
notifyAll();
}
private synchronized JICPPacket waitForReply(long timeout) throws ICPException, IOException {
try {
if (currentReply == null) {
wait(timeout);
if (currentReply == null) {
if (isConnected()) {
if (connectionRefreshed) {
throw new ICPException("Connection refreshed");
}
else {
throw new IOException("Response timeout expired");
}
}
else {
throw new ICPException("Connection reset");
}
}
}
JICPPacket tmp = currentReply;
currentReply = null;
return tmp;
}
catch (InterruptedException ie) {
throw new ICPException("Interrupted");
}
}*/
} // END of inner class InputManager
/**
Inner class OutputManager
This class manages the reception of commands and keep-alive
packets from the FrontEnd.
This class also manages the maxDisconnectionTime, i.e. the remote
FrontEnd is considered dead if it cannot re-establish the
OUT connection within the maxDisconnectionTime.
*/
protected class OutputManager {
private Connection myConnection;
private JICPPacket lastResponse;
private int lastSid;
private BackEndSkel mySkel;
private long maxDisconnectionTime, expirationDeadline;
OutputManager(int n, BackEndSkel s, long t) {
lastSid = n;
mySkel = s;
maxDisconnectionTime = t;
}
synchronized void setConnection(Connection c) {
// Close the old connection if any
if (myConnection != null) {
close(myConnection);
}
// Set the new connection
myConnection = c;
updateConnectedState();
}
synchronized void resetConnection() {
if (myConnection != null) {
setExpirationDeadline();
close(myConnection);
}
myConnection = null;
updateConnectedState();
}
synchronized void setExpirationDeadline() {
expirationDeadline = System.currentTimeMillis() + maxDisconnectionTime;
}
final void checkConnection(Connection c) throws ICPException {
if (c != myConnection) {
throw new ICPException("Wrong connection");
}
}
final boolean isConnected() {
return (myConnection != null);
}
void shutdown() {
resetConnection();
}
final synchronized JICPPacket handleCommand(Connection c, JICPPacket cmd) throws ICPException {
checkConnection(c);
JICPPacket reply = null;
byte sid = cmd.getSessionID();
if (sid == lastSid) {
myLogger.log(Logger.WARNING, myID + ": Duplicated command from FE " + sid);
reply = lastResponse;
} else {
if (myLogger.isLoggable(Logger.FINE)) {
myLogger.log(Logger.FINE, myID + ": Received command " + sid + " from FE");
}
byte[] rspData = mySkel.handleCommand(cmd.getData());
if (myLogger.isLoggable(Logger.FINER)) {
myLogger.log(Logger.FINER, myID + ": Command " + sid + " from FE served ");
}
reply = new JICPPacket(JICPProtocol.RESPONSE_TYPE, getReconnectInfo(), rspData);
reply.setSessionID(sid);
lastSid = sid;
lastResponse = reply;
}
return reply;
}
synchronized JICPPacket handleKeepAlive(Connection c, JICPPacket command) throws ICPException {
checkConnection(c);
if (myLogger.isLoggable(Logger.FINEST)) {
myLogger.log(Logger.FINEST, myID + ": Keep-alive received");
}
return new JICPPacket(JICPProtocol.RESPONSE_TYPE, getReconnectInfo(), null);
}
final synchronized boolean checkMaxDisconnectionTime(long currentTime) {
return (!isConnected()) && (currentTime > expirationDeadline);
}
} // END of inner class OutputManager
private final byte getReconnectInfo() {
byte info = JICPProtocol.DEFAULT_INFO;
// If the inpConnection is null request the FrontEnd to reconnect
if (!inpManager.isConnected()) {
info |= JICPProtocol.RECONNECT_INFO;
}
return info;
}
private final void checkTerminatedInfo(JICPPacket pkt) {
if ((pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0) {
peerActive = false;
if (myLogger.isLoggable(Logger.INFO)) {
myLogger.log(Logger.INFO, myID + ": Peer termination notification received");
}
}
}
private void close(Connection c) {
try {
c.close();
} catch (IOException ioe) {
}
}
}