/***************************************************************** JADE - Java Agent DEvelopment Framework is a framework to develop multi-agent systems in compliance with the FIPA specifications. Copyright (C) 2000 CSELT S.p.A. GNU Lesser General Public License This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 2.1 of the License. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *****************************************************************/ package jade.imtp.leap.http; import jade.core.FEConnectionManager; import jade.core.FrontEnd; import jade.core.BackEnd; import jade.core.IMTPException; import jade.core.Specifier; import jade.core.TimerDispatcher; import jade.core.Timer; import jade.core.TimerListener; import jade.mtp.TransportAddress; import jade.imtp.leap.BackEndStub; import jade.imtp.leap.MicroSkeleton; import jade.imtp.leap.FrontEndSkel; import jade.imtp.leap.Dispatcher; import jade.imtp.leap.ICPException; import jade.imtp.leap.ConnectionListener; import jade.imtp.leap.JICP.*; import jade.util.leap.Properties; import jade.util.Logger; import java.util.Vector; import java.io.*; /*#MIDP_INCLUDE_BEGIN //import javax.microedition.io.*; #MIDP_INCLUDE_END*/ /** FrontEnd-side dispatcher class using JICP over HTTP as transport protocol @author Giovanni Caire - TILAB */ public class HTTPFEDispatcher implements FEConnectionManager, Dispatcher, TimerListener { private MicroSkeleton mySkel; private BackEndStub myStub; private Thread terminator; private DisconnectionManager myDisconnectionManager; private KeepAliveManager myKeepAliveManager; private InputManager myInputManager; private int outCnt; private boolean waitingForFlush = false; private long maxDisconnectionTime; private long keepAliveTime; private Properties props; private TransportAddress mediatorTA; private String myMediatorID; private String owner; private String beAddrsText; private String[] backEndAddresses; private ConnectionListener myConnectionListener; private Object connectorLock = new Object(); private boolean locked = false; private int verbosity = 1; private Logger myLogger = Logger.getMyLogger(getClass().getName()); protected String myMediatorClass = "jade.imtp.leap.http.HTTPBEDispatcher"; //////////////////////////////////////////////// // FEConnectionManager interface implementation //////////////////////////////////////////////// /** Create a BackEnd in the fixed network and return a stub to communicate with it */ public BackEnd getBackEnd(FrontEnd fe, Properties p) throws IMTPException { props = p; beAddrsText = props.getProperty(FrontEnd.REMOTE_BACK_END_ADDRESSES); backEndAddresses = parseBackEndAddresses(beAddrsText); // Host String host = props.getProperty("host"); if (host == null) { host = "localhost"; } // Port int port = JICPProtocol.DEFAULT_PORT; try { port = Integer.parseInt(props.getProperty("port")); } catch (NumberFormatException nfe) { // Use default } // Compose URL. Note that we build a JICPAddress just to avoid loading the HTTPAddress class. mediatorTA = JICPProtocol.getInstance().buildAddress(host, String.valueOf(port), null, null); // Mediator class String tmp = props.getProperty(JICPProtocol.MEDIATOR_CLASS_KEY); if (tmp != null) { myMediatorClass = tmp; }else{ // Set the default mediator class since this must be propagated to the mediator. props.setProperty(JICPProtocol.MEDIATOR_CLASS_KEY, myMediatorClass); } // Read re-connection retry time long retryTime = JICPProtocol.DEFAULT_RETRY_TIME; try { retryTime = Long.parseLong(props.getProperty(JICPProtocol.RECONNECTION_RETRY_TIME_KEY)); } catch (Exception e) { // Use default } // Read Max disconnection time maxDisconnectionTime = JICPProtocol.DEFAULT_MAX_DISCONNECTION_TIME; try { maxDisconnectionTime = Long.parseLong(props.getProperty(JICPProtocol.MAX_DISCONNECTION_TIME_KEY)); } catch (Exception e) { // Use default props.setProperty(JICPProtocol.MAX_DISCONNECTION_TIME_KEY, String.valueOf(maxDisconnectionTime)); } // Read Keep-alive time keepAliveTime = JICPProtocol.DEFAULT_KEEP_ALIVE_TIME; try { keepAliveTime = Long.parseLong(props.getProperty(JICPProtocol.KEEP_ALIVE_TIME_KEY)); } catch (Exception e) { // Use default props.setProperty(JICPProtocol.KEEP_ALIVE_TIME_KEY, String.valueOf(keepAliveTime)); } if (myLogger.isLoggable(Logger.CONFIG)) { myLogger.log(Logger.CONFIG, "Remote URL = http://"+host+":"+port); myLogger.log(Logger.CONFIG, "Mediator class = "+myMediatorClass); myLogger.log(Logger.CONFIG, "Reconnection retry time = "+retryTime); myLogger.log(Logger.CONFIG, "Max disconnection time = "+maxDisconnectionTime); myLogger.log(Logger.CONFIG, "Keep-alive time = "+keepAliveTime); } myDisconnectionManager = new DisconnectionManager(retryTime, maxDisconnectionTime); myKeepAliveManager = new KeepAliveManager(keepAliveTime); myInputManager = new InputManager(); // Read the owner if any owner = props.getProperty("owner"); // Create the BackEnd stub and the FrontEnd skeleton myStub = new BackEndStub(this); mySkel = new FrontEndSkel(fe); // Start the InputManager myInputManager.start(); // Create the remote BackEnd createBackEnd(); return myStub; } /** Make this HTTPFEDispatcher terminate. Note that when the BackEnd receives the termination notification (explicitly sent in case of a self-initiated shutdown or attached to the response to the EXIT command), it closes the input connection. The InputManager gets an exception and, since it has been killed, terminates. */ public void shutdown() { terminator = Thread.currentThread(); myLogger.log(Logger.INFO, "Dispatcher shutting down. Self-initiated = "+(terminator != myInputManager)); if (terminator != myInputManager) { // Self-initiated shut down // If connected, explicitly notify the BackEnd. if (myDisconnectionManager.isReachable()) { JICPPacket pkt = new JICPPacket(JICPProtocol.COMMAND_TYPE, (byte) (JICPProtocol.DEFAULT_INFO), null); myLogger.log(Logger.INFO, "Pushing termination notification"); try { deliver(pkt); } catch (IOException ioe) { // When the BackEnd receives the termination notification, // it just closes the connection --> we always have this exception myLogger.log(Logger.FINE, "BackEnd closed"); } } // Kill the InputManager myInputManager.kill(); } } /** Send the CREATE_MEDIATOR command with the necessary parameter in order to create the BackEnd in the fixed network. Executed - at bootstrap time by the thread that creates the FrontEndContainer. - To re-attach to the platform after a fault of the BackEnd */ private synchronized void createBackEnd() throws IMTPException { StringBuffer sb = BackEndStub.encodeCreateMediatorRequest(props); if (myMediatorID != null) { // This is a request to re-create my expired back-end BackEndStub.appendProp(sb, JICPProtocol.MEDIATOR_ID_KEY, myMediatorID); BackEndStub.appendProp(sb, "outcnt", String.valueOf(outCnt)); BackEndStub.appendProp(sb, "lastsid", String.valueOf(lastSid)); } JICPPacket pkt = new JICPPacket(JICPProtocol.CREATE_MEDIATOR_TYPE, JICPProtocol.DEFAULT_INFO, null, sb.toString().getBytes()); // Try first with the current transport address, then with the various backup addresses for(int i = -1; i < backEndAddresses.length; i++) { if(i >= 0) { // Set the mediator address to a new address.. String addr = backEndAddresses[i]; int colonPos = addr.indexOf(':'); String host = addr.substring(0, colonPos); String port = addr.substring(colonPos + 1, addr.length()); mediatorTA = new JICPAddress(host, port, myMediatorID, ""); } try { HTTPClientConnection hc = (HTTPClientConnection)getConnection(mediatorTA); myLogger.log(Logger.INFO, "Creating BackEnd on "+hc.getProtocol()+mediatorTA.getHost()+":"+mediatorTA.getPort()); pkt = deliver(pkt); String replyMsg = new String(pkt.getData()); if (pkt.getType() != JICPProtocol.ERROR_TYPE) { // BackEnd creation successful BackEndStub.parseCreateMediatorResponse(replyMsg, props); myMediatorID = props.getProperty(JICPProtocol.MEDIATOR_ID_KEY); // Complete the mediator address with the mediator ID mediatorTA = new JICPAddress(mediatorTA.getHost(), mediatorTA.getPort(), myMediatorID, null); myDisconnectionManager.setReachable(); myKeepAliveManager.update(); myLogger.log(Logger.INFO, "BackEnd OK. Mediator ID is "+myMediatorID); return; } else { myLogger.log(Logger.WARNING, "Mediator error: "+replyMsg); } } catch (IOException ioe) { // Ignore it, and try the next address... myLogger.log(Logger.WARNING, "Connection error", ioe); } } // No address succeeded! throw new IMTPException("Error creating the BackEnd."); } ////////////////////////////////////////////// // Dispatcher interface implementation ////////////////////////////////////////////// /** * Dispatch a serialized command to the BackEnd and get back a serialized response. * Mutual exclusion with itself to preserve dispatching order */ public synchronized byte[] dispatch(byte[] payload, boolean flush) throws ICPException { // Note that we don't even try to dispatch packets while the // device is not reachable to preserve dispatching order. // If dispatching succeeded in fact this command would overcome // any postponed command waiting to be flushed. if (myDisconnectionManager.isReachable()) { // The following check preserves dispatching order when the // device has just reconnected but flushing has not started yet if (waitingForFlush && !flush) { throw new ICPException("Upsetting dispatching order"); } waitingForFlush = false; int sid = outCnt; outCnt = (outCnt+1) & 0x0f; myLogger.log(Logger.FINE, "Issuing outgoing command "+sid); try { JICPPacket pkt = new JICPPacket(JICPProtocol.COMMAND_TYPE, JICPProtocol.DEFAULT_INFO, payload); pkt.setSessionID((byte) sid); pkt = deliver(pkt); myLogger.log(Logger.FINE, "Response received "+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())); } return pkt.getData(); } catch (IOException ioe) { // Can't reach the BackEnd. Assume we are unreachable myLogger.log(Logger.WARNING, "IOException on output connection", ioe); myDisconnectionManager.setUnreachable(false); throw new ICPException("Dispatching error.", ioe); } } else { throw new ICPException("Unreachable"); } } // These variables are only used within the InputManager class, // but are declared externally since they must "survive" when // an InputManager is replaced private JICPPacket lastResponse = null; private byte lastSid = 0x10; private int cnt = 0; /** Inner class InputManager This class deals with incoming commands (possibly keep-alive packets) */ private class InputManager extends Thread { private boolean active = true; private Connection myConnection = null; private int myId; public void run() { if (cnt == 0) { // Give precedence to the Thread that is creating the BackEnd Thread.yield(); // In the meanwhile load the ConnectionListener if any try { myConnectionListener = (ConnectionListener) Class.forName(props.getProperty("connection-listener")).newInstance(); } catch (Exception e) { // Just ignore it } } myId = cnt++; myLogger.log(Logger.INFO, "IM-"+myId+" started"); // Prepare an initial dummy response JICPPacket rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, null); while (active) { try { // Prepare the connection for next incoming command refreshConnection(); myDisconnectionManager.waitUntilReachable(); // Deliver the response to the previous incoming command and wait for the next one // If we are delivering the response to an exit command, set active to false to avoid an annoying stack trace // (due to the fact that the back-end will close the connection instead of sending further commands) and exit if (this == terminator) { active = false; } JICPPacket cmd = deliver(rsp, myConnection); myKeepAliveManager.update(); if (cmd.getType() == JICPProtocol.KEEP_ALIVE_TYPE) { // Keep-alive if (myLogger.isLoggable(Logger.FINER)) { myLogger.log(Logger.FINER, "Keep-alive received"); } rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.OK_INFO, null); } else { // Command byte sid = cmd.getSessionID(); if (sid == lastSid) { myLogger.log(Logger.WARNING, "Duplicated command received "+sid); rsp = lastResponse; } else { myLogger.log(Logger.FINE, "Incoming command received "+sid); byte[] rspData = mySkel.handleCommand(cmd.getData()); myLogger.log(Logger.FINE, "Incoming command served "+ sid); rsp = new JICPPacket(JICPProtocol.RESPONSE_TYPE, JICPProtocol.DEFAULT_INFO, rspData); rsp.setSessionID(sid); lastSid = sid; lastResponse = rsp; } } } catch (Exception e) { if (active) { myLogger.log(Logger.WARNING, "Exception on input connection", e); // Note that the boolean value passed to setUnreachable() only indicates if the unreachability was detected by a missing keep-alive myDisconnectionManager.setUnreachable(false); } } } myLogger.log(Logger.INFO, "IM-"+myId+" terminated"); } private synchronized void refreshConnection() throws IOException { if (active) { if (myConnection != null) { myConnection.close(); } myConnection = getConnection(mediatorTA); } else { // We were killed throw new IOException("Killed"); } } private synchronized void kill() { active = false; try { myConnection.close(); } catch (Exception e) {} myConnection = null; } } // END of inner class InputManager protected Connection getConnection(TransportAddress ta) { return new HTTPClientConnection(ta); } /** * Deliver a packet to the BackEnd and get back a response using a fresh one-shot connection */ private JICPPacket deliver(JICPPacket pkt) throws IOException { Connection c = getConnection(mediatorTA); try { return deliver(pkt, c); } finally { try { c.close(); } catch (Exception e) {} } } /** * Deliver a packet over a given connection and get back a response */ private JICPPacket deliver(JICPPacket pkt, Connection c) throws IOException { boolean lastPacket = false; if (Thread.currentThread() == terminator) { pkt.setTerminatedInfo(true); lastPacket = true; } pkt.setRecipientID(mediatorTA.getFile()); byte type = pkt.getType(); int status = 0; try { c.writePacket(pkt); status = 1; /*#MIDP_INCLUDE_BEGIN lock(); if (type == JICPProtocol.RESPONSE_TYPE) { TimerDispatcher.getTimerDispatcher().add(new Timer(System.currentTimeMillis()+5000, this)); } #MIDP_INCLUDE_END*/ pkt = c.readPacket(); status = 2; if (lastPacket && (pkt.getInfo() & JICPProtocol.TERMINATED_INFO) != 0) { // When we send a packet marked with the terminated-info, the back-end may either close // the connection (in this case we would have got an Exception) or reply with another // packet marked with the terminated-info --> throws an Exception to expose a uniform behaviour myLogger.log(Logger.INFO, "Termination notification ACK received"); throw new IOException("Terminated-info"); } return pkt; } catch (IOException ioe) { // Re-throw the exception adding the status throw new IOException(ioe.getMessage()+'['+status+']'); } finally { /*#MIDP_INCLUDE_BEGIN if (type != JICPProtocol.RESPONSE_TYPE) { // If we delivered a RESPONSE unlock() is already called by the TimerDispatcher unlock(); } #MIDP_INCLUDE_END*/ } } public void doTimeOut(Timer t) { unlock(); } private void lock() { synchronized (connectorLock) { while (locked) { try { connectorLock.wait(); } catch (Exception e) {} } locked = true; } } private void unlock() { synchronized (connectorLock) { locked = false; connectorLock.notifyAll(); } } /** * Inner class DisconnectionManager. * Manages issues related to disconnection of the device. */ class DisconnectionManager implements Runnable { private boolean reachable = false; private boolean pingOK = false; private Thread myThread; private long retryTime; private long maxDisconnectionTime; private DisconnectionManager(long retryTime, long maxDisconnectionTime) { this.retryTime = retryTime; this.maxDisconnectionTime = maxDisconnectionTime; } private synchronized final boolean isReachable() { return reachable; } /** * Set the reachability state as "unreachable" and starts * a separate thread that periodically ping the back-end to * detect when we are reachable again */ private synchronized void setUnreachable(boolean missingKA) { if (reachable) { if (missingKA || !pingOK) { if (myConnectionListener != null) { myConnectionListener.handleConnectionEvent(ConnectionListener.DISCONNECTED, null); } reachable = false; myLogger.log(Logger.INFO, "Starting DM ("+System.currentTimeMillis()+")."); myThread = new Thread(this); myThread.start(); if (pingOK) { // The InputManager is blocked waiting for data that will never arrive // Kill it and create a new one myInputManager.kill(); myInputManager = new InputManager(); myInputManager.start(); } } } } /** Set the reachability state as "reachable" and notify the InputManager thread in case it is waiting in waitUntilReachable(). */ private synchronized void setReachable() { reachable = true; if (myConnectionListener != null) { myConnectionListener.handleConnectionEvent(ConnectionListener.RECONNECTED, null); } notifyAll(); } /** * Wait until the device is reachable again. This is * executed by the InputManager thread before sending a response */ synchronized void waitUntilReachable() { while (!reachable) { try { wait(); } catch (InterruptedException ie) { } } pingOK = false; } /** Periodically ping (that is send a CONNECT_MEDIATOR packet) the BackEnd to detect when the device is reachable again. When the BackEnd receives a CONNECT_MEDIATOR packet it resets the input connection --> If blocked waiting for incoming commands, the InputManager thread should get an IOException. */ public void run() { int attemptCnt = 0; long startTime = System.currentTimeMillis(); try { while (!ping(attemptCnt)) { attemptCnt++; if ((System.currentTimeMillis() - startTime) > maxDisconnectionTime) { throw new ICPException("Max disconnection timeout expired"); } else { waitABit(retryTime); } } // Ping succeeded myLogger.log(Logger.INFO, "Reconnection ping OK."); synchronized (this) { pingOK = true; setReachable(); myKeepAliveManager.update(); // Activate postponed commands flushing waitingForFlush = myStub.flush(); } } catch (ICPException icpe) { // Impossible to reconnect to the BackEnd myLogger.log(Logger.SEVERE, "Impossible to reconnect to the BackEnd ("+System.currentTimeMillis()+")", icpe); if (myConnectionListener != null) { myConnectionListener.handleConnectionEvent(ConnectionListener.RECONNECTION_FAILURE, null); } } } private void waitABit(long time) { try { Thread.sleep(time); } catch (InterruptedException ie) { } } } // END of Inner class DisconnectionManager /** Inner class KeepAliveManager This class is responsible for taking track of keep-alive packets and detect problems when they miss. */ private class KeepAliveManager implements TimerListener { private long kaTimeout = -1; private Timer kaTimer; private KeepAliveManager(long keepAliveTime) { if (keepAliveTime > 0) { kaTimeout = keepAliveTime*2; } } public synchronized void doTimeOut(Timer t) { if (t == kaTimer) { // Missing keep-alive --> Try to reconnect myLogger.log(Logger.WARNING, "Missing Keep-alive"); myDisconnectionManager.setUnreachable(true); } } private synchronized void update() { if (kaTimeout > 0) { TimerDispatcher td = TimerDispatcher.getTimerDispatcher(); if (kaTimer != null) { td.remove(kaTimer); } kaTimer = td.add(new Timer(System.currentTimeMillis() + kaTimeout, this)); } } } // END of inner class KeepAliveManager /** * Send a CONNECT_MEDIATOR packet to the BackEnd to check if it is reachable */ private boolean ping(int cnt) throws ICPException { // Try first with the current transport address, then with the various backup addresses for(int i = -1; i < backEndAddresses.length; i++) { if(i >= 0) { // Set the mediator address to a new address.. String addr = backEndAddresses[i]; int colonPos = addr.indexOf(':'); String host = addr.substring(0, colonPos); String port = addr.substring(colonPos + 1, addr.length()); mediatorTA = new JICPAddress(host, port, myMediatorID, ""); } try { myLogger.log(Logger.FINE, "Ping "+mediatorTA.getHost()+":"+mediatorTA.getPort()+"("+cnt+")..."); JICPPacket pkt = new JICPPacket(JICPProtocol.CONNECT_MEDIATOR_TYPE, JICPProtocol.DEFAULT_INFO, null); pkt = deliver(pkt); if (pkt.getType() == JICPProtocol.ERROR_TYPE) { // Communication OK, but there was a JICP error. String errorMsg = new String(pkt.getData()); if (errorMsg.equals(JICPProtocol.NOT_FOUND_ERROR)) { // Back-end not found: either the max disconnection time expired server side or there was a fault and restart // --> Try to recreate the Back-end myLogger.log(Logger.WARNING, "Communication OK, but Back-end no longer present. Try to recreate it"); if (myConnectionListener != null) { myConnectionListener.handleConnectionEvent(ConnectionListener.BE_NOT_FOUND, null); } try { createBackEnd(); } catch (IMTPException imtpe) { myLogger.log(Logger.WARNING, "Error re-creating the Back-end."); return false; } } else { // Generic JICP error. No need to go on throw new ICPException("JICP error. "+errorMsg); } } return true; } catch (IOException ioe) { // Ignore it, and try the next address... myLogger.log(Logger.FINE, "Ping KO", ioe); } } // No address succeeded. return false; } private String[] parseBackEndAddresses(String addressesText) { Vector addrs = Specifier.parseList(addressesText, ';'); // Convert the list into an array of strings String[] result = new String[addrs.size()]; for(int i = 0; i < result.length; i++) { result[i] = (String)addrs.elementAt(i); } return result; } /*private String[] parseBackEndAddresses(String addressesText) { Vector addrs = new Vector(); if(addressesText != null && !addressesText.equals("")) { // Copy the string with the specifiers into an array of char char[] addressesChars = new char[addressesText.length()]; addressesText.getChars(0, addressesText.length(), addressesChars, 0); // Create the StringBuffer to hold the first address StringBuffer sbAddr = new StringBuffer(); int i = 0; while(i < addressesChars.length) { char c = addressesChars[i]; if((c != ',') && (c != ';') && (c != ' ') && (c != '\n') && (c != '\t')) { sbAddr.append(c); } else { // The address is terminated --> Add it to the result list String tmp = sbAddr.toString().trim(); if (tmp.length() > 0) { // Add the Address to the list addrs.addElement(tmp); } // Create the StringBuffer to hold the next specifier sbAddr = new StringBuffer(); } ++i; } // Handle the last specifier String tmp = sbAddr.toString().trim(); if(tmp.length() > 0) { // Add the Address to the list addrs.addElement(tmp); } } // Convert the list into an array of strings String[] result = new String[addrs.size()]; for(int i = 0; i < result.length; i++) { result[i] = (String)addrs.elementAt(i); } return result; }*/ }