hatch, rearranged to use javafx Application for browser printing
authorBill Erickson <berick@esilibrary.com>
Tue, 15 Apr 2014 19:56:34 +0000 (15:56 -0400)
committerJeff Godin <jgodin@tadl.org>
Fri, 3 Jun 2016 20:38:46 +0000 (16:38 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
README [new file with mode: 0644]
run.sh [new file with mode: 0755]
src/org/evergreen_ils/hatch/FileIO.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/Hatch.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/HatchWebSocketHandler.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/HatchWebSocketServlet.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/PrintManager.java [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..da2faa2
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+Hatch - Java Print / Storage / Etc Service
+
+TODO: more info
diff --git a/run.sh b/run.sh
new file mode 100755 (executable)
index 0000000..186de4d
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,15 @@
+JAVA_HOME=jdk1.8.0
+JETTY_HOME=jetty-distribution-9.1.4.v20140401
+
+# compile
+$JAVA_HOME/bin/javac \
+    -cp "$JETTY_HOME/lib/*:$JETTY_HOME/lib/websocket/*:lib/*" \
+    -Xdiags:verbose -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
diff --git a/src/org/evergreen_ils/hatch/FileIO.java b/src/org/evergreen_ils/hatch/FileIO.java
new file mode 100644 (file)
index 0000000..95fc012
--- /dev/null
@@ -0,0 +1,145 @@
+package org.evergreen_ils.hatch;
+import java.io.*;
+import java.util.LinkedList;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class FileIO {
+
+    String basePath;
+    private static final Logger logger = Log.getLogger("FileIO");
+
+    public FileIO(String directory) {
+        basePath = directory;
+    }
+
+    protected File getFile(String key) {
+        File dir = new File(basePath);
+        if (!dir.exists()) {
+            if (!dir.mkdir()) {
+                logger.info("Unable to create director: " + basePath);
+                return null;
+            }
+        }
+        return new File(dir, key);
+    }
+
+    public boolean set(String key, String text) {
+        logger.info("set => " + key);
+        File file = getFile(key);
+
+        try {
+
+            // delete the file if it exists
+            if (!file.exists() && !file.createNewFile()) {
+                logger.info(
+                    "Unable to create file: " + file.getCanonicalPath());
+                return false;
+            }
+
+            // destructive write (replace existing text)
+            Writer outStream = new BufferedWriter(
+                new FileWriter(file.getAbsoluteFile()));
+
+            outStream.write(text);
+            outStream.close();
+
+        } catch(IOException e) {
+            logger.warn("Error calling set() with key " + key);
+            logger.warn(e);
+            return false;
+        }
+
+        return true;
+    }
+
+    public boolean append(String key, String text) {
+        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());
+                return false;
+            }
+
+            // non-destructive write (append)
+            Writer outStream = new BufferedWriter(
+                new FileWriter(file.getAbsoluteFile(), true));
+            outStream.write(text);
+            outStream.close();
+
+        } catch(IOException e) {
+            logger.warn("Error in append() with key " + key);
+            logger.warn(e);
+            return false;
+        }
+
+        return true;
+    }
+
+    public BufferedReader get(String key) {
+        logger.info("get => " + key);
+        File file = getFile(key);
+        if (!file.exists()) return null;
+
+        StringBuffer sbuf = new StringBuffer();
+        try {
+            return new BufferedReader(
+                new FileReader(file.getAbsoluteFile()));
+        } catch (IOException e) {
+            logger.warn("Error reading key: " + key);
+            logger.warn(e);
+            return null;
+        }
+    }
+
+    public boolean delete(String key) {
+        logger.info("delete => " + key);
+        File file = getFile(key);
+        try {
+            if (file.exists() && !file.delete()) {
+                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;
+        }
+    }
+
+    public String[] keys() {
+        return keys(null);
+    }
+
+    public String[] keys(String prefix) {
+        logger.info("keys => " + prefix);
+        File dir = new File(basePath);
+        if (!dir.exists()) return new String[0];
+
+        LinkedList<String> nameList = new LinkedList<String>();
+        File[] files = dir.listFiles();
+
+        for (File file : files) {
+            if (file.isFile()) {
+                String name = file.getName();
+                if (prefix == null) {
+                    nameList.add(name);
+                } else {
+                    if (name.startsWith(prefix)) {
+                        nameList.add(name);
+                    }
+                }
+            }
+        }
+
+        return (String[]) nameList.toArray(new String[0]);
+    }
+}
diff --git a/src/org/evergreen_ils/hatch/Hatch.java b/src/org/evergreen_ils/hatch/Hatch.java
new file mode 100644 (file)
index 0000000..c3c2941
--- /dev/null
@@ -0,0 +1,166 @@
+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.util.resource.Resource;
+import org.eclipse.jetty.xml.XmlConfiguration;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+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;
+public class Hatch extends Application {
+
+    private BrowserView browser;
+    private Stage primaryStage;
+    static final Logger logger = Log.getLogger("Hatch");
+
+    private static LinkedBlockingQueue<Map> requestQueue = 
+        new LinkedBlockingQueue<Map>();
+
+    /**
+     * Printable region containing a browser
+     */
+    class BrowserView extends Region {
+        WebView webView = new WebView();
+        WebEngine webEngine = webView.getEngine();
+        public BrowserView() {
+            getChildren().add(webView);
+        }
+    }
+
+    /**
+     * 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,String>> {
+        protected Task<Map<String,String>> createTask() {
+            return new Task<Map<String,String>>() {
+                protected Map<String,String> call() {
+                    while (true) {
+                        try {
+                            // take() blocks until a message is available
+                            return requestQueue.take();
+                        } catch (InterruptedException e) { 
+                            // interrupted, go back and listen
+                            continue;
+                        }
+                    }
+                }
+            };
+        }
+    }
+
+
+    @Override
+    public void start(Stage primaryStage) {
+        this.primaryStage = primaryStage;
+        logger.debug("start()"); 
+        startMsgTask();
+    }
+
+    public static void enqueueMessage(Map<String,String> params) {
+        logger.debug("queueing print message");
+        requestQueue.offer(params);
+    }
+
+    /**
+     * Build a browser view from the print content, tell the 
+     * browser to print itself.
+     */
+    private void handlePrint(Map<String,String> params) {
+        String printer = params.get("printer");
+        String content = params.get("content");
+        String contentType = params.get("contentType");
+
+        browser = new BrowserView();
+        Scene scene = new Scene(browser, 640, 480); // TODO: printer dimensions
+        primaryStage.setScene(scene);
+
+        browser.webEngine.getLoadWorker()
+            .stateProperty()
+            .addListener( (ChangeListener) (obsValue, oldState, newState) -> {
+                if (newState == State.SUCCEEDED) {
+                    logger.debug("browser page loaded");
+                    new PrintManager().print(browser.webEngine);
+                }
+            });
+
+        logger.info("printing " + content.length() + " bytes of " + contentType);
+        browser.webEngine.loadContent(content, contentType);
+    }
+
+    /**
+     * Fire off the Service task, which checks for queued messages.
+     *
+     * When a queued message is found, it's analyzed and passed off
+     * to the correct message handler
+     */
+    public void startMsgTask() {
+
+        MsgListenService service = new MsgListenService();
+
+        service.setOnSucceeded(
+            new EventHandler<WorkerStateEvent>() {
+
+            @Override
+            public void handle(WorkerStateEvent t) {
+                Map<String,String> message = 
+                    (Map<String,String>) t.getSource().getValue();
+
+                if (message != null) handlePrint(message);
+
+                // once this task is complete, kick off the next
+                // message task handler.
+                startMsgTask();
+            }
+        });
+
+        service.start();
+    }
+    public static void main(String[] args) throws Exception {
+
+        Server server = new Server(8080);
+        ServletHandler handler = new ServletHandler();
+        server.setHandler(handler);
+
+        // TODO: config file; ditto profileDirectory, logging, etc.
+        HatchWebSocketHandler.trustedDomainsString = "*"; 
+
+        handler.addServletWithMapping(HatchWebSocketServlet.class, "/hatch");
+
+        server.start(); // no join() -- let server thread run in parallel
+        launch(args); // launch the Application
+    }
+}
diff --git a/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java b/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java
new file mode 100644 (file)
index 0000000..661c3d2
--- /dev/null
@@ -0,0 +1,266 @@
+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 {
+
+    private Session session;
+    static String[] trustedDomains;
+    static String trustedDomainsString = null;
+    static boolean trustAllDomains = false;
+    static String profileDirectory;
+    private static final Logger logger = Log.getLogger("WebSocketHandler");
+
+    /**
+     * config is passed in from our WebSocketServlet container, 
+     * hence the public+static.  Possible to access directly?
+     */
+    //public static void configure(ServletConfig config) {
+    public static void configure() {
+        logger.info("WebSocketHandler.configure()");
+
+        /*
+        trustedDomainsString = 
+            config.getServletContext().getInitParameter("trustedDomains");
+
+        logger.info("trusted domains " + trustedDomainsString);
+
+        profileDirectory = 
+            config.getServletContext().getInitParameter("profileDirectory");
+            */
+
+        // 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");
+            }
+        }   
+
+        if (trustedDomainsString == null) {
+            logger.info("No trusted domains configured");
+
+        } else {
+
+            if (trustedDomainsString.equals("*")) {
+                trustAllDomains = true;
+                logger.info("All domains trusted");
+
+            } else {
+
+                trustedDomains = trustedDomainsString.split(",");
+                for(String domain : trustedDomains) {
+                    logger.info("Trusted domain: " + domain);
+                }
+            }
+        }
+    }  
+
+    protected boolean verifyOriginDomain() {
+        logger.info("received connection from IP " + 
+            session.getRemoteAddress().getAddress());
+
+        String 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;
+    }
+
+
+    @OnWebSocketConnect
+    public void onConnect(Session session) {
+        this.session = session;
+        if (!verifyOriginDomain()) session.close();
+    }
+
+    @OnWebSocketClose
+    public void onClose(int statusCode, String reason) {
+        logger.info("onClose() statusCode=" + statusCode + ", reason=" + reason);
+        this.session = null;
+    }
+
+    private void reply(Object json, String msgid) {
+        reply(json, msgid, true);
+    }
+
+    private void reply(Object json, String msgid, boolean success) {
+
+        Map<String, Object> response = new HashMap<String, Object>();
+        response.put("msgid", msgid);
+        if (success) {
+            response.put("success", json);
+        } else {
+            response.put("error", json);
+        }
+
+        try {
+            String jsonString = JSON.toString(response);
+            if (!success) logger.warn(jsonString);
+            session.getRemote().sendString(jsonString);
+        } catch (IOException e) {
+            logger.warn(e);
+        }
+    }
+
+    @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,String> params = null;
+
+        try {
+            params = (HashMap<String,String>) JSON.parse(message);
+        } catch (ClassCastException e) {
+            reply("Invalid WebSockets JSON message " + message, "", false);
+        }
+
+        FileIO io;
+        String msgid = params.get("msgid");
+        String action = params.get("action");
+        String key = params.get("key");
+        String value = params.get("value");
+        String mime = params.get("mime");
+
+        // all requets require a message ID
+        if (msgid == null || msgid.equals("")) {
+            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;
+        }
+
+        if (action.equals("keys")) {
+            io = new FileIO(profileDirectory);
+            String[] keys = io.keys(key); // OK for key to be null
+            if (keys != null) {
+                reply(keys, msgid);
+            } else {
+                reply("key lookup error", msgid, false);
+            }
+            return;
+        }
+
+        if (action.equals("printers")) {
+            List printers = new PrintManager().getPrinters();
+            reply(printers, msgid);
+            return;
+        }
+
+        if (action.equals("print")) {
+            // TODO: validate the print target first so we can respond
+            // with an error if the requested printer / attributes are
+            // not supported.  Printing occurs in a separate thread,
+            // so for now just assume it succeeded.  Maybe later add
+            // a response queue and see if this handler is capable of
+            // responding from an alternate thread.
+            Hatch.enqueueMessage(params);
+            reply("print succeeded", msgid);
+            return;
+        }
+
+        // all remaining requests require a key
+        if (key == null || key.equals("")) {
+            reply("No key specified in request", msgid, false);
+            return;
+        }
+
+        if (action.equals("get")) {
+            io = new FileIO(profileDirectory);
+            BufferedReader reader = io.get(key);
+            if (reader != null) {
+                String line;
+                try {
+                    while ( (line = reader.readLine()) != null) {
+                        // relay lines of text to the caller as we read them
+                        // assume the text content is JSON and return it
+                        // un-JSON-ified.
+                        reply(line, msgid);
+                    }
+                } catch (IOException e) {
+                    logger.warn(e);
+                }
+            } else {
+                reply("Error accessing property " + key, msgid, false);
+            }
+            return;
+        }
+
+        if (action.equals("delete")) {
+            io = new FileIO(profileDirectory);
+            if (io.delete(key)) {
+                reply("Delete of " + key + " successful", msgid);
+            } else {
+                reply("Delete of " + key + " failed", msgid, false);
+            }
+            return;
+        }
+
+        // all remaining actions require value
+        if (value == null) {
+            reply("No value specified in request", msgid, false);
+            return;
+        }
+
+        switch (action) {
+
+            case "set" :
+                io = new FileIO(profileDirectory);
+                if (io.set(key, value)) {
+                    reply("setting value for " + key + " succeeded", msgid);
+                } else {
+                    reply("setting value for " + key + " succeeded", msgid, false);
+                }
+                break;
+
+            case "append" :
+                io = new FileIO(profileDirectory);
+                if (io.append(key, value)) {
+                    reply("appending value for " + key + " succeeded", msgid);
+                } else {
+                    reply("appending value for " + key + " succeeded", msgid, false);
+                }
+                break;
+
+            default:
+                reply("No such action: " + action, msgid, false);
+        }
+    }
+}
diff --git a/src/org/evergreen_ils/hatch/HatchWebSocketServlet.java b/src/org/evergreen_ils/hatch/HatchWebSocketServlet.java
new file mode 100644 (file)
index 0000000..17e523f
--- /dev/null
@@ -0,0 +1,28 @@
+package org.evergreen_ils.hatch;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+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;
+
+public class HatchWebSocketServlet extends WebSocketServlet {
+
+    static final Logger logger = Log.getLogger("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(config);
+        HatchWebSocketHandler.configure();
+    }  
+}
+
diff --git a/src/org/evergreen_ils/hatch/PrintManager.java b/src/org/evergreen_ils/hatch/PrintManager.java
new file mode 100644 (file)
index 0000000..3407929
--- /dev/null
@@ -0,0 +1,91 @@
+package org.evergreen_ils.hatch;
+
+// logging
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+// printing
+import javafx.print.*;
+import javafx.scene.web.WebEngine;
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+import javax.print.attribute.Attribute;
+import javax.print.attribute.AttributeSet;
+
+// data structures
+import java.util.Map;
+import java.util.List;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+public class PrintManager {
+
+    static final Logger logger = Log.getLogger("PrintManager");
+
+    public void print(WebEngine engine) {
+        debugPrintService(null); // testing
+
+        Printer printer = Printer.getDefaultPrinter();
+        PrinterJob job = PrinterJob.createPrinterJob();
+        if (!job.showPrintDialog(null)) return; // print canceled by user
+        engine.print(job);
+        job.endJob();
+    }
+
+    private void debugPrintService(PrintService printer) {
+
+        PrintService[] printServices;
+        String defaultPrinter = "";
+
+        if (printer != null) {
+            printServices = new PrintService[] {printer};
+        } else {
+            printServices = PrintServiceLookup.lookupPrintServices(null, null);
+            PrintService def = PrintServiceLookup.lookupDefaultPrintService();
+            if (def != null) defaultPrinter = def.getName();
+        }
+
+        for (PrintService service : printServices) {
+            logger.info("Printer Debug: found printer " + service.getName());
+            if (service.getName().equals(defaultPrinter)) {
+                logger.info("    Printer Debug: Is Default");
+            }
+
+            AttributeSet attributes = service.getAttributes();
+            for (Attribute a : attributes.toArray()) {
+                String name = a.getName();
+                String value = attributes.get(a.getClass()).toString();
+                logger.info("    Printer Debug: " + name + " => " + value);
+            }
+        }
+    }
+
+    public List<HashMap> getPrinters() {
+
+        List<HashMap> printers = new LinkedList<HashMap>();
+        PrintService[] printServices = 
+            PrintServiceLookup.lookupPrintServices(null, null);
+
+        String defaultPrinter = "";
+        PrintService def = PrintServiceLookup.lookupDefaultPrintService();
+        if (def != null) defaultPrinter = def.getName();
+
+        for (PrintService service : printServices) {
+            HashMap<String, Object> printer = new HashMap<String, Object>();
+            printers.add(printer);
+
+            if (service.getName().equals(defaultPrinter)) 
+                printer.put("is-default", new Boolean(true));
+
+            AttributeSet attributes = service.getAttributes();
+            for (Attribute a : attributes.toArray()) {
+                String name = a.getName();
+                String value = attributes.get(a.getClass()).toString();
+                printer.put(name, value);
+            }
+        }
+
+        return printers;
+    }
+}
+