initial import
authorBill Erickson <berick@esilibrary.com>
Fri, 21 Feb 2014 21:56:09 +0000 (16:56 -0500)
committerBill Erickson <berick@esilibrary.com>
Fri, 21 Feb 2014 21:56:09 +0000 (16:56 -0500)
 * jetty servlet
 * simple file IO handler
 * really dumb sample print driver
 * dependency fetcher, compiler script

Signed-off-by: Bill Erickson <berick@esilibrary.com>
WEB-INF/web.xml [new file with mode: 0644]
hatch-test.html [new file with mode: 0644]
jetty_control.sh [new file with mode: 0755]
src/org/evergreen_ils/hatch/FileIO.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/HatchServlet.java [new file with mode: 0644]
src/org/evergreen_ils/hatch/PrintDriver.java [new file with mode: 0644]

diff --git a/WEB-INF/web.xml b/WEB-INF/web.xml
new file mode 100644 (file)
index 0000000..6fb7917
--- /dev/null
@@ -0,0 +1,40 @@
+<?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>
diff --git a/hatch-test.html b/hatch-test.html
new file mode 100644 (file)
index 0000000..5840470
--- /dev/null
@@ -0,0 +1,64 @@
+<!--
+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>
diff --git a/jetty_control.sh b/jetty_control.sh
new file mode 100755 (executable)
index 0000000..3842337
--- /dev/null
@@ -0,0 +1,75 @@
+#!/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;
+
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/HatchServlet.java b/src/org/evergreen_ils/hatch/HatchServlet.java
new file mode 100644 (file)
index 0000000..da0d838
--- /dev/null
@@ -0,0 +1,227 @@
+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);
+    }
+}
diff --git a/src/org/evergreen_ils/hatch/PrintDriver.java b/src/org/evergreen_ils/hatch/PrintDriver.java
new file mode 100644 (file)
index 0000000..f245c0a
--- /dev/null
@@ -0,0 +1,88 @@
+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);
+   }
+}
+
+