Hatch as Native Messaging handler WIP
authorBill Erickson <berickxx@gmail.com>
Tue, 8 Nov 2016 17:12:30 +0000 (12:12 -0500)
committerBill Erickson <berickxx@gmail.com>
Tue, 8 Nov 2016 17:12:30 +0000 (12:12 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
README
hatch.xml [deleted file]
run.sh
src/org/evergreen_ils/hatch/FileIO.java
src/org/evergreen_ils/hatch/Hatch.java
src/org/evergreen_ils/hatch/HatchWebSocketHandler.java [deleted file]
src/org/evergreen_ils/hatch/HatchWebSocketServlet.java [deleted file]
src/org/evergreen_ils/hatch/MessageIO.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/PrintManager.java
src/org/evergreen_ils/hatch/RequestHandler.java [new file with mode: 0644]

diff --git a/README b/README
index 7c06daa..72a3f08 100644 (file)
--- a/README
+++ b/README
@@ -2,26 +2,14 @@ Hatch - Java Print / Storage / Etc Service
 
 ** ROUGH SETUP NOTES **
 
-Install Hatch on your desktop -- Linux edition: 
-
-% wget http://download.eclipse.org/jetty/stable-9/dist/jetty-distribution-9.2.5.v20141112.tar.gz 
-% tar -zxf jetty-distribution-9.2.5.v20141112.tar.gz 
-% ln -s jetty-distribution-9.2.5.v20141112 jetty
-
 # download jdk1.8 (requires license agreement) -- haven't tested on openjdk yet. 
 # http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
 # and extract in the same directory
 % ln -s jdk1.8.0_25 jdk1.8
 
 % mkdir lib
-% wget -O lib/jetty-util-ajax-9.2.5.v20141112.jar \
-    'http://central.maven.org/maven2/org/eclipse/jetty/jetty-util-ajax/9.2.5.v20141112/jetty-util-ajax-9.2.5.v20141112.jar'
-
-# create an SSL certificat for jetty
-# if you use a password other than "password", modify references to 
-# "password" in hath.xml (in the top directory).
-% cd jetty/etc/
-% ../../jdk1.8/bin/keytool -keystore keystore -alias jetty -genkey -keyalg RSA
+Download org.json jar from and place into lib (modify run.sh CP)
+https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.json%22%20AND%20a%3A%22json%22
 
 # compile
 % ./run.sh
@@ -29,7 +17,3 @@ Install Hatch on your desktop -- Linux edition:
 # compile + run
 % ./run.sh 1
 
-# open https://localhost:8443/ in Chrome and click through the security warning.
-# Then open the browser client.
-# Set "This workstation uses a remote print / storage service ("Hatch")?" under Admin -> Workstation
-# optionally configure / test printing
diff --git a/hatch.xml b/hatch.xml
deleted file mode 100644 (file)
index 83f5bf6..0000000
--- a/hatch.xml
+++ /dev/null
@@ -1,144 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
-
-<Configure id="Server" class="org.eclipse.jetty.server.Server">
-
-  <!--
-  <Get id="Logger" class="org.eclipse.jetty.util.log.Log" name="log"/>
-  <Ref id="Logger">
-    <Set name="debugEnabled">true</Set>
-  </Ref>
-  -->
-
-  <Set class="org.evergreen_ils.hatch.HatchWebSocketHandler" name="trustedDomains">
-    <Array type="String">
-        <!-- 
-        List of origin domains which are allowed to connect to Hatch.
-        If the first item in the list is "*", then all domains are 
-        trusted, which is useful for testing.
-        -->
-        <Item>*</Item>
-    </Array>
-  </Set>
-
-  <!--
-  <Set class="org.evergreen_ils.hatch.HatchWebSocketHandler" 
-    name="profileDirectory"></Set>
-  -->
-
-  <!-- basic HTTP setup -->
-  <New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">   
-    <Set name="secureScheme">https</Set>                                     
-    <Set name="securePort"><Property name="jetty.secure.port" default="8443" /></Set>
-    <Set name="outputBufferSize"><Property name="jetty.output.buffer.size" default="32768" /></Set>
-    <Set name="requestHeaderSize"><Property name="jetty.request.header.size" default="8192" /></Set>
-    <Set name="responseHeaderSize"><Property name="jetty.response.header.size" default="8192" /></Set>
-    <Set name="sendServerVersion"><Property name="jetty.send.server.version" default="true" /></Set>
-    <Set name="sendDateHeader"><Property name="jetty.send.date.header" default="false" /></Set>
-    <Set name="headerCacheSize">512</Set>                                    
-  </New>
-
-  <!-- SSL configuration -->
-  <!-- Using the stock Jetty certificates for now.  
-       To set a temporary trust on the cert, navigate to 
-       https://<hostname>:8443/ and confirm the cert is trusted -->
-  <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
-    <!-- TODO: make this better -->
-    <Set name="KeyStorePath"><Property name="jetty.home" default="." />/jetty/etc/keystore</Set>
-    <Set name="KeyStorePassword">password</Set>
-    <Set name="KeyManagerPassword">password</Set>
-    <Set name="TrustStorePath"><Property name="jetty.home" default="." />/jetty/etc/keystore</Set>
-    <Set name="TrustStorePassword">password</Set>
-  </New> 
-
-  <New id="sslHttpConfig" class="org.eclipse.jetty.server.HttpConfiguration">  
-    <Arg><Ref refid="httpConfig"/></Arg>                                       
-    <Call name="addCustomizer">                                                
-      <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
-    </Call>                                                                    
-  </New>                                                                       
-
-  <!-- SSL HTTP connector -->
-  <Call name="addConnector">                               
-    <Arg>                                                                      
-      <New class="org.eclipse.jetty.server.ServerConnector">                   
-        <Arg name="server"><Ref refid="Server" /></Arg>                        
-          <Arg name="factories">                                               
-            <Array type="org.eclipse.jetty.server.ConnectionFactory">          
-              <Item>                                                           
-                <New class="org.eclipse.jetty.server.SslConnectionFactory">    
-                  <Arg name="next">http/1.1</Arg>                              
-                  <Arg name="sslContextFactory"><Ref refid="sslContextFactory"/></Arg>
-                </New>                                                         
-              </Item>                                                          
-              <Item>                                                           
-                <New class="org.eclipse.jetty.server.HttpConnectionFactory">   
-                  <Arg name="config"><Ref refid="sslHttpConfig"/></Arg>        
-                </New>                                                         
-              </Item>                                                          
-            </Array>                                                           
-          </Arg>                                                               
-          <Set name="host"><Property name="jetty.host" /></Set>                
-          <Set name="port"><Property name="jetty.secure.port" default="8443" /></Set>  
-          <Set name="idleTimeout"><Property name="https.timeout" default="30000"/></Set>
-          <Set name="soLingerTime"><Property name="https.soLingerTime" default="-1"/></Set>
-      </New>                                                                 
-    </Arg>                                                                     
-  </Call> 
-
-  <!-- HTTP connector -->
-  <Call name="addConnector">
-    <Arg>
-      <New class="org.eclipse.jetty.server.ServerConnector">
-        <Arg name="server">
-          <Ref refid="Server"/>
-        </Arg>
-        <Arg name="factories">
-          <Array type="org.eclipse.jetty.server.ConnectionFactory">
-            <Item>
-              <New class="org.eclipse.jetty.server.HttpConnectionFactory">
-                <Arg name="config"><Ref refid="httpConfig" /></Arg> 
-              </New>
-            </Item>
-          </Array>
-        </Arg>
-        <Set name="host"><Property name="jetty.host"/></Set>
-        <Set name="port"><Property name="jetty.port" default="8080"/></Set>
-        <Set name="idleTimeout"><Property name="http.timeout" default="30000"/></Set>
-        <Set name="soLingerTime"><Property name="http.soLingerTime" default="-1"/></Set>
-      </New>
-    </Arg>
-  </Call>
-
-  <!-- TODO get properties working for:
-      jetty.proxy.maxThreads
-      jetty.proxy.maxConnections
-      jetty.proxy.idleTimeout
-      jetty.proxy.timeout
-  -->
-
-
-  <!-- wrap our websocketservlet into something the server can run -->
-  <New id="context" class="org.eclipse.jetty.servlet.ServletContextHandler">
-    <Set name="contextPath">/</Set>
-    <Call name="addServlet">
-      <Arg>org.evergreen_ils.hatch.HatchWebSocketServlet</Arg>
-      <Arg>/hatch</Arg>
-    </Call>
-  </New>
-
-  <!-- set our websocket handler as the server handler -->
-  <Set name="handler">
-    <New class="org.eclipse.jetty.server.handler.HandlerCollection">
-      <Set name="handlers">
-        <Array type="org.eclipse.jetty.server.Handler">
-        <Item> <Ref refid="context" /> </Item>
-        <Item>
-          <New class="org.eclipse.jetty.server.handler.DefaultHandler" />
-        </Item>
-        </Array>
-      </Set>
-    </New>
-  </Set>
-
-</Configure>
diff --git a/run.sh b/run.sh
index 91d2dca..3400107 100755 (executable)
--- a/run.sh
+++ b/run.sh
@@ -1,15 +1,10 @@
 JAVA_HOME=jdk1.8
-JETTY_HOME=jetty
+CP=lib:lib/json-20160810.jar
 
 # compile
-$JAVA_HOME/bin/javac \
-    -cp "$JETTY_HOME/lib/*:$JETTY_HOME/lib/websocket/*:lib/*" \
-    -Xdiags:verbose -d lib \
-    src/org/evergreen_ils/hatch/*.java
+$JAVA_HOME/bin/javac -cp $CP -d lib src/org/evergreen_ils/hatch/*.java
 
 [ -z "$1" ] && exit;
 
 # run
-$JAVA_HOME/bin/java \
-    -cp "$JETTY_HOME/lib/*:$JETTY_HOME/lib/websocket/*:lib/*:lib" \
-    org.evergreen_ils.hatch.Hatch
+$JAVA_HOME/bin/java -cp $CP org.evergreen_ils.hatch.Hatch
index 0aa2fc7..8813b7c 100644 (file)
@@ -18,8 +18,6 @@ package org.evergreen_ils.hatch;
 import java.io.*;
 import java.util.LinkedList;
 import java.util.Arrays;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
 
 public class FileIO {
 
@@ -60,7 +58,7 @@ public class FileIO {
     // -------------------------------------------------- 
 
     // logger
-    private static final Logger logger = Log.getLogger("FileIO");
+    //private static final Logger logger = Log.getLogger("FileIO");
 
     /**
      * Constructs a new FileIO with the provided base path.
@@ -84,7 +82,7 @@ public class FileIO {
         File dir = new File(basePath);
         if (!dir.exists()) {
             if (!dir.mkdir()) {
-                logger.info("Unable to create directory: " + dir.getName());
+                //logger.info("Unable to create directory: " + dir.getName());
                 return null;
             }
         }
@@ -93,12 +91,12 @@ public class FileIO {
         File subDir = new File(basePath, originDomain);
         if (!subDir.exists()) {
             if (!subDir.mkdir()) {
-                logger.info("Unable to create directory: " + subDir.getName());
+                //logger.info("Unable to create directory: " + subDir.getName());
                 return null;
             }
         }
 
-        logger.info("baseDir: " + subDir.getName());
+        //logger.info("baseDir: " + subDir.getName());
         return subDir;
     }
 
@@ -124,7 +122,7 @@ public class FileIO {
      * @return success or failure
      */
     public boolean set(String key, String text) {
-        logger.info("set => " + key);
+        //logger.info("set => " + key);
         File file = getFile(key);
 
         if (text == null) return false;
@@ -133,8 +131,8 @@ public class FileIO {
 
             // delete the file if it exists
             if (!file.exists() && !file.createNewFile()) {
-                logger.info(
-                    "Unable to create file: " + file.getCanonicalPath());
+                //logger.info(
+                    //"Unable to create file: " + file.getCanonicalPath());
                 return false;
             }
 
@@ -146,8 +144,8 @@ public class FileIO {
             outStream.close();
 
         } catch(IOException e) {
-            logger.warn("Error calling set() with key " + key);
-            logger.warn(e);
+            //logger.warn("Error calling set() with key " + key);
+            //logger.warn(e);
             return false;
         }
 
@@ -164,15 +162,15 @@ public class FileIO {
      * @return success or failure
      */
     public boolean append(String key, String text) {
-        logger.info("append => " + key);
+        //logger.info("append => " + key);
         File file = getFile(key);
 
         try {
 
             // create the file if it doesn's already exist
             if (!file.exists() && !file.createNewFile()) {
-                logger.info(
-                    "Unable to create file: " + file.getCanonicalPath());
+                //logger.info(
+                    //"Unable to create file: " + file.getCanonicalPath());
                 return false;
             }
 
@@ -183,8 +181,8 @@ public class FileIO {
             outStream.close();
 
         } catch(IOException e) {
-            logger.warn("Error in append() with key " + key);
-            logger.warn(e);
+            //logger.warn("Error in append() with key " + key);
+            //logger.warn(e);
             return false;
         }
 
@@ -198,7 +196,7 @@ public class FileIO {
      * @return The text content of the file
      */
     public String get(String key) {
-        logger.info("get => " + key);
+        //logger.info("get => " + key);
         File file = getFile(key);
         if (!file.exists()) return null;
 
@@ -213,8 +211,8 @@ public class FileIO {
                 buf.append(line);
             }
         } catch (IOException e) {
-            logger.warn("Error reading key: " + key);
-            logger.warn(e);
+            //logger.warn("Error reading key: " + key);
+            //logger.warn(e);
             return null;
         }
 
@@ -228,20 +226,20 @@ public class FileIO {
      * @return success or failure
      */
     public boolean remove(String key) {
-        logger.info("remove => " + key);
+        //logger.info("remove => " + key);
         File file = getFile(key);
-        try {
+        //try {
             if (file.exists() && !file.delete()) {
-                logger.info(
-                    "Unable to delete file: " + file.getCanonicalPath());
+                //logger.info(
+                    //"Unable to delete file: " + file.getCanonicalPath());
                 return false;
             }
             return true;
-        } catch (IOException e) {
-            logger.warn("Error deleting key: " + key);
-            logger.warn(e);
-            return false;
-        }
+        //} catch (IOException e) {
+            //logger.warn("Error deleting key: " + key);
+            //logger.warn(e);
+            //return false;
+        //}
     }
 
     /**
@@ -261,7 +259,7 @@ public class FileIO {
      * @return Array of keys
      */
     public String[] keys(String prefix) {
-        logger.info("keys => " + prefix);
+        //logger.info("keys => " + prefix);
 
         File dir = baseDir();
         if (dir == null || !dir.exists()) 
index 0bed93f..05400ab 100644 (file)
  */
 package org.evergreen_ils.hatch;
 
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.servlet.ServletHandler;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.server.handler.HandlerList;
-import org.eclipse.jetty.util.resource.Resource;
-import org.eclipse.jetty.xml.XmlConfiguration;
+import java.util.Map;
+import java.util.logging.*;
+import org.json.*;
 
 import javafx.application.Application;
 import javafx.application.Platform;
@@ -38,41 +25,16 @@ import javafx.scene.Scene;
 import javafx.scene.layout.Region;
 import javafx.scene.web.WebEngine;
 import javafx.scene.web.WebView;
-import javafx.scene.Node;
 import javafx.stage.Stage;
-import javafx.scene.transform.Scale;
 import javafx.beans.value.ChangeListener;
-import javafx.concurrent.Worker;
 import javafx.concurrent.Worker.State;
-import javafx.concurrent.Service;
-import javafx.concurrent.Task;
-import javafx.event.EventHandler;
-import javafx.concurrent.WorkerStateEvent;
 import java.util.concurrent.LinkedBlockingQueue;
 
-import org.eclipse.jetty.util.ajax.JSON;
-
-import java.util.Map;
-
-import java.io.FileInputStream;
 
 /**
  * Main class for Hatch.
  *
- * This class operates as a two-headed beast, whose heads will occasionally
- * communicate with each other.
- *
- * It runs a JavaFX thread for printing HTML documents and runs a Jetty
- * server thread for handling communication with external clients.
- *
- * Most of the work performed happens solely in the Jetty server thread.
- * Attempts to print, however, are passed into the JavaFX thread so that
- * the HTML may be loaded into a WebView for printing, which must happen
- * within the JavaFX thread.
- *
- * Messages are passed from the Jetty thread to the JavaFX thread via a
- * blocking thread queue, observed by a separate Service thread, whose 
- * job is only to pull messages from the queue.
+ * TODO
  *
  * Beware: On Mac OS, the "FX Application Thread" is renamed to 
  * "AppKit Thread" when the first call to print() or showPrintDialog() 
@@ -88,15 +50,11 @@ public class Hatch extends Application {
     private Stage primaryStage;
     
     /** Our logger instance */
-    static final Logger logger = Log.getLogger("Hatch");
-
-    /** Message queue for passing messages from the Jetty thread into
-     * the JavaFX Application thread */
-    private static LinkedBlockingQueue<Map> requestQueue =
-        new LinkedBlockingQueue<Map>();
+    static Logger logger;
 
+    
     /**
-     * Printable region containing a browser
+     * Printable region containing a browser.
      */
     class BrowserView extends Region {
         WebView webView = new WebView();
@@ -107,47 +65,11 @@ public class Hatch extends Application {
     }
 
     /**
-     * Service task which listens for inbound messages from the
-     * servlet.
-     *
-     * The code blocks on the concurrent queue, so it must be
-     * run in a separate thread to avoid locking the main FX thread.
-     */
-    private static class MsgListenService extends Service<Map<String,Object>> {
-        protected Task<Map<String,Object>> createTask() {
-            return new Task<Map<String,Object>>() {
-                protected Map<String,Object> call() {
-                    while (true) {
-                        logger.info("MsgListenService waiting for a message...");
-                        try {
-                            // take() blocks until a message is available
-                            return requestQueue.take();
-                        } catch (InterruptedException e) {
-                            // interrupted, go back and listen
-                            continue;
-                        }
-                    }
-                }
-            };
-        }
-    }
-
-
-    /**
      * JavaFX startup call
      */
     @Override
     public void start(Stage primaryStage) {
         this.primaryStage = primaryStage;
-        startMsgTask();
-    }
-
-    /**
-     * Queues a message for processing by the queue processing thread.
-     */
-    public static void enqueueMessage(Map<String,Object> params) {
-        logger.debug("queueing print message");
-        requestQueue.offer(params);
     }
 
     /**
@@ -159,7 +81,7 @@ public class Hatch extends Application {
         String contentType = (String) params.get("contentType");
 
         if (content == null) {
-            logger.warn("handlePrint() called with no content");
+            logger.warning("handlePrint() called with no content");
             return;
         }
 
@@ -185,71 +107,136 @@ public class Hatch extends Application {
 
         logger.info("printing " + content.length() + " bytes of " + contentType);
         browser.webEngine.loadContent(content, contentType);
-
-        // After queueing up the HTML for printing, go back to listening
-        // for new messages.
-        startMsgTask();
     }
 
-    /**
-     * Fire off the Service task, which checks for queued messages.
-     *
-     * When a queued message is found, it's sent off for printing.
+    /* TODO: make me configurable via config file
      */
-    public void startMsgTask() {
+    public static Logger getLogger() {
+        if (logger != null) return logger;
 
-        MsgListenService service = new MsgListenService();
+        logger = Logger.getLogger("org.evergreen_ils.hatch");
+        logger.setLevel(Level.ALL);
 
-        logger.info("starting MsgTask");
+        ConsoleHandler handler = new ConsoleHandler();
+        handler.setLevel(Level.ALL);
+        handler.setFormatter(new SimpleFormatter());
+        logger.addHandler(handler);
 
-        service.setOnSucceeded(
-            new EventHandler<WorkerStateEvent>() {
+        logger.setUseParentHandlers(false);
 
-            @Override
-            public void handle(WorkerStateEvent t) {
-                logger.info("MsgTask handling message.. ");
-                Map<String,Object> message = 
-                    (Map<String,Object>) t.getSource().getValue();
+        return logger;
+    }
 
-                // avoid nesting UI event loops by kicking off the print
-                // operation from the main FX loop after this event handler 
-                // has exited.
-                Platform.runLater(
-                    new Runnable() {
-                        @Override public void run() {
-                            handlePrint(message);
-                        }
+    /*
+    public void onMessage(String message) {
+        if (session == null || !session.isOpen()) return;
+        logger.info("onMessage() " + message);
+
+        HashMap<String,Object> params = null;
+
+        try {
+            params = (HashMap<String,Object>) JSON.parse(message);
+        } catch (ClassCastException e) {
+            reply("Invalid WebSockets JSON message " + message, 
+                new Long(-1), false);
+        }
+
+        Long msgid = (Long) params.get("msgid");
+        String action = (String) params.get("action");
+        String key = (String) params.get("key");
+        String value = (String) params.get("value");
+        String mime = (String) params.get("mime");
+
+        logger.info("Received request for action " + action);
+
+        // all requets require a message ID
+        if (msgid == null) {
+            reply("No msgid specified in request", msgid, false);
+            return;
+        }
+
+        // all requests require an action
+        if (action == null || action.equals("")) {
+            reply("No action specified in request", msgid, false);
+            return;
+        }
+
+        Object response = null;
+        boolean error = false;
+        FileIO io = new FileIO(profileDirectory, origin);
+
+        switch (action) {
+            case "keys":
+                response = io.keys(key);
+                break;
+
+            case "printers":
+                response = new PrintManager().getPrintersAsMaps();
+                break;
+
+            case "print":
+                // pass ourselves off to the print handler so it can reply
+                // for us after printing has completed.
+                params.put("socket", this);
+                Hatch.enqueueMessage(params);
+
+                // we don't want to return a response below, since the 
+                // FX thread will handle that for us.
+                return;
+
+            case "print-config":
+                try {
+                    response = new PrintManager().configurePrinter(params);
+                } catch(IllegalArgumentException e) {
+                    response = e.toString();
+                    error = true;
+                }
+                break;
+
+            case "get":
+                String val = io.get(key);
+                if (val != null) {
+                    // set() stores bare JSON. We must pass an 
+                    // Object to reply so that it may be embedded into
+                    // a larger JSON response object, hence the JSON.parse().
+                    try {
+                        response = JSON.parse(val);
+                    } catch(java.lang.IllegalStateException e) {
+                        error = true;
+                        response = "Error JSON-parsing stored value " + val;
                     }
-                );
-            }
-        });
+                }
+                break;
 
-        service.start();
-    }
+            case "remove":
+                response = io.remove(key);
+                break;
 
-    /**
-     * Hatch main.
-     *
-     * Reads the Jetty configuration, starts the Jetty server thread, 
-     * then launches the JavaFX Application thread.
-     */
-    public static void main(String[] args) throws Exception {
+            case "set" :
+                response = io.set(key, value);
+                break;
 
-        // build a server from our hatch.xml configuration file
-        XmlConfiguration configuration =
-            new XmlConfiguration(new FileInputStream("hatch.xml"));
+            case "append" :
+                response = io.append(key, value);
+                break;
 
-        Server server = (Server) configuration.configure();
+            default:
+                response = "No such action: " + action;
+                error = true;
+        }
 
-        logger.info("Starting Jetty server");
+        reply(response, msgid, !error);
+    }
+    */
 
-        // start our server, but do not join(), since we want to server
-        // to continue running in its own thread
-        server.start();
 
-        logger.info("Launching FX Application");
 
-        // launch the FX Application thread
-        launch(args);
+    /**
+     * Hatch main.
+     *
+     */
+    public static void main(String[] args) throws Exception {
+        new RequestHandler().start();
+        launch(args);   // launch the FX Application thread
     }
 }
diff --git a/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java b/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java
deleted file mode 100644 (file)
index bb2ee9c..0000000
+++ /dev/null
@@ -1,314 +0,0 @@
-/* -----------------------------------------------------------------------
- * Copyright 2014 Equinox Software, Inc.
- * Bill Erickson <berick@esilibrary.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program 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 General Public License for more details.
- * -----------------------------------------------------------------------
- */
-package org.evergreen_ils.hatch;
-
-import java.io.IOException;
-import java.io.File;
-import java.io.BufferedReader;
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
-import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import javax.servlet.ServletConfig;
-
-import org.eclipse.jetty.util.ajax.JSON;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import java.util.Arrays;
-import java.util.List;
-import java.util.HashMap;
-import java.util.Map;
-
-@WebSocket
-public class HatchWebSocketHandler {
-
-    /** A single connection to a WebSockets client */
-    private Session session;
-
-    /** Current origin domain */
-    private String origin;
-
-    /** List of Origin domains from which we allow connections */
-    private static String[] trustedDomains;
-
-    /** True if we trust all Origin domains */
-    private static boolean trustAllDomains = false;
-
-    /** Root directory for all FileIO operations */
-    private static String profileDirectory;
-
-    /** Our logger instance */
-    private static final Logger logger = Log.getLogger("WebSocketHandler");
-
-    /**
-     * Apply trusted domains.
-     *
-     * If the first domain in the list equals "*", that signifies that
-     * all domains should be trusted.
-     *
-     * @param domains Array of domains to trust.
-     */
-    public static void setTrustedDomains(String[] domains) {
-        trustedDomains = domains;
-
-        if (domains.length > 0 ) {
-
-            if ("*".equals(domains[0])) {
-                logger.info("All domains trusted");
-                trustAllDomains = true;
-
-            } else {
-
-                for(String domain : trustedDomains) {
-                    logger.info("Trusted domain: " + domain);
-                }
-            }
-        } else {
-            logger.warn("No domains are trusted.  All requests will be denied");
-        }
-    }
-
-    /**
-     * Sets the profile directory
-     *
-     * @param directory Directory path as a String
-     */
-    public static void setProfileDirectory(String directory) {
-        profileDirectory = directory;
-    }
-
-
-    /**
-     * Runs the initial, global configuration for this handler.
-     * TODO: move this into setProfileDirectory() (which will need to
-     * be force-called regardless of config)?
-     */
-    public static void configure() {
-        logger.info("WebSocketHandler.configure()");
-
-        // default to ~/.evergreen
-        if (profileDirectory == null) {
-            String home = System.getProperty("user.home");
-            profileDirectory = new File(home, ".evergreen").getPath();
-            if (profileDirectory == null) {
-                logger.info("Unable to set profile directory");
-            }
-        }
-    }
-
-    /**
-     * Compares the Origin of the current WebSocket connection to the list
-     * of allowed domains to determine if the current connection should
-     * be allowed.
-     *
-     * @return True if the Origin domain is allowed, false otherwise.
-     */
-    protected boolean verifyOriginDomain() {
-        logger.info("received connection from IP " +
-            session.getRemoteAddress().getAddress());
-
-        origin = session.getUpgradeRequest().getHeader("Origin");
-
-        if (origin == null) {
-            logger.warn("No Origin header in request; Dropping connection");
-            return false;
-        }
-
-        logger.info("connection origin is " + origin);
-
-        if (trustAllDomains) return true;
-
-        if (java.util.Arrays.asList(trustedDomains).indexOf(origin) < 0) {
-            logger.warn("Request from un-trusted domain: " + origin);
-            return false;
-        }
-
-        return true;
-    }
-
-
-    /**
-     * WebSocket onConnect handler.
-     *
-     * Verify the Origin domain before any communication may take place
-     */
-    @OnWebSocketConnect
-    public void onConnect(Session session) {
-        this.session = session;
-        if (!verifyOriginDomain()) session.close();
-    }
-
-    /**
-     * WebSocket onClose handler.
-     *
-     * Clears our current session.
-     */
-    @OnWebSocketClose
-    public void onClose(int statusCode, String reason) {
-        logger.info("onClose() statusCode=" + statusCode + ", reason=" + reason);
-        this.session = null;
-    }
-
-    /**
-     * Send a message to our connected client.
-     *
-     * @param json A JSON-encodable object to send to the caller.
-     * @param msgid The message identifier
-     */
-    protected void reply(Object json, Long msgid) {
-        reply(json, msgid, true);
-    }
-
-    /**
-     * Send a message to our connected client.
-     *
-     * @param json A JSON-encodable object to send to the caller.
-     * @param msgid The message identifier
-     * @param success If false, the response will be packaged as an error 
-     * message.
-     */
-    protected void reply(Object json, Long msgid, boolean success) {
-
-        Map<String, Object> response = new HashMap<String, Object>();
-        response.put("msgid", msgid);
-
-        if (success) {
-            response.put("content", json);
-        } else {
-            response.put("error", json);
-        }
-
-        String jsonString = JSON.toString(response);
-        logger.info("replying with : " + jsonString);
-
-        try {
-            if (!success) logger.warn(jsonString);
-            session.getRemote().sendString(jsonString);
-        } catch (IOException e) {
-            logger.warn(e);
-        }
-    }
-
-    /**
-     * WebSocket onMessage handler.
-     *
-     * Processes the incoming message and passes the request off to the 
-     * necessary handler.  Messages must be encoded as JSON strings.
-     */
-    @OnWebSocketMessage
-    @SuppressWarnings("unchecked") // direct casting JSON-parsed objects
-    public void onMessage(String message) {
-        if (session == null || !session.isOpen()) return;
-        logger.info("onMessage() " + message);
-
-        HashMap<String,Object> params = null;
-
-        try {
-            params = (HashMap<String,Object>) JSON.parse(message);
-        } catch (ClassCastException e) {
-            reply("Invalid WebSockets JSON message " + message, 
-                new Long(-1), false);
-        }
-
-        Long msgid = (Long) params.get("msgid");
-        String action = (String) params.get("action");
-        String key = (String) params.get("key");
-        String value = (String) params.get("value");
-        String mime = (String) params.get("mime");
-
-        logger.info("Received request for action " + action);
-
-        // all requets require a message ID
-        if (msgid == null) {
-            reply("No msgid specified in request", msgid, false);
-            return;
-        }
-
-        // all requests require an action
-        if (action == null || action.equals("")) {
-            reply("No action specified in request", msgid, false);
-            return;
-        }
-
-        Object response = null;
-        boolean error = false;
-        FileIO io = new FileIO(profileDirectory, origin);
-
-        switch (action) {
-            case "keys":
-                response = io.keys(key);
-                break;
-
-            case "printers":
-                response = new PrintManager().getPrintersAsMaps();
-                break;
-
-            case "print":
-                // pass ourselves off to the print handler so it can reply
-                // for us after printing has completed.
-                params.put("socket", this);
-                Hatch.enqueueMessage(params);
-
-                // we don't want to return a response below, since the 
-                // FX thread will handle that for us.
-                return;
-
-            case "print-config":
-                try {
-                    response = new PrintManager().configurePrinter(params);
-                } catch(IllegalArgumentException e) {
-                    response = e.toString();
-                    error = true;
-                }
-                break;
-
-            case "get":
-                String val = io.get(key);
-                if (val != null) {
-                    // set() stores bare JSON. We must pass an 
-                    // Object to reply so that it may be embedded into
-                    // a larger JSON response object, hence the JSON.parse().
-                    try {
-                        response = JSON.parse(val);
-                    } catch(java.lang.IllegalStateException e) {
-                        error = true;
-                        response = "Error JSON-parsing stored value " + val;
-                    }
-                }
-                break;
-
-            case "remove":
-                response = io.remove(key);
-                break;
-
-            case "set" :
-                response = io.set(key, value);
-                break;
-
-            case "append" :
-                response = io.append(key, value);
-                break;
-
-            default:
-                response = "No such action: " + action;
-                error = true;
-        }
-
-        reply(response, msgid, !error);
-    }
-}
diff --git a/src/org/evergreen_ils/hatch/HatchWebSocketServlet.java b/src/org/evergreen_ils/hatch/HatchWebSocketServlet.java
deleted file mode 100644 (file)
index b7e07ec..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/* -----------------------------------------------------------------------
- * Copyright 2014 Equinox Software, Inc.
- * Bill Erickson <berick@esilibrary.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program 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 General Public License for more details.
- * -----------------------------------------------------------------------
- */
-package org.evergreen_ils.hatch;
-
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
-
-/**
- * Links HatchWebSocketHandler in as a Servlet handler.
- */
-public class HatchWebSocketServlet extends WebSocketServlet {
-
-    @Override
-    public void configure(WebSocketServletFactory factory) {
-        factory.register(HatchWebSocketHandler.class);
-    }
-
-    @Override
-    public void init(ServletConfig config) throws ServletException {
-        super.init(config); // required for WS
-        HatchWebSocketHandler.configure();
-    }
-}
-
diff --git a/src/org/evergreen_ils/hatch/MessageIO.java b/src/org/evergreen_ils/hatch/MessageIO.java
new file mode 100644 (file)
index 0000000..63aa13b
--- /dev/null
@@ -0,0 +1,161 @@
+package org.evergreen_ils.hatch;
+
+import java.util.logging.*;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.nio.ByteBuffer;
+import java.io.IOException;
+import org.json.*;
+
+
+public class MessageIO {
+
+    private LinkedBlockingQueue<JSONObject> inQueue;
+    private LinkedBlockingQueue<JSONObject> outQueue;
+    static final Logger logger = Hatch.getLogger();
+
+    private MessageReader reader;
+    private MessageWriter writer;
+
+    public MessageIO() {
+        inQueue = new LinkedBlockingQueue<JSONObject>();
+        outQueue = new LinkedBlockingQueue<JSONObject>();
+        reader = new MessageReader();
+        writer = new MessageWriter();
+    }
+
+    public void listen() {
+        writer.start();
+        reader.start();
+    }
+
+    public JSONObject recvMessage() {
+        while (true) {
+            try {
+                return inQueue.take();
+            } catch (InterruptedException e) {}
+        }
+    }
+
+    public void sendMessage(JSONObject msg) {
+        outQueue.offer(msg);
+    }
+
+    class EndOfStreamException extends IOException { }
+
+    class MessageReader extends Thread {
+
+        private int bytesToInt(byte[] bytes) {
+            return 
+                  (bytes[3] << 24) & 0xff000000 
+                | (bytes[2] << 16) & 0x00ff0000
+                | (bytes[1] <<  8) & 0x0000ff00 
+                | (bytes[0] <<  0) & 0x000000ff;
+        }
+
+        private String readOneMessage() throws EndOfStreamException, IOException {
+            byte[] lenBytes = new byte[4];
+            int bytesRead = System.in.read(lenBytes);
+
+            if (bytesRead == -1) {
+                throw new EndOfStreamException();
+            }
+
+            int msgLength = bytesToInt(lenBytes);
+
+            if (msgLength == 0) {
+                throw new IOException("Inbound message is 0 bytes.  Interrupted?");
+            }
+
+            logger.info("MessageReader read message length: " + msgLength);
+
+            byte[] msgBytes = new byte[msgLength];
+
+            bytesRead = System.in.read(msgBytes);
+
+            if (bytesRead == -1) {
+                throw new EndOfStreamException();
+            }
+
+            String message = new String(msgBytes, "UTF-8");
+
+            logger.info("MessageReader read message " + message);
+
+            return message;
+        }
+
+        public void run() {
+
+            while (true) {
+
+                String message = "";
+                JSONObject jsonMsg = null;
+
+                try {
+                    
+                    message = readOneMessage();
+                    jsonMsg = new JSONObject(message);
+
+                } catch (EndOfStreamException eose) {
+
+                    logger.warning("STDIN closed... exiting");
+                    System.exit(1);
+
+                } catch (IOException ioe) {
+                    logger.warning(ioe.toString());
+
+                } catch (JSONException je) {
+
+                    logger.warning("Error parsing JSON message on STDIN " +
+                        je.toString() + " : " + message);
+
+                    continue;
+                }
+
+                inQueue.offer(jsonMsg);
+
+                logger.info("inQueue contains " + inQueue.size() + " messages");
+            }
+        }
+    }
+
+    class MessageWriter extends Thread {
+
+        private byte[] intToBytes(int length) {
+            byte[] bytes = new byte[4];
+            bytes[0] = (byte) (length & 0xFF);
+            bytes[1] = (byte) ((length >> 8) & 0xFF);
+            bytes[2] = (byte) ((length >> 16) & 0xFF);
+            bytes[3] = (byte) ((length >> 24) & 0xFF);
+            return bytes;
+        }
+
+        public void writeOneMessage(String message) throws IOException {
+            System.out.write(intToBytes(message.length()));
+            System.out.write(message.getBytes("UTF-8"));
+            System.out.flush();
+        }
+
+        public void run() {
+
+            while (true) {
+                logger.info("MessageWriter waiting for outQueue message");
+
+                try {
+
+                    // take() blocks the thread until a message is available
+                    JSONObject jsonMsg = outQueue.take();
+
+                    writeOneMessage(jsonMsg.toString());
+
+                } catch (InterruptedException e) {
+                    // interrupted, go back and listen
+                    continue;
+                } catch (IOException ioe) {
+                    logger.warning(
+                        "Error writing message to STDOUT: " + ioe.toString());
+                }
+           }
+        }
+    }
+}
+
index 40a4003..f63d7e7 100644 (file)
@@ -15,9 +15,6 @@
  */
 package org.evergreen_ils.hatch;
 
-// logging
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
 
 // printing
 import javafx.print.*;
@@ -47,7 +44,7 @@ import java.util.LinkedHashSet;
 public class PrintManager {
 
     /** Our logger instance */
-    static final Logger logger = Log.getLogger("PrintManager");
+    //static final Logger logger = Log.getLogger("PrintManager");
 
     /**
      * Shows the print dialog, allowing the user to modify settings,
@@ -95,43 +92,45 @@ public class PrintManager {
         Map<String,Object> settings = 
             (Map<String,Object>) params.get("config");
 
+        /*
         HatchWebSocketHandler socket = 
             (HatchWebSocketHandler) params.get("socket");
+            */
 
         PrinterJob job = null;
 
         try {
             job = buildPrinterJob(settings);
         } catch(IllegalArgumentException e) {
-            socket.reply(e.toString(), msgid, false);
+            //socket.reply(e.toString(), msgid, false);
             return;
         }
 
         if (showDialog != null && showDialog.booleanValue()) {
-            logger.info("Print dialog requested");
+            //logger.info("Print dialog requested");
 
             if (!job.showPrintDialog(null)) {
                 // job canceled by user
-                logger.info("after dialog");
+                //logger.info("after dialog");
                 job.endJob();
-                socket.reply("Print job canceled", msgid);
+                //socket.reply("Print job canceled", msgid);
                 return;
             }
         } else {
-            logger.info("No print dialog requested");
+            //logger.info("No print dialog requested");
         }
 
         Thread[] all = new Thread[100];
         int count = Thread.currentThread().enumerate(all);
-        logger.info(count + " active threads in print");
-        logger.info("Thread " + Thread.currentThread().getId() + " printing...");
+        //logger.info(count + " active threads in print");
+        //logger.info("Thread " + Thread.currentThread().getId() + " printing...");
 
         engine.print(job);
-        logger.info("after print");
+        //logger.info("after print");
 
         job.endJob();
 
-        socket.reply("Print job succeeded", msgid);
+        //socket.reply("Print job succeeded", msgid);
     }
 
     /**
@@ -193,7 +192,7 @@ public class PrintManager {
         Set<Paper> papers = printerAttrs.getSupportedPapers();
         for (Paper source : papers) {
             if (source.getName().equals(paperName)) {
-                logger.info("Found matching paper for " + paperName);
+                //logger.info("Found matching paper for " + paperName);
                 paper = source;
                 break;
             }
@@ -258,7 +257,7 @@ public class PrintManager {
             // meaning no source.. meaning let the printer decide.
             for (PaperSource source : paperSources) {
                 if (source.getName().equals(paperSource)) {
-                    logger.info("matched paper source for " + paperSource);
+                    //logger.info("matched paper source for " + paperSource);
                     jobSettings.setPaperSource(source);
                     break;
                 }
@@ -267,7 +266,7 @@ public class PrintManager {
 
 
         if (pageRanges != null) {
-            logger.info("pageRanges = " + pageRanges.toString());
+            //logger.info("pageRanges = " + pageRanges.toString());
             List<PageRange> builtRanges = new LinkedList<PageRange>();
             int i = 0, start = 0, end = 0;
             do {
@@ -296,7 +295,7 @@ public class PrintManager {
         Map<String,Object> settings = new HashMap<String,Object>();
         JobSettings jobSettings = job.getJobSettings();
 
-        logger.info("Extracting print job settings from " + job);
+        //logger.info("Extracting print job settings from " + job);
 
         settings.put(
             jobSettings.collationProperty().getName(),
@@ -358,7 +357,7 @@ public class PrintManager {
             }
         }
 
-        logger.info("compiled printer properties: " + settings.toString());
+        //logger.info("compiled printer properties: " + settings.toString());
         return settings;
     }
 
@@ -397,7 +396,7 @@ public class PrintManager {
                 printer.getName().equals(defaultPrinter.getName())) {
                 printerMap.put("is-default", new Boolean(true));
             }
-            logger.info("found printer " + printer.getName());            
+            //logger.info("found printer " + printer.getName());            
         }
 
         return printerMaps;
diff --git a/src/org/evergreen_ils/hatch/RequestHandler.java b/src/org/evergreen_ils/hatch/RequestHandler.java
new file mode 100644 (file)
index 0000000..f60a6cc
--- /dev/null
@@ -0,0 +1,43 @@
+package org.evergreen_ils.hatch;
+
+import org.json.*;
+import java.util.logging.*;
+
+public class RequestHandler extends Thread {
+    private MessageIO io;
+    static final Logger logger = Hatch.getLogger();
+
+    public RequestHandler() {
+        io = new MessageIO();
+    }
+
+    void dispatchRequest(JSONObject request) throws JSONException {
+    
+        long msgid = request.getLong("msgid");
+        String action = request.getString("action");
+        /*
+        String key = request.getString("key");
+        String content = request.getString("content");
+        String contentType = request.getString("contentType");
+        */
+        boolean showDialog = request.optBoolean("showDialog");
+
+        logger.info("Received message action: " + action);
+    }
+
+
+    public void run() {
+
+        io.listen(); // STDIN/STDOUT handler
+
+        while (true) { 
+            try {
+                dispatchRequest(io.recvMessage()); 
+            } catch (JSONException je) {
+                logger.warning(
+                    "JSON request protocol error: " + je.toString());
+            }
+        }
+    }
+}
+