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; /** This class implements the BIFEDispatcher related BackEnd dispatcher managable by an asynchronous JICPMediatorManager @author Giovanni Caire - Telecom Italia LAB S.p.A. */ public class BackEndDispatcher implements NIOMediator, BEConnectionManager, Dispatcher { private static final long RESPONSE_TIMEOUT = 60000; private long keepAliveTime; private long maxDisconnectionTime; private long expirationDeadline; 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; private Connection myConnection = null; private Object writeLock = new Object(); 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 NIOMediator */ public void init(JICPMediatorManager mgr, String id, Properties props) throws ICPException { System.out.println("BackEndDispatcher starting..."); myMediatorManager = mgr; myID = id; myProperties = props; // Max disconnection time 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 } // 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); } 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 synchronized boolean handleIncomingConnection(Connection c, JICPPacket pkt, InetAddress addr, int port) { checkTerminatedInfo(pkt); // Update keep-alive info lastReceivedTime = System.currentTimeMillis(); if (peerActive) { myConnection = c; updateConnectedState(); inpManager.setConnection(myConnection); connectionDropped = false; return true; } else { // The remote FrontEnd has terminated spontaneously --> // Kill the above container (this will also kill this BackEndDispatcher). kill(); return false; } } /** Notify this NIOMediator that an error occurred on one of the Connections it is 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 synchronized void handleConnectionError(Connection c, Exception e) { if (active && peerActive) { if (c == myConnection) { myConnection = null; updateConnectedState(); inpManager.resetConnection(); myLogger.log(Logger.WARNING, myID+": Disconnection detected"); setExpirationDeadline(); } } } /** 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 { checkTerminatedInfo(pkt); // Update keep-alive info lastReceivedTime = System.currentTimeMillis(); JICPPacket reply = null; byte type = pkt.getType(); switch (type) { case JICPProtocol.DROP_DOWN_TYPE: System.out.println("DROP_DOWN received: "+pkt.getSessionID()); // 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); break; case JICPProtocol.COMMAND_TYPE: System.out.println("COMMAND received: "+pkt.getSessionID()); if (peerActive) { reply = outManager.handleCommand(pkt); } else { // The remote FrontEnd has terminated spontaneously --> // Kill the above container (this will also kill this NIOBEDispatcher). kill(); } break; case JICPProtocol.KEEP_ALIVE_TYPE: System.out.println("KEEP_ALIVE received: "+pkt.getSessionID()); reply = outManager.handleKeepAlive(pkt); break; case JICPProtocol.RESPONSE_TYPE: case JICPProtocol.ERROR_TYPE: System.out.println("RESPONSE/ERROR received: "+pkt.getSessionID()); inpManager.notifyIncomingResponseReceived(pkt); break; default: throw new ICPException("Unexpected packet type "+type); } if (reply != null) { try { writePacket(myConnection, reply); System.out.println("RESPONSE sent back: "+reply.getSessionID()); } catch (IOException ioe) { myLogger.log(Logger.WARNING, myID+": Communication error sending back response. "+ioe); } } return null; } private void writePacket(Connection c, JICPPacket pkt) throws IOException { // This is done to ensure that commands and responses are sent to FE separately synchronized (writeLock) { c.writePacket(pkt); } } /** 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) Evaluate the keep alive if (keepAliveTime > 0) { if ((currentTime - lastReceivedTime) > (keepAliveTime + RESPONSE_TIMEOUT)) { // Missing keep-alive. // FIXME: to be implemented } } // 2) Evaluate the max disconnection time if (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 BackEndDispatcher). 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 BackEndDispatcher 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 synchronized 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(); 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 (inpManager.isEmpty()) { JICPPacket rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, null); writePacket(c, rsp); myConnection = null; updateConnectedState(); inpManager.resetConnection(); connectionDropped = true; } else { // If we have some postponed command to flush, refuse dropping the connection myLogger.log(Logger.WARNING, myID+": DROP_DOWN request refused."); JICPPacket rsp = new JICPPacket(JICPProtocol.ERROR_TYPE, JICPProtocol.DEFAULT_INFO, null); writePacket(c, 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; setExpirationDeadline(); requestRefresh(); } /** Request the FE to refresh the connection. This default implementation does nothing. Subclasses may redefine this method to exploit some application specific out-of-band channel */ protected void requestRefresh() { } public synchronized boolean isConnected() { return myConnection != null; } 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 Connection myConnection; private boolean dispatching = false; private boolean waitingForFlush; private JICPPacket lastIncomingResponse; private int inpCnt; private FrontEndStub myStub; InputManager(int c, FrontEndStub s) { inpCnt = c; myStub = s; } FrontEndStub getStub() { return myStub; } void setConnection(Connection c) { myConnection = c; waitingForFlush = myStub.flush(); } void resetConnection() { synchronized (BackEndDispatcher.this) { myConnection = null; // If there was someone waiting for a response on the connection notify it. BackEndDispatcher.this.notifyAll(); } } 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. */ final JICPPacket dispatch(JICPPacket pkt, boolean flush) throws ICPException { dispatching = true; try { if (active && myConnection != null) { if (waitingForFlush && !flush) { throw new ICPException("Upsetting dispatching order"); } waitingForFlush = false; if (myLogger.isLoggable(Logger.FINE)) { myLogger.log(Logger.FINE, myID+": Sending command "+inpCnt+" to FE"); } pkt.setSessionID((byte) inpCnt); try { lastIncomingResponse = null; System.out.println("Sending command to FE "+pkt.getSessionID()); writePacket(myConnection, pkt); System.out.println("Waiting for response from FE "+pkt.getSessionID()); pkt = waitForResponse(inpCnt, RESPONSE_TIMEOUT); if (pkt != null) { System.out.println("Response received from FE "+pkt.getSessionID()); if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, myID+": Response received from FE "+pkt.getSessionID()); } if (pkt.getType() == JICPProtocol.ERROR_TYPE) { // Communication OK, but there was a JICP error on the peer throw new ICPException(new String(pkt.getData())); } checkTerminatedInfo(pkt); if (!peerActive) { // This is the response to an exit command --> Suicide, without // killing the above container since it is already dying. BackEndDispatcher.this.shutdown(); } inpCnt = (inpCnt+1) & 0x0f; return pkt; } else { myLogger.log(Logger.WARNING, myID+": Response timeout expired"); handleConnectionError(myConnection, null); throw new ICPException("Response timeout expired"); } } catch (IOException ioe) { // There was an IO exception writing data to the connection // --> reset the connection. myLogger.log(Logger.WARNING, myID+": "+ioe); handleConnectionError(myConnection, ioe); throw new ICPException("Dispatching error.", ioe); } } else { throw new ICPException("Unreachable"); } } finally { dispatching = false; } } private JICPPacket waitForResponse(int sessionID, long timeout) { try { while (lastIncomingResponse == null ) { BackEndDispatcher.this.wait(timeout); if (lastIncomingResponse != null && lastIncomingResponse.getSessionID() != sessionID) { myLogger.log(Logger.WARNING, myID+": Duplicated response from FE: type="+lastIncomingResponse.getType()+" info="+lastIncomingResponse.getInfo()+" SID="+lastIncomingResponse.getSessionID()); // Go back waiting lastIncomingResponse = null; continue; } break; } } catch (Exception e) {} return lastIncomingResponse; } private void notifyIncomingResponseReceived(JICPPacket rsp) { synchronized (BackEndDispatcher.this) { lastIncomingResponse = rsp; BackEndDispatcher.this.notifyAll(); } } } // END of inner class InputManager /** Inner class OutputManager This class manages the reception of commands and keep-alive packets from the FrontEnd. */ protected class OutputManager { private JICPPacket lastResponse; private int lastSid; private BackEndSkel mySkel; OutputManager(int n, BackEndSkel s) { lastSid = n; mySkel = s; } void shutdown() { } final JICPPacket handleCommand(JICPPacket cmd) throws ICPException { JICPPacket reply = null; byte sid = cmd.getSessionID(); if (sid == lastSid) { myLogger.log(Logger.WARNING,myID+": Duplicated packet from FE: pkt-type="+cmd.getType()+" info="+cmd.getInfo()+" SID="+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, JICPProtocol.DEFAULT_INFO, rspData); reply.setSessionID(sid); lastSid = sid; lastResponse = reply; } return reply; } final JICPPacket handleKeepAlive(JICPPacket command) throws ICPException { if(myLogger.isLoggable(Logger.FINEST)) { myLogger.log(Logger.FINEST,myID+": Keep-alive received"); } return new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, null); } } // END of inner class OutputManager private synchronized final void setExpirationDeadline() { expirationDeadline = System.currentTimeMillis() + maxDisconnectionTime; } private synchronized final boolean checkMaxDisconnectionTime(long currentTime) { return (!isConnected()) && (currentTime > expirationDeadline); } private final boolean 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"); } } return peerActive; } }