--- /dev/null
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app
+ xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ metadata-complete="false"
+ version="3.0">
+
+ <servlet>
+ <servlet-name>Hatch</servlet-name>
+ <servlet-class>org.evergreen_ils.hatch.HatchServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>Hatch</servlet-name>
+ <url-pattern>/*</url-pattern>
+ </servlet-mapping>
+
+ <context-param>
+ <description>
+ Comma-separated list of Origin domains
+ allowed to make requests of this service.
+ </description>
+ <param-name>trusted-domains</param-name>
+ <!--
+ * == all domains are trusted. Use for testing only.
+ Change to match your trusted Evergreen domain(s).
+ -->
+ <param-value>*</param-value>
+ </context-param>
+
+ <!-- defaults to ~/.evergreen/ -->
+ <!--
+ <context-param>
+ <description>Profile directory</description>
+ <param-name>profile-directory</param-name>
+ <param-value>/path/to/profile/.evergreen</param-value>
+ </context-param>
+ -->
+
+</web-app>
--- /dev/null
+<!--
+1. run hatch
+2. put this file on a remote server and access it via web browser
+-->
+<html>
+ <head>
+ <script>
+
+ // using sync requests since they are easier to read
+ // in a non-promise world.
+ function sendHatchRequest(action, key, value, oncomplete) {
+ var url = 'http://localhost:8080/hatch/'; // trailing slash important
+ var data = 'action=' + action;
+
+ if (key !== undefined) {
+ data += '&key=' + key;
+ } else {
+ key = ''; /* logging */
+ }
+
+ if (value !== undefined) {
+ data += '&value=' + encodeURIComponent(value);
+ } else {
+ value = ''; /* logging */
+ }
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", url, false);
+ xhr.setRequestHeader(
+ 'Content-Type', 'application/x-www-form-urlencoded');
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ document.getElementById('content').innerHTML +=
+ action + ' => ' + key + ' => ' + value + ' => ' +
+ xhr.status + ' ==> ' + xhr.responseText + '<br/>';
+ if (oncomplete) oncomplete();
+ }
+ }
+
+ xhr.send(data);
+ }
+
+ function go() {
+ sendHatchRequest('set', 'a.b.c', 'Wally World...\n');
+ sendHatchRequest('set', 'a.b.d', 'Sugar Plum Fairy!\n');
+ sendHatchRequest('append', 'a.b.c', 'Here I Come!\n');
+ sendHatchRequest('keys'); // list of all keys
+ sendHatchRequest('keys', 'a.b'); // list of all keys starting with 'a.b'
+ sendHatchRequest('get', 'a.b.c');
+ sendHatchRequest('delete', 'a.b.c');
+ sendHatchRequest('delete', 'a.b.d');
+ sendHatchRequest('keys');
+
+ // really dumb print operation
+ sendHatchRequest('print', 'xxx', 'Who, I say, I say,\n who let the dogs out?');
+ }
+
+ </script>
+ </head>
+ <body onload='go()'>
+ <div id='content'></div>
+ </body>
+</html>
--- /dev/null
+#!/bin/bash
+set -e
+
+BASE_DIR=$PWD
+JETTY_VERSION=9.1.2.v20140210
+JETTY_DIR="jetty-distribution-$JETTY_VERSION"
+JETTY_PACKAGE="$JETTY_DIR.tar.gz"
+JETTY_UTIL_AJAX=jetty-util-ajax-$JETTY_VERSION.jar
+
+OPT_FETCH=0;
+OPT_COMPILE=0
+OPT_RUN=0
+
+# return here on exit
+trap "{ cd $BASE_DIR; }" EXIT
+
+function usage() {
+ echo ""
+ echo "Fetch dependencies, compile, and run Hatch:";
+ echo ""
+ echo "$0 -fcr";
+ echo ""
+}
+
+while getopts "fcrh" flag; do
+ case $flag in
+ "f") OPT_FETCH=1;;
+ "c") OPT_COMPILE=1;;
+ "r") OPT_RUN=1;;
+ "h"|*) usage;;
+ esac;
+done
+
+if [ $OPT_FETCH -eq 1 ]; then
+
+ mkdir -p WEB-INF/lib;
+ mkdir -p WEB-INF/classes;
+
+ if [ ! -f $JETTY_PACKAGE ]; then
+ echo "Fetching Jetty package...";
+ wget "http://carroll.aset.psu.edu/pub/eclipse/jetty/$JETTY_VERSION/dist/$JETTY_PACKAGE";
+ fi;
+
+ if [ ! -d $JETTY_DIR ]; then
+ echo "Unpacking Jetty...";
+ tar xf $JETTY_PACKAGE;
+ cp $JETTY_DIR/lib/jetty-util-$JETTY_VERSION.jar WEB-INF/lib/;
+ fi;
+
+ if [ ! -f $JETTY_UTIL_AJAX ]; then
+ echo "Fetching $JETTY_UTIL_AJAX..."
+ wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-util-ajax/$JETTY_VERSION/$JETTY_UTIL_AJAX";
+ cp $JETTY_UTIL_AJAX $JETTY_DIR/lib/;
+ cp $JETTY_UTIL_AJAX WEB-INF/lib/;
+ fi;
+
+ if [ ! -d $JETTY_DIR/webapps/hatch ]; then
+ echo "Setting up webapp links...";
+ mkdir $JETTY_DIR/webapps/hatch;
+ ln -s $BASE_DIR/WEB-INF $JETTY_DIR/webapps/hatch/WEB-INF;
+ fi;
+fi;
+
+if [ $OPT_COMPILE -eq 1 ]; then
+ echo "Compiling..."
+ cd $BASE_DIR/src;
+ javac -Xlint:unchecked -d ../WEB-INF/classes/ -cp "../$JETTY_DIR/lib/*" org/evergreen_ils/hatch/*.java;
+fi;
+
+if [ $OPT_RUN -eq 1 ]; then
+ echo "Running..."
+ cd $BASE_DIR/$JETTY_DIR;
+ java -jar start.jar
+fi;
+
--- /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 java.io.*;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+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;
+
+public class HatchServlet extends HttpServlet {
+
+ static final String contentType = "application/json";
+ static boolean trustAllDomains = false;
+ static String trustedDomainsString;
+ static String[] trustedDomains;
+ static String profileDirectory;
+ private static final Logger logger = Log.getLogger("HatchetServlet");
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+
+ trustedDomainsString =
+ config.getServletContext().getInitParameter("trusted-domains");
+
+ profileDirectory =
+ config.getServletContext().getInitParameter("profile-directory");
+
+ // 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(
+ HttpServletRequest request, HttpServletResponse response) {
+ String origin = request.getHeader("Origin");
+
+ if (trustAllDomains) {
+ if (origin == null) origin = request.getRemoteHost();
+ response.addHeader("Access-Control-Allow-Origin", origin);
+ return true;
+ }
+
+ if (origin == null) {
+ logger.warn("No Origin header in request; Dropping");
+ return false;
+ }
+
+ logger.info("Receive request from " + origin);
+
+ if (java.util.Arrays.asList(trustedDomains).indexOf(origin) < 0) {
+ logger.warn("Request from un-trusted domain: " + origin);
+ return false;
+ }
+
+ response.addHeader("Access-Control-Allow-Origin", trustedDomainsString);
+ return true;
+ }
+
+
+ /**
+ * Honor POST and GET the same.
+ */
+ protected void doPostGet(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+
+ response.setContentType(contentType);
+
+ // ensure the caller is allowed to access this resource
+ if (!verifyOriginDomain(request, response)) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ FileIO io;
+ String action = request.getParameter("action");
+ String key = request.getParameter("key");
+ String value = request.getParameter("value");
+
+ // all requests require an action
+ if (action == null || action.equals("")) {
+ String err = JSON.toString("No action specified in request");
+ response.getWriter().println(err);
+ logger.info(err);
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ if (action.equals("keys")) {
+ io = new FileIO(profileDirectory);
+ String[] keys = io.keys(key); // OK for key to be null
+
+ if (keys != null) {
+ String json = JSON.toString(keys);
+ response.getWriter().println(json);
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ return;
+ }
+
+ // all remaining requests require a key
+ if (key == null || key.equals("")) {
+ String err = JSON.toString("No key specified in request");
+ response.getWriter().println(err);
+ logger.info(err);
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ if (action.equals("get")) {
+ io = new FileIO(profileDirectory);
+ BufferedReader reader = io.get(key);
+ if (reader != null) {
+ String line;
+ response.setStatus(HttpServletResponse.SC_OK);
+ 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.
+ response.getWriter().println(line);
+ }
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ return;
+ }
+
+ if (action.equals("delete")) {
+ io = new FileIO(profileDirectory);
+ if (io.delete(key)) {
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ return;
+ }
+
+ // all remaining actions require value
+ if (value == null) {
+ String err = JSON.toString("No value specified in request");
+ response.getWriter().println(err);
+ logger.info(err);
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ switch(action) {
+
+ case "print" :
+ boolean ok = new PrintDriver().printWithDialog(value);
+ if (ok) {
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ break;
+
+ case "set" :
+ io = new FileIO(profileDirectory);
+ if (io.set(key, value)) {
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ break;
+
+ case "append" :
+ io = new FileIO(profileDirectory);
+ if (io.append(key, value)) {
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ response.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ break;
+
+ default:
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response.getWriter().println(JSON.toString(
+ "\"No Such Action: " + action + "\""));
+ logger.info("No such action: " + action);
+ }
+ }
+
+ protected void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ doPostGet(request, response);
+ }
+
+ protected void doPost(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ doPostGet(request, response);
+ }
+}
--- /dev/null
+package org.evergreen_ils.hatch;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.awt.print.*;
+
+public class PrintDriver implements Printable {
+
+ private String printText;
+
+ public int print(Graphics g, PageFormat pf, int page)
+ throws PrinterException {
+
+ // for now, assume we only have one page
+ if (page > 0) return NO_SUCH_PAGE;
+
+ // find the imageable area
+ Graphics2D g2d = (Graphics2D)g;
+ g2d.translate(pf.getImageableX(), pf.getImageableY());
+
+ int x = 5;
+ int y = 5;
+ for (String line : printText.split("\n"))
+ g.drawString(line, x, y += g.getFontMetrics().getHeight());
+
+ return PAGE_EXISTS;
+ }
+
+ /**
+ * Spawns standard JAVA-driven print dialog and prints text
+ */
+ public boolean printWithDialog(String text) {
+ printText = text;
+ PrinterJob job = PrinterJob.getPrinterJob();
+ job.setPrintable(this);
+ if (!job.printDialog()) return true; // print canceled by user
+ try {
+ job.print();
+ } catch (PrinterException ex) {
+ // TODO
+ return false;
+ }
+ return true;
+ }
+
+ // experiment
+ // show our own print dialog before the real print action takes over
+ // currently just shows Print and Cancel.
+ // Not sure if there is a need for such a thing..
+ public void printWithCustomDialog(String msg) {
+ UIManager.put("swing.boldMetal", Boolean.FALSE);
+ JFrame f = new JFrame("Hello World Printer");
+
+ // close the frame when Cancel / X are clicked
+ f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ final String printMsg = msg;
+ JButton printButton = new JButton("Print '" + msg + "'");
+ printButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ printWithDialog(printMsg);
+ }
+ });
+
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Component component = (Component) e.getSource();
+ JFrame frame = (JFrame) SwingUtilities.getRoot(component);
+ WindowEvent windowClosing =
+ new WindowEvent(frame, WindowEvent.WINDOW_CLOSING);
+ frame.dispatchEvent(windowClosing);
+ }
+ });
+
+ JPanel grid = new JPanel(new FlowLayout(FlowLayout.LEFT,3,3));
+ f.add(grid);
+ grid.add(cancelButton);
+ grid.add(printButton);
+ f.pack();
+ f.setVisible(true);
+ }
+}
+
+