From be21a146195408bc460f067684ff38675305c017 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 15 Apr 2014 15:56:34 -0400 Subject: [PATCH] hatch, rearranged to use javafx Application for browser printing Signed-off-by: Bill Erickson --- README | 3 + run.sh | 15 ++ src/org/evergreen_ils/hatch/FileIO.java | 145 +++++++++++ src/org/evergreen_ils/hatch/Hatch.java | 166 +++++++++++++ .../evergreen_ils/hatch/HatchWebSocketHandler.java | 266 +++++++++++++++++++++ .../evergreen_ils/hatch/HatchWebSocketServlet.java | 28 +++ src/org/evergreen_ils/hatch/PrintManager.java | 91 +++++++ 7 files changed, 714 insertions(+) create mode 100644 README create mode 100755 run.sh create mode 100644 src/org/evergreen_ils/hatch/FileIO.java create mode 100644 src/org/evergreen_ils/hatch/Hatch.java create mode 100644 src/org/evergreen_ils/hatch/HatchWebSocketHandler.java create mode 100644 src/org/evergreen_ils/hatch/HatchWebSocketServlet.java create mode 100644 src/org/evergreen_ils/hatch/PrintManager.java diff --git a/README b/README new file mode 100644 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 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 index 0000000..95fc012 --- /dev/null +++ b/src/org/evergreen_ils/hatch/FileIO.java @@ -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 nameList = new LinkedList(); + 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 index 0000000..c3c2941 --- /dev/null +++ b/src/org/evergreen_ils/hatch/Hatch.java @@ -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 requestQueue = + new LinkedBlockingQueue(); + + /** + * 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> { + protected Task> createTask() { + return new Task>() { + protected Map 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 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 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() { + + @Override + public void handle(WorkerStateEvent t) { + Map message = + (Map) 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 index 0000000..661c3d2 --- /dev/null +++ b/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java @@ -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 response = new HashMap(); + 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 params = null; + + try { + params = (HashMap) 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 index 0000000..17e523f --- /dev/null +++ b/src/org/evergreen_ils/hatch/HatchWebSocketServlet.java @@ -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 index 0000000..3407929 --- /dev/null +++ b/src/org/evergreen_ils/hatch/PrintManager.java @@ -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 getPrinters() { + + List printers = new LinkedList(); + PrintService[] printServices = + PrintServiceLookup.lookupPrintServices(null, null); + + String defaultPrinter = ""; + PrintService def = PrintServiceLookup.lookupDefaultPrintService(); + if (def != null) defaultPrinter = def.getName(); + + for (PrintService service : printServices) { + HashMap printer = new HashMap(); + 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; + } +} + -- 2.11.0