package context.arch.enactor.server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.ServerSocket; import java.net.Socket; import java.util.logging.Level; import java.util.logging.Logger; import org.jdom.Document; import org.jdom.input.SAXBuilder; import context.arch.comm.DataObject; import context.arch.comm.DataObjects; import context.arch.enactor.Enactor; import context.arch.enactor.EnactorComponentInfo; import context.arch.enactor.EnactorListener; import context.arch.enactor.EnactorParameter; import context.arch.enactor.EnactorReference; import context.arch.storage.Attribute; import context.arch.storage.Attributes; /** * An XML server that tunnels all enactor events through sockets to clients, using XML * to translate DataObjects to a language-independent serialized format. As client connections * are created, each client becomes an EnactorListener to the Enactor passed into the server * constructor. In this sense a server represents a single enactor. Different enactors use * different servers and so have different addresses and ports. The specific protocol used * for XML is compatible with the Macromedia Flash XMLSocket. Also, the EnactorXMLServer * responds with HTML information about the Enactor when it receives HTTP GET requests. So, * the one server on the single port acts as both an XML server and as a simple HTTP server. * * @author newbergr */ public class EnactorXMLServer implements Runnable { private static final Logger LOGGER = Logger.getLogger(EnactorXMLServer.class.getName()); static {LOGGER.setLevel(Level.WARNING);} // this should be set in a configuration file? public EnactorXMLServer(Enactor e, int port) { this.port = port; this.enactor = e; start(); } public Enactor getEnactor() { return enactor; } /** * Begins the XML Server by started a thread that opens a server and * listens for external connections. */ public void start() { if (serverThread == null) { serverThread = new Thread(this); serverThread.start(); } } /** * Closes server and stops its thread */ public void stop() { //this causes the while loop in the run() method below to break. stop = true; } /** * when the thread is started, the server begins to listen on its port and * continually accepts new clients. */ public void run() { LOGGER.info("Starting SituationXMLServer on port " + port); try { server = new ServerSocket(port); while(!stop) { Socket s = server.accept(); //avoid buffering since our messages are likely to be short s.setTcpNoDelay(true); LOGGER.info("Accepted new connection, creating client"); Client c = null; try { c = new Client(s); c.start(); } catch (IOException client_ioe) { LOGGER.log(Level.SEVERE, "IOException when creating EnactorXMLServer client", client_ioe); } } } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "IOException when running EnactorXMLServer", ioe); } finally { finish(); } } private void finish() { LOGGER.info("closing server"); try { if (server!= null) server.close(); //reset stop boolean stop = false; } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "IOException when finishing EnactorXMLServer", ioe); } } /** * takes a set parameter message sent by a client and sets the appropriate enactor parameter. */ protected void handleSetParameter(DataObject dobject) { String paramName = (String) dobject.getDataObjectFirstValue("parameterName"); EnactorParameter rp = enactor.getParameter(paramName); if (rp != null) { Attributes atts = Attributes.fromDataObject(dobject.getDataObject(Attributes.ATTRIBUTES)); Object value = dobject.getDataObjectFirstValue("parameterValue"); rp.setValue(atts, value); } } /** * This should at some point programmatically build an XML document that is then * formatted in the browser via XSL. For now, just returns HTML. * * TODO: improve data dictionary * * @return String HTML-formatted data dictionary */ protected String getDataDictionary() { StringBuffer strBuf = new StringBuffer(); strBuf.append("<HTML><HEAD><TITLE>Data Dictionary</TITLE></HEAD><BODY>"); strBuf.append("<h1>Data Dictionary</h1>"); strBuf.append("<h3>Parameters</h3><table border=1 cellpadding=3><tr><th>name</th><th>description</th><th>attributes</th></tr>"); for (EnactorParameter rp : enactor.getParameters()) { strBuf.append("<tr><td>").append(rp.getName()).append("</td><td>").append(rp.getDescription()).append("</td><td>"); Attributes attsTemplate = rp.getAttributesTemplate(); for (Attribute<?> att : attsTemplate.values()) { strBuf.append(att.getName()); strBuf.append(";"); } strBuf.append("</td></tr>"); } strBuf.append("</table>"); strBuf.append("<h3>References</h3><table border=1 cellpadding=3><tr><th>no.</th><th>descriptionQuery</th></tr>"); int erCount = 0; for (EnactorReference rr : enactor.getReferences()) { erCount++; strBuf.append("<tr><td>").append(erCount).append("</td><td>").append(rr.getConditionQuery()).append("</td></tr>"); } strBuf.append("</table>"); strBuf.append("</BODY></HTML>"); return strBuf.toString(); } /** * This class handles client activity. Each client is a thread that monitors its * socket for incoming data, and actively scans it. It handles both continuous XML * input delimitied by null characters (as per the Flash XMLSocket protocol), and * one-time HTTP GET requests, for which it returns HTML data dictionaries and closes. * * @author newbergr */ class Client extends Thread implements EnactorListener { Client(Socket sock) throws IOException { socket = sock; try { in = new BufferedReader(new InputStreamReader(sock.getInputStream())); out = new OutputStreamWriter(sock.getOutputStream()); } catch (IOException ioe) { finish(); throw ioe; } } public void run() { enactor.addListener(this); try { char[] buf = new char[1024]; StringBuffer strBuf = new StringBuffer(); while(true) { //this call to in.ready() should throw IOException when in is closed, //causing the thread to exit. if (in.ready()) { synchronized (this) { int amt = in.read(buf); //scan for '0' terminating character, possible multiple occurrence int marker = 0; for(int i=0; i<amt; i++) { if (buf[i] == (char) 0) { strBuf.append(buf, marker, i-marker); handleMessage(strBuf.toString()); strBuf.setLength(0); marker = i+1; } } if(marker < amt) { strBuf.append(buf, marker, amt-marker); } //check if this is an HTTP GET request, if so send dictionary response String str = strBuf.toString(); if (str.startsWith("GET") && str.indexOf("\r\n\r\n") > 0) { sendHTMLDataDictionary(); in.close(); } } } Thread.sleep(20); } } catch (Exception e) { } finally { finish(); } LOGGER.info("Client thread finishing"); } /** * parses the XML message and converts it to a DataObject. currently only handles * the setting of enactor parameters. * * @param xmlString the XML formatted message sent by a client */ private void handleMessage(String xmlString) { //we don't want any exceptions from handling to affect receiving other //messages on this thread. try { Document doc = saxBuilder.build(new StringReader(xmlString)); DataObject dataobject = XMLDataObjectTranslator.fromXML(doc); if (dataobject != null) { LOGGER.info("DataObject parsed with name " + dataobject.getName()); if ("setParameter".equals(dataobject.getName())) { handleSetParameter(dataobject); } } else { LOGGER.warning("XML received that was not a proper dataobject"); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "received XML could not be parsed or handled:\n" + xmlString, e); } } /** * sends XML data down to a client, followed by a 0 byte as * per the Flash XMLSocket protocol. * * @param xml the data to send down to the client */ public synchronized void sendXML(String xml) { if (xml == null) return; try { out.write( xml ); out.write( (byte)0 ); out.flush(); } catch (IOException ioe) { LOGGER.info("IOException while sending XML, closing client"); finish(); } } /** * sends the data dictionary for the enactor, and instructs the * HTTP client that we will close its connection. * * @throws IOException */ private void sendHTMLDataDictionary() throws IOException { out.write("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"); String datadict = getDataDictionary(); out.write(datadict); out.flush(); finish(); } public void finish() { LOGGER.info("closing client"); enactor.removeListener(this); try { in.close(); out.close(); socket.close(); } catch (IOException ioe) { LOGGER.log(Level.SEVERE,"error while finishing client",ioe); } } /** * converts a data object into XML and sends it to all clients. * * @param result DataObject containing information for client receipt */ public void sendDataObject(DataObject result) { String xmlResult = XMLDataObjectTranslator.toXML(result); sendXML(xmlResult); } ///////////////////////// // Begin Listener methods ///////////////////////// public void componentEvaluated(EnactorComponentInfo eci) { DataObjects v = new DataObjects(); v.add(eci.getCurrentState().toDataObject()); sendDataObject(new DataObject(EnactorListener.COMPONENT_EVALUATED, v)); } public void componentAdded(EnactorComponentInfo eci, Attributes paramAtts) { DataObjects v = new DataObjects(); v.add(eci.getCurrentState().toDataObject()); if (paramAtts != null) { DataObjects subv = new DataObjects(); subv.add(paramAtts.toDataObject()); v.add(new DataObject("parameters",subv)); } sendDataObject(new DataObject(EnactorListener.COMPONENT_ADDED,v)); } public void componentRemoved(EnactorComponentInfo eci, Attributes paramAtts) { DataObjects v = new DataObjects(); v.add(eci.getCurrentState().toDataObject()); if (paramAtts != null) { DataObjects subv = new DataObjects(); subv.add(paramAtts.toDataObject()); v.add(new DataObject("parameters",subv)); } sendDataObject(new DataObject(EnactorListener.COMPONENT_REMOVED,v)); } public void parameterValueChanged(EnactorParameter parameter, Attributes paramAtts, Object value) { DataObjects v = new DataObjects(); if (paramAtts != null) { v.add(paramAtts.toDataObject()); } v.add(new DataObject(EnactorListener.PARAMETER_VALUE_CHANGED+"Name",parameter.getName())); v.add(new DataObject(EnactorListener.PARAMETER_VALUE_CHANGED+"Value",String.valueOf(value))); sendDataObject(new DataObject(EnactorListener.PARAMETER_VALUE_CHANGED,v)); } public void serviceExecuted(EnactorComponentInfo eci, String serviceName, String functionName, Attributes input, DataObject returnDataObject) { //TODO: make data object to hold serviceExecuted information and send to Client } /////////////////////// // End Listener methods /////////////////////// private Socket socket; private BufferedReader in; private OutputStreamWriter out; private SAXBuilder saxBuilder = new SAXBuilder(); } private int port; private ServerSocket server; private Thread serverThread; private Enactor enactor; private boolean stop = false; }