--- /dev/null
+Hatch - Java Print / Storage / Etc Service
+
+TODO: more info
--- /dev/null
+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
--- /dev/null
+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]);
+ }
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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();
+ }
+}
+
--- /dev/null
+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;
+ }
+}
+