* Hatch is now installed as a native messaging host.
* Includes numerous repairs and refactoring to Hatch Java.
* Updated install docs
* Updated hatch.sh / hatch.bat scripts.
* New standalone logging.properties configuration file
* Remove Jetty / websocket code.
* Add org.json requirement for JSON parsing
Signed-off-by: Bill Erickson <berickxx@gmail.com>
--- /dev/null
+= Hatch Developer Install Documentation =
+
+== Build and Test Hatch Java Libs ==
+
+=== Windows ===
+ TODO
+
+=== Linux ===
+
+==== Download JDK 8 ====
+
+1. Go to
+http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html[oracle.com],
+accept the license, download the Linux .tar.gz file.
+
+2. Extract the file in the Hatch root directory and link it into place.
+
+[source,sh]
+-------------------------------------------------------------------------
+$ tar -zxf jdk*tar.gz
+$ ln -s jdk1.8* jdk1.8
+-------------------------------------------------------------------------
+
+NOTE: We may some day use openjdk, but its JavaFX libs are not ready
+for prime time as of writing.
+
+==== Download org.json Java JSON Library ====
+
+[source,sh]
+-------------------------------------------------------------------------
+$ mkdir -p lib
+$ cd lib
+$ wget -O json-20160810.jar \
+ 'https://search.maven.org/remotecontent?filepath=org/json/json/20160810/json-20160810.jar'
+$ cd ../
+-------------------------------------------------------------------------
+
+==== Compile Hatch Java ====
+
+===== Windows =====
+
+[source,sh]
+-------------------------------------------------------------------------
+C:\> hatch.bat compile
+-------------------------------------------------------------------------
+
+===== Linux =====
+
+[source,sh]
+-------------------------------------------------------------------------
+$ ./hatch.sh compile
+-------------------------------------------------------------------------
+
+==== Test Hatch Java ====
+
+Assuming the Java code compiles OK, this will run a series of tests.
+
+NOTE: print commands are disabled by default in the tests to avoid
+unexpected printing, but they can be added by un-commenting
+them in src/org/evergreen_ils/hatch/TestHatch.java and recompiling.
+
+===== Windows =====
+
+[source,sh]
+-------------------------------------------------------------------------
+C:\> hatch.bat test
+-------------------------------------------------------------------------
+
+===== Linux =====
+
+[source,sh]
+-------------------------------------------------------------------------
+$ ./hatch.sh test
+-------------------------------------------------------------------------
+
+== Configure Chrome for Native Messaging with Hatch ==
+
+=== Setup Chrome Extension ===
+
+NOTE: At time of writing, the Evergreen server used must have the
+patches included in the http://git.evergreen-ils.org/?p=working/Evergreen.git;a=shortlog;h=refs/heads/user/berick/lp1640255-hatch-native-messaging[Hatch Native Messaging working branch].
+
+==== Install Chrome Extension ====
+
+. Open Chrome and navigate to chrome://extensions
+. Enable "Developer Mode" along the top right of the page.
+. Click the "Load Unpacked Extension..." button.
+. Load the directory at Hatch/extension/app
+
+===== Debugging The Chrome Extension =====
+
+ * Click the "Background Page" link to see the exension console.
+ * Use the "Reload" link to apply changes made to the extension
+ (e.g. main.js).
+ * See also https://developer.chrome.com/extensions/getstarted
+
+=== Setup Chrome Native Messaging Host ===
+
+See also https://developer.chrome.com/extensions/nativeMessaging
+
+==== Windows ====
+
+Edit extension/host/org.evergreen_ils.hatch.WINDOWS.json and change the
+"path" value to match the location of your copy of "hatch.bat", found in the
+root directory of the Hatch repository.
+
+Create a Native Messaging registry key entry via the Windows command prompt.
+Modify the path value to point to your copy of
+HATCH/extension/host/org.evergreen_ils.hatch.WINDOWS.json.
+
+[source,sh]
+-------------------------------------------------------------------------
+C:\> REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\org.evergreen_ils.hatch" /ve /t REG_SZ /d "C:\path\to\extension\host\org.evergreen_ils.hatch.WINDOWS.json" /f
+-------------------------------------------------------------------------
+
+==== Linux ====
+
+Edit extension/host/org.evergreen_ils.hatch.json and change the "path"
+value to match the location of your copy of "hatch.sh", found in the root
+directory of the Hatch repository.
+
+Copy the host file into Chrome's configuration directory.
+
+For Chrome:
+
+[source,sh]
+-------------------------------------------------------------------------
+$ mkdir -p ~/.config/google-chrome/NativeMessagingHosts/
+$ cp extension/host/org.evergreen_ils.hatch.json ~/.config/google-chrome/NativeMessagingHosts/
+-------------------------------------------------------------------------
+
+For Chromium:
+
+[source,sh]
+-------------------------------------------------------------------------
+$ mkdir -p ~/.config/chromium/NativeMessagingHosts/
+$ cp extension/host/org.evergreen_ils.hatch.json ~/.config/chromium/NativeMessagingHosts/
+-------------------------------------------------------------------------
+
+++ /dev/null
-Hatch - Java Print / Storage / Etc Service
-
-** ROUGH SETUP NOTES **
-
-Install Hatch on your desktop -- Linux edition:
-
-% wget http://download.eclipse.org/jetty/stable-9/dist/jetty-distribution-9.2.5.v20141112.tar.gz
-% tar -zxf jetty-distribution-9.2.5.v20141112.tar.gz
-% ln -s jetty-distribution-9.2.5.v20141112 jetty
-
-# download jdk1.8 (requires license agreement) -- haven't tested on openjdk yet.
-# http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
-# and extract in the same directory
-% ln -s jdk1.8.0_25 jdk1.8
-
-% mkdir lib
-% wget -O lib/jetty-util-ajax-9.2.5.v20141112.jar \
- 'http://central.maven.org/maven2/org/eclipse/jetty/jetty-util-ajax/9.2.5.v20141112/jetty-util-ajax-9.2.5.v20141112.jar'
-
-# create an SSL certificat for jetty
-# if you use a password other than "password", modify references to
-# "password" in hath.xml (in the top directory).
-% cd jetty/etc/
-% ../../jdk1.8/bin/keytool -keystore keystore -alias jetty -genkey -keyalg RSA
-
-# compile
-% ./run.sh
-
-# compile + run
-% ./run.sh 1
-
-# open https://localhost:8443/ in Chrome and click through the security warning.
-# Then open the browser client.
-# Set "This workstation uses a remote print / storage service ("Hatch")?" under Admin -> Workstation
-# optionally configure / test printing
+++ /dev/null
-Hatch - Java Print / Storage / Etc Service
-
-** ROUGH SETUP NOTES **
-
-Install Hatch on your desktop -- Windows edition:
-
-If you're reading this online, first download hatch to your desktop.
-
-The canonical location for this project is:
-http://git.evergreen-ils.org/?p=Hatch.git
-
-But a convenient snapshot as of 2015-08-20 is here:
-https://github.com/phasefx/random/archive/hatch.zip
-
-Unzip the file wherever you'd like to install the application, such as in C:\Program Files\
-
-Then go to http://download.eclipse.org/jetty/
-
-Download the latest stable-9 .zip version into the same directory as this README file.
-
-Unzip and rename the folder to "jetty".
-
-Then go to http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
-
-Download the Development Kit .exe for your version of Windows.
-
-Install it.
-
-In the same directory as this README file, create a subdirectory named "lib".
-
-Then go to http://mvnrepository.com/artifact/org.eclipse.jetty/jetty-util-ajax
-
-Download the latest 9.2.x .jar file into the "lib" directory.
-
-Create a dummy SSL certificate for Jetty.
-For step 2, modify the JDK path to match your version/location.
-The password used when creating the certicate will have to be added
-to hatch.xml, unless you use "password".
-1. cd jetty\etc
-2. C:\"Program Files"\Java\jdk1.8.0_60\bin\keytool.exe -keystore keystore -alias jetty -genkey -keyalg RSA
-
-Edit the run-win.bat file, and, if needed, change the JAVA_HOME variable to the location where you installed the Java Development Kit. In my case, I changed the line:
-
-SET JAVA_HOME="C:\Program Files\Java\jdk1.8.0_20"
-
-to
-
-SET JAVA_HOME="C:\Program Files\Java\jdk1.8.0_31"
-
-Then open a Command window and cd into the same directory as this README file. One easy way to do this is to click into the address of the bar of window displaying the directory, typing "cmd" and pressing enter.
-
-Then enter:
-run-win.bat
-
-Choose Allow for any Firewall/Security prompt.
-
-Then go to https://localhost:8443/ in Chrome and click through the security warning.
-(In my case, I clicked on the link "Advanced" and then "Proceed to localhost (unsafe)")
-
-You should expect a 404 Not Found page.
-
-Then open the browser client.
-
-Set "This workstation uses a remote print / storage service ("Hatch")?" under Admin -> Workstation
-
-Optionally configure / test printing
--- /dev/null
+/* -----------------------------------------------------------------------
+ * Copyright 2016 King County Library System
+ * Bill Erickson <berickxx@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * -----------------------------------------------------------------------
+ *
+ * Hatch Content Script.
+ *
+ * Relays messages between the browser tab and the Hatch extension.js
+ * script.
+ */
+
+console.debug('Loading Hatch relay content script');
+
+// Tell the page DOM we're here.
+document.body.setAttribute('hatch-is-open', '4-8-15-16-23-42');
+
+/**
+ * Open a port to our extension.
+ */
+var port = chrome.runtime.connect();
+
+/**
+ * Relay all messages received from the extension back to the tab
+ */
+port.onMessage.addListener(function(message) {
+
+ /*
+ console.debug(
+ "Content script received from extension: "+ JSON.stringify(message));
+ */
+
+ window.postMessage(message, location.origin);
+});
+
+
+/**
+ * Receive messages from the browser tab and relay them to the
+ * Hatch extension script.
+ */
+window.addEventListener("message", function(event) {
+
+ // We only accept messages from ourselves
+ if (event.source != window) return;
+
+ var message = event.data;
+
+ // Ignore broadcast messages. We only care about messages
+ // received from our browser tab/page.
+ if (message.from != 'page') return;
+
+ /*
+ console.debug(
+ "Content script received from page: " + JSON.stringify(message));
+ */
+
+ // standard Hatch-bound message; relay to extension.
+ port.postMessage(message);
+
+}, false);
+
+
+
--- /dev/null
+/* -----------------------------------------------------------------------
+ * Copyright 2016 King County Library System
+ * Bill Erickson <berickxx@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * -----------------------------------------------------------------------
+ */
+
+// Singleton connection to Hatch
+var hatchPort = null;
+
+// Map of tab identifers to tab-specific connection ports.
+var browserPorts = {};
+
+const HATCH_EXT_NAME = 'org.evergreen_ils.hatch';
+const HATCH_RECONNECT_TIME = 5; // seconds
+
+var hatchHostUnavailable = false;
+function connectToHatch() {
+ console.debug("Connecting to native messaging host: " + HATCH_EXT_NAME);
+ try {
+ hatchPort = chrome.runtime.connectNative(HATCH_EXT_NAME);
+ hatchPort.onMessage.addListener(onNativeMessage);
+ hatchPort.onDisconnect.addListener(onDisconnected);
+ } catch (E) {
+ console.warn("Hatch host connection failed: " + E);
+ hatchHostUnavailable = true;
+ }
+}
+
+/**
+ * Called when the connection to Hatch goes away.
+ */
+function onDisconnected() {
+ console.warn("Hatch disconnected: " + chrome.runtime.lastError.message);
+ hatchPort = null;
+
+ if (hatchHostUnavailable) return;
+
+ // If we can reasonablly assume a connection to the Hatch host
+ // is possible, attempt to reconnect after a failure.
+ setTimeout(connectToHatch, HATCH_RECONNECT_TIME * 1000);
+
+ console.debug("Reconnecting to Hatch after connection failure in " +
+ HATCH_RECONNECT_TIME + " seconds...");
+}
+
+
+/**
+ * Handle response messages received from Hatch.
+ */
+function onNativeMessage(message) {
+ var tabId = message.clientid;
+
+ if (tabId && browserPorts[tabId]) {
+ message.from = 'extension';
+ browserPorts[tabId].postMessage(message);
+ } else {
+ // if browserPorts[tabId] is empty, it generally means the
+ // user navigated away before receiving the response.
+ }
+}
+
+
+/**
+ * Called when our content script opens connection to this extension.
+ */
+chrome.runtime.onConnect.addListener(function(port) {
+ var tabId = port.sender.tab.id;
+
+ browserPorts[tabId] = port;
+ console.debug('new port connected with id ' + tabId);
+
+ port.onMessage.addListener(function(msg) {
+ console.debug("Received message from browser on port " + tabId);
+
+ if (!hatchPort) {
+ // TODO: we could queue failed messages for redelivery
+ // after a reconnect. Not sure yet if that level of
+ // sophistication is necessary.
+ console.debug("Cannot send message " +
+ msg.msgid + " - no Hatch connection present");
+ return;
+ }
+
+ // tag the message with the browser tab ID for response routing.
+ msg.clientid = tabId;
+
+ // Stamp the origin (protocol + host) on every request.
+ msg.origin = port.sender.url.match(/https?:\/\/[^\/]+/)[0];
+
+ hatchPort.postMessage(msg);
+ });
+
+ port.onDisconnect.addListener(function() {
+ console.log("Removing port " + tabId + " on tab disconnect");
+ delete browserPorts[tabId];
+ });
+});
+
+
+function setPageActionRules() {
+ // Replace all rules on extension reload
+ chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
+ chrome.declarativeContent.onPageChanged.addRules([
+ {
+ conditions: [
+ new chrome.declarativeContent.PageStateMatcher({
+ pageUrl : {
+ pathPrefix : '/eg/staff/',
+ schemes : ['https']
+ },
+ css: ["eg-navbar"] // match on <eg-navbar/>
+ })
+ ],
+ actions: [
+ new chrome.declarativeContent.RequestContentScript({
+ 'js': ['content.js']
+ })
+ ]
+ }
+ ]);
+ });
+}
+
+chrome.browserAction.onClicked.addListener(function (tab) {
+ chrome.permissions.request({
+ origins: ['https://*/eg/staff/*']
+ }, function (ok) {
+ if (ok) {
+ console.log('access granted');
+ } else if (chrome.runtime.lastError) {
+ alert('Permission Error: ' + chrome.runtime.lastError.message);
+ } else {
+ alert('Optional permission denied.');
+ }
+ });
+});
+
+
+// Link the page action icon to loading the content script
+chrome.runtime.onInstalled.addListener(setPageActionRules);
+
+// Connect to Hatch on startup.
+connectToHatch();
+
--- /dev/null
+{
+ // Extension ID: knldjmfmopnpolahpmmgbagdohdnhkik
+ "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBHwzDvyBQ6bDppkIs9MP4ksKqCMyXQ/A52JivHZKh4YO/9vJsT3oaYhSpDCE9RPocOEQvwsHsFReW2nUEc6OLLyoCFFxIb7KkLGsmfakkut/fFdNJYh0xOTbSN8YvLWcqph09XAY2Y/f0AL7vfO1cuCqtkMt8hFrBGWxDdf9CQIDAQAB",
+ "name": "Hatch Native Messenger",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "Relays messages to/from Hatch.",
+ "background" : {
+ "scripts" : ["extension.js"]
+ },
+ "browser_action": {
+ "default_title": "Hatch"
+ },
+ "permissions": [
+ "nativeMessaging",
+ "declarativeContent"
+ ],
+ "optional_permissions": [
+ "https://*/eg/staff/*"
+ ],
+ "minimum_chrome_version": "38"
+}
--- /dev/null
+{
+ "name": "org.evergreen_ils.hatch",
+ "description": "Hatch Native Messaging Host",
+ "path": "C:\\Path\\To\\hatch.bat",
+ "type": "stdio",
+ "allowed_origins": [
+ "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
+ ]
+}
--- /dev/null
+{
+ "name": "org.evergreen_ils.hatch",
+ "description": "Hatch Native Messaging Host",
+ "path": "/path/to/hatch.sh",
+ "type": "stdio",
+ "allowed_origins": [
+ "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
+ ]
+}
--- /dev/null
+@echo off
+REM Windows Hatch Execution Script
+REM @echo off required for STDIO to work with the browser.
+SET JAVA_HOME="C:\Program Files\Java\jdk1.8.0_111"
+SET JAVA=%JAVA_HOME%\bin\java
+SET JAVAC=%JAVA_HOME%\bin\javac
+
+IF "%1" == "compile" (
+
+ %JAVAC% -cp "lib\*" -Xdiags:verbose^
+ -d lib src\org\evergreen_ils\hatch\*.java
+
+) ELSE (
+
+ IF "%1" == "test" (
+
+ %JAVA% -cp "lib\*;lib"^
+ -Djava.util.logging.config.file=logging.properties^
+ org.evergreen_ils.hatch.TestHatch | %JAVA% -cp "lib\*;lib"^
+ -Djava.util.logging.config.file=logging.properties^
+ org.evergreen_ils.hatch.Hatch | %JAVA% -cp "lib\*;lib"^
+ -Djava.util.logging.config.file=logging.properties^
+ org.evergreen_ils.hatch.TestHatch receive
+
+ ) ELSE ( REM No options means run Hatch
+
+ %JAVA% -cp "lib\*;lib"^
+ -Djava.util.logging.config.file=logging.properties^
+ org.evergreen_ils.hatch.Hatch
+
+ )
+)
+
--- /dev/null
+#!/bin/bash
+#
+# Linux/Mac Hatch Execution Script
+
+JAVA_HOME=jdk1.8
+JAVA=$JAVA_HOME/bin/java
+JAVAC=$JAVA_HOME/bin/javac
+LOGS=-Djava.util.logging.config.file=logging.properties
+
+COMMAND="$1"
+
+if [ "$COMMAND" == "compile" ]; then
+
+ $JAVAC -Xdiags:verbose -Xlint:unchecked \
+ -cp lib:lib/\* -d lib src/org/evergreen_ils/hatch/*.java
+
+elif [ "$COMMAND" == "test" ]; then
+
+ # 1. Run TestHatch in (default) send mode, which emits JSON requests
+ # 2. Run Hatch and process messages emitted from #1.
+ # 3. Run TestHatch in receive mode to log the responses.
+
+ $JAVA "$LOGS" -cp lib:lib/\* org.evergreen_ils.hatch.TestHatch \
+ | $JAVA "$LOGS" -cp lib:lib/\* org.evergreen_ils.hatch.Hatch \
+ | $JAVA "$LOGS" -cp lib:lib/\* org.evergreen_ils.hatch.TestHatch receive
+
+else
+
+ # run Hatch
+ $JAVA "$LOGS" -cp lib:lib/\* org.evergreen_ils.hatch.Hatch
+fi;
+++ /dev/null
-<?xml version="1.0"?>
-<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
-
-<Configure id="Server" class="org.eclipse.jetty.server.Server">
-
- <!--
- <Get id="Logger" class="org.eclipse.jetty.util.log.Log" name="log"/>
- <Ref id="Logger">
- <Set name="debugEnabled">true</Set>
- </Ref>
- -->
-
- <Set class="org.evergreen_ils.hatch.HatchWebSocketHandler" name="trustedDomains">
- <Array type="String">
- <!--
- List of origin domains which are allowed to connect to Hatch.
- If the first item in the list is "*", then all domains are
- trusted, which is useful for testing.
- -->
- <Item>*</Item>
- </Array>
- </Set>
-
- <!--
- <Set class="org.evergreen_ils.hatch.HatchWebSocketHandler"
- name="profileDirectory"></Set>
- -->
-
- <!-- basic HTTP setup -->
- <New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
- <Set name="secureScheme">https</Set>
- <Set name="securePort"><Property name="jetty.secure.port" default="8443" /></Set>
- <Set name="outputBufferSize"><Property name="jetty.output.buffer.size" default="32768" /></Set>
- <Set name="requestHeaderSize"><Property name="jetty.request.header.size" default="8192" /></Set>
- <Set name="responseHeaderSize"><Property name="jetty.response.header.size" default="8192" /></Set>
- <Set name="sendServerVersion"><Property name="jetty.send.server.version" default="true" /></Set>
- <Set name="sendDateHeader"><Property name="jetty.send.date.header" default="false" /></Set>
- <Set name="headerCacheSize">512</Set>
- </New>
-
- <!-- SSL configuration -->
- <!-- Using the stock Jetty certificates for now.
- To set a temporary trust on the cert, navigate to
- https://<hostname>:8443/ and confirm the cert is trusted -->
- <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
- <!-- TODO: make this better -->
- <Set name="KeyStorePath"><Property name="jetty.home" default="." />/jetty/etc/keystore</Set>
- <Set name="KeyStorePassword">password</Set>
- <Set name="KeyManagerPassword">password</Set>
- <Set name="TrustStorePath"><Property name="jetty.home" default="." />/jetty/etc/keystore</Set>
- <Set name="TrustStorePassword">password</Set>
- </New>
-
- <New id="sslHttpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
- <Arg><Ref refid="httpConfig"/></Arg>
- <Call name="addCustomizer">
- <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
- </Call>
- </New>
-
- <!-- SSL HTTP connector -->
- <Call name="addConnector">
- <Arg>
- <New class="org.eclipse.jetty.server.ServerConnector">
- <Arg name="server"><Ref refid="Server" /></Arg>
- <Arg name="factories">
- <Array type="org.eclipse.jetty.server.ConnectionFactory">
- <Item>
- <New class="org.eclipse.jetty.server.SslConnectionFactory">
- <Arg name="next">http/1.1</Arg>
- <Arg name="sslContextFactory"><Ref refid="sslContextFactory"/></Arg>
- </New>
- </Item>
- <Item>
- <New class="org.eclipse.jetty.server.HttpConnectionFactory">
- <Arg name="config"><Ref refid="sslHttpConfig"/></Arg>
- </New>
- </Item>
- </Array>
- </Arg>
- <Set name="host"><Property name="jetty.host" /></Set>
- <Set name="port"><Property name="jetty.secure.port" default="8443" /></Set>
- <Set name="idleTimeout"><Property name="https.timeout" default="30000"/></Set>
- <Set name="soLingerTime"><Property name="https.soLingerTime" default="-1"/></Set>
- </New>
- </Arg>
- </Call>
-
- <!-- HTTP connector -->
- <Call name="addConnector">
- <Arg>
- <New class="org.eclipse.jetty.server.ServerConnector">
- <Arg name="server">
- <Ref refid="Server"/>
- </Arg>
- <Arg name="factories">
- <Array type="org.eclipse.jetty.server.ConnectionFactory">
- <Item>
- <New class="org.eclipse.jetty.server.HttpConnectionFactory">
- <Arg name="config"><Ref refid="httpConfig" /></Arg>
- </New>
- </Item>
- </Array>
- </Arg>
- <Set name="host"><Property name="jetty.host"/></Set>
- <Set name="port"><Property name="jetty.port" default="8080"/></Set>
- <Set name="idleTimeout"><Property name="http.timeout" default="30000"/></Set>
- <Set name="soLingerTime"><Property name="http.soLingerTime" default="-1"/></Set>
- </New>
- </Arg>
- </Call>
-
- <!-- TODO get properties working for:
- jetty.proxy.maxThreads
- jetty.proxy.maxConnections
- jetty.proxy.idleTimeout
- jetty.proxy.timeout
- -->
-
-
- <!-- wrap our websocketservlet into something the server can run -->
- <New id="context" class="org.eclipse.jetty.servlet.ServletContextHandler">
- <Set name="contextPath">/</Set>
- <Call name="addServlet">
- <Arg>org.evergreen_ils.hatch.HatchWebSocketServlet</Arg>
- <Arg>/hatch</Arg>
- </Call>
- </New>
-
- <!-- set our websocket handler as the server handler -->
- <Set name="handler">
- <New class="org.eclipse.jetty.server.handler.HandlerCollection">
- <Set name="handlers">
- <Array type="org.eclipse.jetty.server.Handler">
- <Item> <Ref refid="context" /> </Item>
- <Item>
- <New class="org.eclipse.jetty.server.handler.DefaultHandler" />
- </Item>
- </Array>
- </Set>
- </New>
- </Set>
-
-</Configure>
--- /dev/null
+
+# Log to console and file
+handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
+
+# This format is more consice than the default -- one line per message.
+java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$-6s %5$s%6$s%n
+java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
+
+# log files go to $SYSTEM_TMP/hatch.log
+java.util.logging.FileHandler.pattern = %h/.evergreen/hatch.log
+java.util.logging.FileHandler.limit = 50000000
+
+# Log everything everywhre
+org.evergreen_ils.hatch.level=ALL
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.FileHandler.level=ALL
+
+# Avoid duplicate logs
+java.util.logging.ConsoleHandler.useParentHandlers=false
+java.util.logging.FileHandler.useParentHandlers=false
+++ /dev/null
-SET JAVA_HOME="C:\Program Files\Java\jdk1.8.0_31"
-SET JETTY_HOME=jetty
-
-%JAVA_HOME%\bin\javac -cp "%JETTY_HOME%\lib\*;%JETTY_HOME%\lib\websocket\*;lib\*" -Xdiags:verbose -d lib src\org\evergreen_ils\hatch\*.java
-
-%JAVA_HOME%\bin\java -cp "%JETTY_HOME%\lib\*;%JETTY_HOME%\lib\websocket\*;lib\*;lib" org.evergreen_ils.hatch.Hatch
+++ /dev/null
-JAVA_HOME=jdk1.8
-JETTY_HOME=jetty
-
-# 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
import java.io.*;
import java.util.LinkedList;
import java.util.Arrays;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
+import java.util.logging.Logger;
public class FileIO {
// --------------------------------------------------
// logger
- private static final Logger logger = Log.getLogger("FileIO");
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
/**
* Constructs a new FileIO with the provided base path.
File dir = new File(basePath);
if (!dir.exists()) {
if (!dir.mkdir()) {
- logger.info("Unable to create directory: " + dir.getName());
+ //logger.info("Unable to create directory: " + dir.getName());
return null;
}
}
File subDir = new File(basePath, originDomain);
if (!subDir.exists()) {
if (!subDir.mkdir()) {
- logger.info("Unable to create directory: " + subDir.getName());
+ //logger.info("Unable to create directory: " + subDir.getName());
return null;
}
}
- logger.info("baseDir: " + subDir.getName());
+ //logger.info("baseDir: " + subDir.getName());
return subDir;
}
* @return success or failure
*/
public boolean set(String key, String text) {
- logger.info("set => " + key);
+ //logger.info("set => " + key);
File file = getFile(key);
if (text == null) return false;
// delete the file if it exists
if (!file.exists() && !file.createNewFile()) {
- logger.info(
- "Unable to create file: " + file.getCanonicalPath());
+ //logger.info(
+ //"Unable to create file: " + file.getCanonicalPath());
return false;
}
outStream.close();
} catch(IOException e) {
- logger.warn("Error calling set() with key " + key);
- logger.warn(e);
+ //logger.warn("Error calling set() with key " + key);
+ //logger.warn(e);
return false;
}
* @return success or failure
*/
public boolean append(String key, String text) {
- logger.info("append => " + key);
+ //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());
+ //logger.info(
+ //"Unable to create file: " + file.getCanonicalPath());
return false;
}
outStream.close();
} catch(IOException e) {
- logger.warn("Error in append() with key " + key);
- logger.warn(e);
+ //logger.warn("Error in append() with key " + key);
+ //logger.warn(e);
return false;
}
* @return The text content of the file
*/
public String get(String key) {
- logger.info("get => " + key);
+ //logger.info("get => " + key);
File file = getFile(key);
if (!file.exists()) return null;
buf.append(line);
}
} catch (IOException e) {
- logger.warn("Error reading key: " + key);
- logger.warn(e);
+ //logger.warn("Error reading key: " + key);
+ //logger.warn(e);
return null;
}
* @return success or failure
*/
public boolean remove(String key) {
- logger.info("remove => " + key);
+ //logger.info("remove => " + key);
File file = getFile(key);
- try {
+ //try {
if (file.exists() && !file.delete()) {
- logger.info(
- "Unable to delete file: " + file.getCanonicalPath());
+ //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;
- }
+ //} catch (IOException e) {
+ //logger.warn("Error deleting key: " + key);
+ //logger.warn(e);
+ //return false;
+ //}
}
/**
* @return Array of keys
*/
public String[] keys(String prefix) {
- logger.info("keys => " + prefix);
+ //logger.info("keys => " + prefix);
File dir = baseDir();
if (dir == null || !dir.exists())
*/
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.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.server.handler.HandlerList;
-import org.eclipse.jetty.util.resource.Resource;
-import org.eclipse.jetty.xml.XmlConfiguration;
+import java.util.Map;
+import java.util.logging.*;
+import org.json.*;
import javafx.application.Application;
import javafx.application.Platform;
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;
/**
* Main class for Hatch.
*
- * This class operates as a two-headed beast, whose heads will occasionally
- * communicate with each other.
- *
- * It runs a JavaFX thread for printing HTML documents and runs a Jetty
- * server thread for handling communication with external clients.
- *
- * Most of the work performed happens solely in the Jetty server thread.
- * Attempts to print, however, are passed into the JavaFX thread so that
- * the HTML may be loaded into a WebView for printing, which must happen
- * within the JavaFX thread.
- *
- * Messages are passed from the Jetty thread to the JavaFX thread via a
- * blocking thread queue, observed by a separate Service thread, whose
- * job is only to pull messages from the queue.
+ * TODO
*
* Beware: On Mac OS, the "FX Application Thread" is renamed to
* "AppKit Thread" when the first call to print() or showPrintDialog()
/** BrowserView requires a stage for rendering */
private Stage primaryStage;
- /** Our logger instance */
- static final Logger logger = Log.getLogger("Hatch");
-
- /** Message queue for passing messages from the Jetty thread into
- * the JavaFX Application thread */
- private static LinkedBlockingQueue<Map> requestQueue =
- new LinkedBlockingQueue<Map>();
+ /** Queue of incoming print requests */
+ private static LinkedBlockingQueue<JSONObject>
+ printRequestQueue = new LinkedBlockingQueue<JSONObject>();
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
+
/**
- * Printable region containing a browser
+ * Printable region containing a browser.
*/
class BrowserView extends Region {
WebView webView = new WebView();
}
/**
- * Service task which listens for inbound messages from the
- * servlet.
+ * Shuffles print requests from the request queue into the
+ * FX Platform queue.
*
- * The code blocks on the concurrent queue, so it must be
- * run in a separate thread to avoid locking the main FX thread.
+ * This step allows the code to process print requests in order
+ * and without nesting UI event loops.
*/
- private static class MsgListenService extends Service<Map<String,Object>> {
- protected Task<Map<String,Object>> createTask() {
- return new Task<Map<String,Object>>() {
- protected Map<String,Object> call() {
- while (true) {
- logger.info("MsgListenService waiting for a message...");
- try {
- // take() blocks until a message is available
- return requestQueue.take();
- } catch (InterruptedException e) {
- // interrupted, go back and listen
- continue;
- }
- }
+ class PrintRequestShuffler extends Thread {
+ public void run() {
+ while (true) {
+ try {
+ JSONObject printRequest = printRequestQueue.take();
+ Platform.runLater(
+ new Runnable() {
+ @Override public void run() {
+ handlePrint(printRequest);
+ }
+ }
+ );
+ } catch (InterruptedException ie) {
}
- };
+ }
}
}
+ /** Add a print request object to the print queue. */
+ public static void enqueuePrintRequest(JSONObject request) {
+ printRequestQueue.offer(request);
+ }
/**
* JavaFX startup call
@Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
- startMsgTask();
- }
-
- /**
- * Queues a message for processing by the queue processing thread.
- */
- public static void enqueueMessage(Map<String,Object> params) {
- logger.debug("queueing print message");
- requestQueue.offer(params);
+ new PrintRequestShuffler().start();
}
/**
* Build a browser view from the print content, tell the
* browser to print itself.
*/
- private void handlePrint(Map<String,Object> params) {
- String content = (String) params.get("content");
- String contentType = (String) params.get("contentType");
-
- if (content == null) {
- logger.warn("handlePrint() called with no content");
- return;
- }
+ private void handlePrint(JSONObject request) {
browser = new BrowserView();
Scene scene = new Scene(browser);
browser.webEngine.getLoadWorker()
.stateProperty()
- .addListener( (ChangeListener) (obsValue, oldState, newState) -> {
- logger.info("browser load state " + newState);
- if (newState == State.SUCCEEDED) {
- logger.info("Print browser page load completed");
+ .addListener( (ChangeListener<State>) (obsValue, oldState, newState) -> {
+ logger.finest("browser load state " + newState);
- // Avoid nested UI event loops -- runLater
- Platform.runLater(new Runnable() {
+ if (newState == State.SUCCEEDED) {
+ Platform.runLater(new Runnable() { // Avoid nested events
@Override public void run() {
- new PrintManager().print(browser.webEngine, params);
+ new PrintManager().print(browser.webEngine, request);
}
});
}
});
- logger.info("printing " + content.length() + " bytes of " + contentType);
- browser.webEngine.loadContent(content, contentType);
+ try {
- // After queueing up the HTML for printing, go back to listening
- // for new messages.
- startMsgTask();
- }
-
- /**
- * Fire off the Service task, which checks for queued messages.
- *
- * When a queued message is found, it's sent off for printing.
- */
- public void startMsgTask() {
-
- MsgListenService service = new MsgListenService();
-
- logger.info("starting MsgTask");
-
- service.setOnSucceeded(
- new EventHandler<WorkerStateEvent>() {
-
- @Override
- public void handle(WorkerStateEvent t) {
- logger.info("MsgTask handling message.. ");
- Map<String,Object> message =
- (Map<String,Object>) t.getSource().getValue();
+ String content = request.getString("content");
+ String contentType = request.getString("contentType");
+
+ logger.info("printing " +
+ content.length() + " bytes of " + contentType);
- // avoid nesting UI event loops by kicking off the print
- // operation from the main FX loop after this event handler
- // has exited.
- Platform.runLater(
- new Runnable() {
- @Override public void run() {
- handlePrint(message);
- }
- }
- );
- }
- });
+ browser.webEngine.loadContent(content, contentType);
- service.start();
+ } catch (JSONException je) {
+ // RequestHandler already confirmed 'content' and 'contentType'
+ // values exist. No exceptions should occur here.
+ }
}
+
/**
* Hatch main.
*
- * Reads the Jetty configuration, starts the Jetty server thread,
- * then launches the JavaFX Application thread.
*/
public static void main(String[] args) throws Exception {
-
- // build a server from our hatch.xml configuration file
- XmlConfiguration configuration =
- new XmlConfiguration(new FileInputStream("hatch.xml"));
-
- Server server = (Server) configuration.configure();
-
- logger.info("Starting Jetty server");
-
- // start our server, but do not join(), since we want to server
- // to continue running in its own thread
- server.start();
-
- logger.info("Launching FX Application");
-
- // launch the FX Application thread
- launch(args);
+ new RequestHandler().start(); // start the STDIO handlers.
+ launch(args); // launch the FX Application thread
}
}
+++ /dev/null
-/* -----------------------------------------------------------------------
- * Copyright 2014 Equinox Software, Inc.
- * Bill Erickson <berick@esilibrary.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * -----------------------------------------------------------------------
- */
-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 {
-
- /** A single connection to a WebSockets client */
- private Session session;
-
- /** Current origin domain */
- private String origin;
-
- /** List of Origin domains from which we allow connections */
- private static String[] trustedDomains;
-
- /** True if we trust all Origin domains */
- private static boolean trustAllDomains = false;
-
- /** Root directory for all FileIO operations */
- private static String profileDirectory;
-
- /** Our logger instance */
- private static final Logger logger = Log.getLogger("WebSocketHandler");
-
- /**
- * Apply trusted domains.
- *
- * If the first domain in the list equals "*", that signifies that
- * all domains should be trusted.
- *
- * @param domains Array of domains to trust.
- */
- public static void setTrustedDomains(String[] domains) {
- trustedDomains = domains;
-
- if (domains.length > 0 ) {
-
- if ("*".equals(domains[0])) {
- logger.info("All domains trusted");
- trustAllDomains = true;
-
- } else {
-
- for(String domain : trustedDomains) {
- logger.info("Trusted domain: " + domain);
- }
- }
- } else {
- logger.warn("No domains are trusted. All requests will be denied");
- }
- }
-
- /**
- * Sets the profile directory
- *
- * @param directory Directory path as a String
- */
- public static void setProfileDirectory(String directory) {
- profileDirectory = directory;
- }
-
-
- /**
- * Runs the initial, global configuration for this handler.
- * TODO: move this into setProfileDirectory() (which will need to
- * be force-called regardless of config)?
- */
- public static void configure() {
- logger.info("WebSocketHandler.configure()");
-
- // 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");
- }
- }
- }
-
- /**
- * Compares the Origin of the current WebSocket connection to the list
- * of allowed domains to determine if the current connection should
- * be allowed.
- *
- * @return True if the Origin domain is allowed, false otherwise.
- */
- protected boolean verifyOriginDomain() {
- logger.info("received connection from IP " +
- session.getRemoteAddress().getAddress());
-
- 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;
- }
-
-
- /**
- * WebSocket onConnect handler.
- *
- * Verify the Origin domain before any communication may take place
- */
- @OnWebSocketConnect
- public void onConnect(Session session) {
- this.session = session;
- if (!verifyOriginDomain()) session.close();
- }
-
- /**
- * WebSocket onClose handler.
- *
- * Clears our current session.
- */
- @OnWebSocketClose
- public void onClose(int statusCode, String reason) {
- logger.info("onClose() statusCode=" + statusCode + ", reason=" + reason);
- this.session = null;
- }
-
- /**
- * Send a message to our connected client.
- *
- * @param json A JSON-encodable object to send to the caller.
- * @param msgid The message identifier
- */
- protected void reply(Object json, Long msgid) {
- reply(json, msgid, true);
- }
-
- /**
- * Send a message to our connected client.
- *
- * @param json A JSON-encodable object to send to the caller.
- * @param msgid The message identifier
- * @param success If false, the response will be packaged as an error
- * message.
- */
- protected void reply(Object json, Long msgid, boolean success) {
-
- Map<String, Object> response = new HashMap<String, Object>();
- response.put("msgid", msgid);
-
- if (success) {
- response.put("content", json);
- } else {
- response.put("error", json);
- }
-
- String jsonString = JSON.toString(response);
- logger.info("replying with : " + jsonString);
-
- try {
- if (!success) logger.warn(jsonString);
- session.getRemote().sendString(jsonString);
- } catch (IOException e) {
- logger.warn(e);
- }
- }
-
- /**
- * WebSocket onMessage handler.
- *
- * Processes the incoming message and passes the request off to the
- * necessary handler. Messages must be encoded as JSON strings.
- */
- @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,Object> params = null;
-
- try {
- params = (HashMap<String,Object>) JSON.parse(message);
- } catch (ClassCastException e) {
- reply("Invalid WebSockets JSON message " + message,
- new Long(-1), false);
- }
-
- Long msgid = (Long) params.get("msgid");
- String action = (String) params.get("action");
- String key = (String) params.get("key");
- String value = (String) params.get("value");
- String mime = (String) params.get("mime");
-
- logger.info("Received request for action " + action);
-
- // all requets require a message ID
- if (msgid == null) {
- 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;
- }
-
- Object response = null;
- boolean error = false;
- FileIO io = new FileIO(profileDirectory, origin);
-
- switch (action) {
- case "keys":
- response = io.keys(key);
- break;
-
- case "printers":
- response = new PrintManager().getPrintersAsMaps();
- break;
-
- case "print":
- // pass ourselves off to the print handler so it can reply
- // for us after printing has completed.
- params.put("socket", this);
- Hatch.enqueueMessage(params);
-
- // we don't want to return a response below, since the
- // FX thread will handle that for us.
- return;
-
- case "print-config":
- try {
- response = new PrintManager().configurePrinter(params);
- } catch(IllegalArgumentException e) {
- response = e.toString();
- error = true;
- }
- break;
-
- case "get":
- String val = io.get(key);
- if (val != null) {
- // set() stores bare JSON. We must pass an
- // Object to reply so that it may be embedded into
- // a larger JSON response object, hence the JSON.parse().
- try {
- response = JSON.parse(val);
- } catch(java.lang.IllegalStateException e) {
- error = true;
- response = "Error JSON-parsing stored value " + val;
- }
- }
- break;
-
- case "remove":
- response = io.remove(key);
- break;
-
- case "set" :
- response = io.set(key, value);
- break;
-
- case "append" :
- response = io.append(key, value);
- break;
-
- default:
- response = "No such action: " + action;
- error = true;
- }
-
- reply(response, msgid, !error);
- }
-}
+++ /dev/null
-/* -----------------------------------------------------------------------
- * Copyright 2014 Equinox Software, Inc.
- * Bill Erickson <berick@esilibrary.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * -----------------------------------------------------------------------
- */
-package org.evergreen_ils.hatch;
-
-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;
-
-/**
- * Links HatchWebSocketHandler in as a Servlet handler.
- */
-public class HatchWebSocketServlet extends 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();
- }
-}
-
--- /dev/null
+/* -----------------------------------------------------------------------
+ * Copyright 2016 King County Library System
+ * Bill Erickson <berickxx@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * -----------------------------------------------------------------------
+ */
+package org.evergreen_ils.hatch;
+
+import java.util.logging.*;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.nio.ByteBuffer;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import org.json.*;
+
+/**
+ * Reads and writes JSON strings from STDIN / to STDOUT.
+ *
+ * Each string is prefixed with a 4-byte message length header. All I/O
+ * occurs in a separate thread, so no blocking of the main thread occurs.
+ */
+public class MessageIO {
+
+ private LinkedBlockingQueue<JSONObject> inQueue;
+ private LinkedBlockingQueue<JSONObject> outQueue;
+
+ private MessageReader reader;
+ private MessageWriter writer;
+
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
+
+ public MessageIO() {
+ inQueue = new LinkedBlockingQueue<JSONObject>();
+ outQueue = new LinkedBlockingQueue<JSONObject>();
+ reader = new MessageReader();
+ writer = new MessageWriter();
+ }
+
+ /**
+ * Starts the read and write threads.
+ */
+ public void listen() {
+ writer.start();
+ reader.start();
+ }
+
+ /**
+ * Receive one message from STDIN.
+ *
+ * This call blocks the current thread until a message is available.
+ */
+ public JSONObject recvMessage() {
+ while (true) {
+ try {
+ return inQueue.take();
+ } catch (InterruptedException e) {}
+ }
+ }
+
+ /**
+ * Queue a message for sending to STDOUT.
+ */
+ public void sendMessage(JSONObject msg) {
+ outQueue.offer(msg);
+ }
+
+ /**
+ * Thrown when STDIN or STDOUT are closed.
+ */
+ class EndOfStreamException extends IOException { }
+
+ /**
+ * Reads JSON-encoded strings from STDIN.
+ *
+ * As messages arrive, they are enqueued for access by recvMessage().
+ *
+ * Each message is prefixed with a 4-byte message length header.
+ */
+ class MessageReader extends Thread {
+
+ /**
+ * Converts a 4-byte array to its integer value.
+ */
+ private int bytesToInt(byte[] bytes) {
+ return
+ (bytes[3] << 24) & 0xff000000
+ | (bytes[2] << 16) & 0x00ff0000
+ | (bytes[1] << 8) & 0x0000ff00
+ | (bytes[0] << 0) & 0x000000ff;
+ }
+
+ /**
+ * Reads one message from STDIN.
+ *
+ * This method blocks until a message is available.
+ */
+ private String readOneMessage() throws EndOfStreamException, IOException {
+ byte[] lenBytes = new byte[4];
+ int bytesRead = System.in.read(lenBytes);
+
+ if (bytesRead == -1) {
+ throw new EndOfStreamException();
+ }
+
+ int msgLength = bytesToInt(lenBytes);
+
+ if (msgLength == 0) {
+ throw new IOException(
+ "Inbound message is 0 bytes. I/O interrupted?");
+ }
+
+ byte[] msgBytes = new byte[msgLength];
+
+ bytesRead = System.in.read(msgBytes);
+
+ if (bytesRead == -1) {
+ throw new EndOfStreamException();
+ }
+
+ String message = new String(msgBytes, "UTF-8");
+
+ logger.finest("MessageReader read: " + message);
+
+ return message;
+ }
+
+ /**
+ * Read messages from STDIN until STDIN is closed or the application exits.
+ */
+ public void run() {
+
+ while (true) {
+
+ String message = "";
+ JSONObject jsonMsg = null;
+
+ try {
+
+ message = readOneMessage();
+ jsonMsg = new JSONObject(message);
+
+ } catch (EndOfStreamException eose) {
+
+ logger.warning("STDIN closed... exiting");
+ System.exit(1);
+
+ } catch (IOException ioe) {
+
+ logger.warning(ioe.toString());
+ continue;
+
+ } catch (JSONException je) {
+
+ logger.warning("Error parsing JSON message on STDIN " +
+ je.toString() + " : " + message);
+ continue;
+ }
+
+ inQueue.offer(jsonMsg);
+ }
+ }
+ }
+
+ /**
+ * Writes JSON-encoded strings from STDOUT.
+ *
+ * As messages are queued for delivery, each is serialized as a JSON
+ * string and stamped with a 4-byte length header.
+ */
+ class MessageWriter extends Thread {
+
+ /**
+ * Returns the 4-byte array representation of an integer.
+ */
+ private byte[] intToBytes(int length) {
+ byte[] bytes = new byte[4];
+ bytes[0] = (byte) (length & 0xFF);
+ bytes[1] = (byte) ((length >> 8) & 0xFF);
+ bytes[2] = (byte) ((length >> 16) & 0xFF);
+ bytes[3] = (byte) ((length >> 24) & 0xFF);
+ return bytes;
+ }
+
+ /**
+ * Encodes and writes one message to STDOUT.
+ */
+ public void writeOneMessage(String message) throws IOException {
+ logger.finest("MessageWriter sending: " + message);
+ System.out.write(intToBytes(message.getBytes("UTF-8").length));
+ System.out.write(message.getBytes("UTF-8"));
+ System.out.flush();
+ }
+
+ /**
+ * Waits for messages to be queued for delivery and writes
+ * each to STDOUT until STDOUT is closed or the application exits.
+ */
+ public void run() {
+
+ while (true) {
+ try {
+
+ // take() blocks the thread until a message is available
+ JSONObject jsonMsg = outQueue.take();
+
+ writeOneMessage(jsonMsg.toString());
+
+ } catch (InterruptedException e) {
+ // interrupted, go back and listen
+ continue;
+ } catch (IOException ioe) {
+ logger.warning(
+ "Error writing message to STDOUT: " + ioe.toString());
+ }
+ }
+ }
+ }
+}
+
*/
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 java.lang.IllegalArgumentException;
// data structures
+import java.util.Set;
import java.util.Map;
import java.util.List;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
-import java.util.Set;
-import java.util.LinkedHashSet;
+
+import java.util.logging.Logger;
+
+import org.json.*;
public class PrintManager {
/** Our logger instance */
- static final Logger logger = Log.getLogger("PrintManager");
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
+
+ /**
+ * Returns all known Printer's.
+ *
+ * @return Array of all printers
+ */
+ protected Printer[] getPrinters() {
+ ObservableSet<Printer> printerObserver = Printer.getAllPrinters();
+
+ if (printerObserver == null) return new Printer[0];
+
+ return (Printer[]) printerObserver.toArray(new Printer[0]);
+ }
/**
- * Shows the print dialog, allowing the user to modify settings,
- * but performs no print.
+ * Returns a list of all known printers, with their attributes
+ * encoded as a simple key/value Map.
*
- * @param params Print request parameters. This is the top-level
- * request object, containing the action, etc. Within 'params'
- * will be a sub-object under the "config" key, which contains
- * the Printer configuration options (if any are already set).
- * @return A Map of printer settings extracted from the print dialog.
+ * @return Map of printer information.
*/
- public Map<String,Object> configurePrinter(
- Map<String,Object> params) throws IllegalArgumentException {
+ protected List<Map<String,Object>> getPrintersAsMaps() {
+ Printer[] printers = getPrinters();
+
+ List<Map<String,Object>> printerMaps =
+ new LinkedList<Map<String,Object>>();
+
+ Printer defaultPrinter = Printer.getDefaultPrinter();
- Map<String,Object> settings =
- (Map<String,Object>) params.get("config");
+ for (Printer printer : printers) {
+ HashMap<String, Object> printerMap = new HashMap<String, Object>();
+ printerMaps.add(printerMap);
+ printerMap.put("name", printer.getName());
+ if (defaultPrinter != null &&
+ printer.getName().equals(defaultPrinter.getName())) {
+ printerMap.put("is-default", new Boolean(true));
+ }
+ }
- PrinterJob job = buildPrinterJob(settings);
-
- boolean approved = job.showPrintDialog(null);
+ return printerMaps;
+ }
- // no printing needed
- job.endJob();
- if (approved) {
- // extract modifications to the settings applied within the dialog
- return extractSettingsFromJob(job);
- } else {
- // return the unmodified settings back to the caller
- return settings;
+ /**
+ * Returns the Printer with the specified name.
+ *
+ * @param name The printer name
+ * @return The printer whose name matches the provided name, or null
+ * if no such printer is found.
+ */
+ protected Printer getPrinterByName(String name) {
+ Printer[] printers = getPrinters();
+ for (Printer printer : printers) {
+ if (printer.getName().equals(name))
+ return printer;
}
+ return null;
}
+
/**
* Print the requested page using the provided settings
*
* @param engine The WebEngine instance to print
* @param params Print request parameters
*/
- public void print(WebEngine engine, Map<String,Object>params) {
-
- Long msgid = (Long) params.get("msgid");
- Boolean showDialog = (Boolean) params.get("showDialog");
+ public void print(WebEngine engine, JSONObject request) {
- Map<String,Object> settings =
- (Map<String,Object>) params.get("config");
+ JSONObject response = new JSONObject();
+ response.put("status", 200);
+ response.put("message", "OK");
- HatchWebSocketHandler socket =
- (HatchWebSocketHandler) params.get("socket");
+ try {
+ response.put("clientid", request.getLong("clientid"));
+ response.put("msgid", request.getLong("msgid"));
- PrinterJob job = null;
+ boolean showDialog = request.optBoolean("showDialog");
- try {
- job = buildPrinterJob(settings);
- } catch(IllegalArgumentException e) {
- socket.reply(e.toString(), msgid, false);
- return;
- }
+ // if no "settings" are applied, use defaults.
+ JSONObject settings = request.optJSONObject("settings");
+ if (settings == null) settings = new JSONObject();
- if (showDialog != null && showDialog.booleanValue()) {
- logger.info("Print dialog requested");
+ PrinterJob job = buildPrinterJob(settings);
- if (!job.showPrintDialog(null)) {
- // job canceled by user
- logger.info("after dialog");
- job.endJob();
- socket.reply("Print job canceled", msgid);
- return;
+ if (showDialog) {
+ if (!job.showPrintDialog(null)) {
+ job.endJob(); // canceled by user
+ response.put("status", 200);
+ response.put("message", "Print job canceled by user");
+ RequestHandler.reply(response);
+ return;
+ }
}
- } else {
- logger.info("No print dialog requested");
- }
- Thread[] all = new Thread[100];
- int count = Thread.currentThread().enumerate(all);
- logger.info(count + " active threads in print");
- logger.info("Thread " + Thread.currentThread().getId() + " printing...");
+ engine.print(job);
+ job.endJob();
+ response.put("message", "Print job queued");
+ // TODO: support watching the print job until it completes
+
+ } catch (JSONException je) {
+
+ String error = "JSON request protocol error: "
+ + je.toString() + " : " + request.toString();
+
+ logger.warning(error);
+ response.put("status", 400);
+ response.put("message", error);
+
+ } catch(IllegalArgumentException iae) {
- engine.print(job);
- logger.info("after print");
+ String error = "Illegal argument in print request: "
+ + iae.toString() + " : " + request.toString();
- job.endJob();
+ logger.warning(error);
+ response.put("status", 400);
+ response.put("message", error);
+ }
- socket.reply("Print job succeeded", msgid);
+ RequestHandler.reply(response);
}
/**
* @return The newly created printer job.
*/
public PrinterJob buildPrinterJob(
- Map<String,Object> settings) throws IllegalArgumentException {
+ JSONObject settings) throws IllegalArgumentException {
- String name = (String) settings.get("printer");
- Printer printer = getPrinterByName(name);
+ Printer printer;
+ if (settings.has("printer")) {
+ String name = settings.getString("printer");
+ printer = getPrinterByName(name);
+ if (printer == null)
+ throw new IllegalArgumentException("No such printer: " + name);
- if (printer == null)
- throw new IllegalArgumentException("No such printer: " + name);
+ } else {
+ printer = Printer.getDefaultPrinter();
+ if (printer == null)
+ throw new IllegalArgumentException(
+ "No printer specified; no default printer is set");
+ }
PageLayout layout = buildPageLayout(settings, printer);
PrinterJob job = PrinterJob.createPrinterJob(printer);
* @return The newly constructed PageLayout object.
*/
protected PageLayout buildPageLayout(
- Map<String,Object> settings, Printer printer) {
-
- // modify the default page layout with our settings
- Map<String,Object> layoutMap =
- (Map<String,Object>) settings.get("pageLayout");
-
- if (layoutMap == null) {
- // Start with a sane default.
- // The Java default is wonky
- return printer.createPageLayout(
- Paper.NA_LETTER,
- PageOrientation.PORTRAIT,
- Printer.MarginType.DEFAULT
- );
- }
+ JSONObject settings, Printer printer) {
PrinterAttributes printerAttrs = printer.getPrinterAttributes();
- // find the paper by name
- Paper paper = null;
- String paperName = (String) layoutMap.get("paper");
- Set<Paper> papers = printerAttrs.getSupportedPapers();
- for (Paper source : papers) {
- if (source.getName().equals(paperName)) {
- logger.info("Found matching paper for " + paperName);
- paper = source;
- break;
+ // Start with default page layout options, replace where possible.
+ Paper paper = printerAttrs.getDefaultPaper();
+ PageOrientation orientation = printerAttrs.getDefaultPageOrientation();
+
+ String paperName = settings.optString("paper", null);
+ String orientationName = settings.optString("pageOrientation", null);
+ String marginName = settings.optString("marginType", null);
+
+ if (paperName != null) {
+ for (Paper source : printerAttrs.getSupportedPapers()) {
+ if (source.getName().equals(paperName)) {
+ logger.finer("Found matching paper: " + paperName);
+ paper = source;
+ break;
+ }
}
}
- if (paper == null)
- paper = printerAttrs.getDefaultPaper();
+ if (orientationName != null) {
+ orientation = PageOrientation.valueOf(orientationName);
+ }
+
+ if (settings.optBoolean("autoMargins", true)) {
+ // Using a pre-defined, automatic margin option
+
+ Printer.MarginType margin = Printer.MarginType.DEFAULT;
+ if (marginName != null) {
+ // An auto-margin type has been specified
+ for (Printer.MarginType marg : Printer.MarginType.values()) {
+ if (marg.toString().equals(marginName)) {
+ logger.finer("Found matching margin: " + marginName);
+ margin = marg;
+ break;
+ }
+ }
+ }
+
+ return printer.createPageLayout(paper, orientation, margin);
+ }
+ // Using manual margins
+ // Any un-specified margins default to 54 == 0.75 inches.
return printer.createPageLayout(
- paper,
- PageOrientation.valueOf((String) layoutMap.get("pageOrientation")),
- ((Number) layoutMap.get("leftMargin")).doubleValue(),
- ((Number) layoutMap.get("rightMargin")).doubleValue(),
- ((Number) layoutMap.get("topMargin")).doubleValue(),
- ((Number) layoutMap.get("bottomMargin")).doubleValue()
+ paper, orientation,
+ settings.optDouble("leftMargin", 54),
+ settings.optDouble("rightMargin", 54),
+ settings.optDouble("topMargin", 54),
+ settings.optDouble("bottomMargin", 54)
);
}
* @param settings The printer configuration settings map.
* @param job A PrinterJob, constructed from buildPrinterJob()
*/
- protected void applySettingsToJob(
- Map<String,Object> settings, PrinterJob job) {
+ protected void applySettingsToJob(JSONObject settings, PrinterJob job) {
JobSettings jobSettings = job.getJobSettings();
PrinterAttributes printerAttrs =
job.getPrinter().getPrinterAttributes();
- String collation = (String) settings.get("collation");
- Long copies = (Long) settings.get("copies");
- String printColor = (String) settings.get("printColor");
- String printQuality = (String) settings.get("printQuality");
- String printSides = (String) settings.get("printSides");
- String paperSource = (String) settings.get("paperSource");
- Object[] pageRanges = (Object[]) settings.get("pageRanges");
+ if (settings.has("collation")) {
+ jobSettings.setCollation(
+ Collation.valueOf(settings.getString("collation")));
+ }
+
+ if (settings.has("copies")) {
+ jobSettings.setCopies(settings.getInt("copies"));
+ }
- if (collation != null)
- jobSettings.setCollation(Collation.valueOf(collation));
+ if (settings.has("printColor")) {
+ jobSettings.setPrintColor(
+ PrintColor.valueOf(settings.getString("printColor")));
+ }
- if (copies != null)
- jobSettings.setCopies(((Long) settings.get("copies")).intValue());
+ if (settings.has("printQuality")) {
+ jobSettings.setPrintQuality(
+ PrintQuality.valueOf(settings.getString("printQuality")));
+ }
- if (printColor != null)
- jobSettings.setPrintColor(PrintColor.valueOf(printColor));
+ if (settings.has("printSides")) {
+ jobSettings.setPrintSides(
+ PrintSides.valueOf(settings.getString("printSides")));
- if (printQuality != null)
- jobSettings.setPrintQuality(PrintQuality.valueOf(printQuality));
+ }
- if (printSides != null)
- jobSettings.setPrintSides(PrintSides.valueOf(printSides));
+ String paperSource = settings.optString("paperSource");
- // find the paperSource by name
if (paperSource != null) {
- Set<PaperSource> paperSources =
- printerAttrs.getSupportedPaperSources();
-
- // note: "Automatic" appears to be a virtual source,
- // meaning no source.. meaning let the printer decide.
- for (PaperSource source : paperSources) {
+ for (PaperSource source : printerAttrs.getSupportedPaperSources()) {
if (source.getName().equals(paperSource)) {
- logger.info("matched paper source for " + paperSource);
+ logger.finer("Found paper source: " + paperSource);
jobSettings.setPaperSource(source);
break;
}
}
}
+ if (!settings.optBoolean("allPages", true)) {
+ JSONArray pageRanges = settings.optJSONArray("pageRanges");
- if (pageRanges != null) {
- logger.info("pageRanges = " + pageRanges.toString());
- List<PageRange> builtRanges = new LinkedList<PageRange>();
- int i = 0, start = 0, end = 0;
- do {
- if (i % 2 == 0 && i > 0)
- builtRanges.add(new PageRange(start, end));
-
- if (i == pageRanges.length) break;
-
- int current = ((Long) pageRanges[i]).intValue();
- if (i % 2 == 0) start = current; else end = current;
-
- } while (++i > 0);
-
- jobSettings.setPageRanges(builtRanges.toArray(new PageRange[0]));
- }
- }
+ if (pageRanges != null) {
+ List<PageRange> builtRanges = new LinkedList<PageRange>();
+ int i = 0, start = 0, end = 0;
+ do {
+ if (i % 2 == 0 && i > 0)
+ builtRanges.add(new PageRange(start, end));
- /**
- * Extracts and flattens the various configuration values from a
- * PrinterJob and its associated printer and stores the values in a Map.
- *
- * @param job The PrinterJob whose attributes are to be extracted.
- * @return The extracted printer settings map.
- */
- protected Map<String,Object> extractSettingsFromJob(PrinterJob job) {
- Map<String,Object> settings = new HashMap<String,Object>();
- JobSettings jobSettings = job.getJobSettings();
+ if (i == pageRanges.length()) break;
- logger.info("Extracting print job settings from " + job);
+ int current = pageRanges.getInt(i);
+ if (i % 2 == 0) start = current; else end = current;
- settings.put(
- jobSettings.collationProperty().getName(),
- jobSettings.collationProperty().getValue()
- );
- settings.put(
- jobSettings.copiesProperty().getName(),
- jobSettings.copiesProperty().getValue()
- );
- settings.put(
- "paperSource",
- jobSettings.getPaperSource().getName()
- );
- settings.put(
- jobSettings.printColorProperty().getName(),
- jobSettings.printColorProperty().getValue()
- );
- settings.put(
- jobSettings.printQualityProperty().getName(),
- jobSettings.printQualityProperty().getValue()
- );
- settings.put(
- jobSettings.printSidesProperty().getName(),
- jobSettings.printSidesProperty().getValue()
- );
+ } while (++i > 0);
- // nested properties...
-
- // page layout --------------
- PageLayout layout = jobSettings.getPageLayout();
- Map<String,Object> layoutMap = new HashMap<String,Object>();
- layoutMap.put("bottomMargin", layout.getBottomMargin());
- layoutMap.put("leftMargin", layout.getLeftMargin());
- layoutMap.put("topMargin", layout.getTopMargin());
- layoutMap.put("rightMargin", layout.getRightMargin());
- layoutMap.put("pageOrientation", layout.getPageOrientation().toString());
- layoutMap.put("printableHeight", layout.getPrintableHeight());
- layoutMap.put("printableWidth", layout.getPrintableWidth());
- layoutMap.put("paper", layout.getPaper().getName());
-
- settings.put("pageLayout", layoutMap);
-
- // page ranges --------------
- PageRange[] ranges = jobSettings.getPageRanges();
- if (ranges != null) {
- List<Integer> pageRanges = new LinkedList<Integer>();
-
- if (ranges.length == 1 &&
- ranges[0].getStartPage() == 1 &&
- ranges[0].getEndPage() == Integer.MAX_VALUE) {
- // full range -- no need to store
-
- } else {
- for (PageRange range : ranges) {
- pageRanges.add(range.getStartPage());
- pageRanges.add(range.getEndPage());
- }
- settings.put("pageRanges", pageRanges);
+ jobSettings.setPageRanges(
+ builtRanges.toArray(new PageRange[0]));
}
}
-
- logger.info("compiled printer properties: " + settings.toString());
- return settings;
}
- /**
- * Returns all known Printer's.
- *
- * @return Array of all printers
- */
- protected Printer[] getPrinters() {
- ObservableSet<Printer> printerObserver = Printer.getAllPrinters();
-
- if (printerObserver == null) return new Printer[0];
+ public JSONObject getPrintersOptions(String printerName) {
+ Printer printer;
- return (Printer[]) printerObserver.toArray(new Printer[0]);
- }
+ if (printerName == null) { // no name provided, use default.
+ printer = Printer.getDefaultPrinter();
+ } else {
+ printer = getPrinterByName(printerName);
+ }
- /**
- * Returns a list of all known printers, with their attributes
- * encoded as a simple key/value Map.
- *
- * @return Map of printer information.
- */
- protected List<Map<String,Object>> getPrintersAsMaps() {
- Printer[] printers = getPrinters();
+ if (printer == null) return null;
- List<Map<String,Object>> printerMaps =
- new LinkedList<Map<String,Object>>();
+ JSONObject options = new JSONObject();
+ PrinterAttributes printerAttrs = printer.getPrinterAttributes();
- Printer defaultPrinter = Printer.getDefaultPrinter();
+ JSONArray papersArray = new JSONArray();
+ for (Paper source : printerAttrs.getSupportedPapers()) {
+ papersArray.put(source.getName());
+ }
+ options.put("paper", papersArray);
+ options.put("defaultPaper",
+ printerAttrs.getDefaultPaper().getName());
+
+ JSONArray paperSourcesArray = new JSONArray();
+ for (PaperSource source :
+ printerAttrs.getSupportedPaperSources()) {
+ paperSourcesArray.put(source.getName());
+ }
+ options.put("paperSource", paperSourcesArray);
+ options.put("defaultPaperSource",
+ printerAttrs.getDefaultPaperSource().getName());
+
+ JSONArray collationsArray = new JSONArray();
+ for (Collation collation :
+ printerAttrs.getSupportedCollations()) {
+ collationsArray.put(collation.toString());
+ }
+ options.put("collation", collationsArray);
+ options.put("defaultCollation",
+ printerAttrs.getDefaultCollation().toString());
+
+ JSONArray colorsArray = new JSONArray();
+ for (PrintColor color :
+ printerAttrs.getSupportedPrintColors()) {
+ colorsArray.put(color.toString());
+ }
+ options.put("printColor", colorsArray);
+ options.put("defaultPrintColor",
+ printerAttrs.getDefaultPrintColor().toString());
+
+ JSONArray qualityArray = new JSONArray();
+ for (PrintQuality quality :
+ printerAttrs.getSupportedPrintQuality()) {
+ qualityArray.put(quality.toString());
+ }
+ options.put("printQuality", qualityArray);
+ options.put("defaultPrintQuality",
+ printerAttrs.getDefaultPrintQuality().toString());
- for (Printer printer : printers) {
- HashMap<String, Object> printerMap = new HashMap<String, Object>();
- printerMaps.add(printerMap);
- printerMap.put("name", printer.getName());
- if (defaultPrinter != null &&
- printer.getName().equals(defaultPrinter.getName())) {
- printerMap.put("is-default", new Boolean(true));
- }
- logger.info("found printer " + printer.getName());
+ JSONArray sidesArray = new JSONArray();
+ for (PrintSides side : printerAttrs.getSupportedPrintSides()) {
+ sidesArray.put(side.toString());
+ }
+ options.put("printSides", sidesArray);
+ options.put("defaultPrintSides",
+ printerAttrs.getDefaultPrintSides().toString());
+
+ JSONArray orientsArray = new JSONArray();
+ for (PageOrientation orient :
+ printerAttrs.getSupportedPageOrientations()) {
+ orientsArray.put(orient.toString());
}
+ options.put("pageOrientation", orientsArray);
+ options.put("defaultPageOrientation",
+ printerAttrs.getDefaultPageOrientation().toString());
- return printerMaps;
- }
+ JSONArray marginsArray = new JSONArray();
+ for (Printer.MarginType margin : Printer.MarginType.values()) {
+ marginsArray.put(margin.toString());
+ }
+ options.put("marginType", marginsArray);
+ options.put("defaultMarginType", Printer.MarginType.DEFAULT);
+ options.put("supportsPageRanges", printerAttrs.supportsPageRanges());
+ options.put("defaultCopies", printerAttrs.getDefaultCopies());
- /**
- * Returns the Printer with the specified name.
- *
- * @param name The printer name
- * @return The printer whose name matches the provided name, or null
- * if no such printer is found.
- */
- protected Printer getPrinterByName(String name) {
- Printer[] printers = getPrinters();
- for (Printer printer : printers) {
- if (printer.getName().equals(name))
- return printer;
- }
- return null;
+ return options;
}
+
}
--- /dev/null
+/* -----------------------------------------------------------------------
+ * Copyright 2016 King County Library System
+ * Bill Erickson <berickxx@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * -----------------------------------------------------------------------
+ */
+package org.evergreen_ils.hatch;
+
+import org.json.*;
+import java.io.File;
+import java.util.logging.*;
+
+/**
+ * Dispatches requests received via MessageIO, sends responses back
+ * via MessageIO.
+ */
+public class RequestHandler extends Thread {
+
+ /** STDIN/STDOUT handler */
+ private static MessageIO io = new MessageIO();
+
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
+
+ /** Root directory for all FileIO operations */
+ private static String profileDirectory = null;
+
+ private void configure() {
+
+ // Find the profile directory.
+ // The profile directory + origin string represent the base
+ // directory for all file I/O for this session.
+ if (profileDirectory == null) { // TODO: make configurable
+ String home = System.getProperty("user.home");
+ profileDirectory = new File(home, ".evergreen").getPath();
+ if (profileDirectory == null) {
+ logger.warning("Unable to set profile directory");
+ }
+ }
+ }
+
+ /**
+ * Unpack a JSON request and send it to the necessary Hatch handler.
+ *
+ * @return True if the calling code should avoid calling reply() with
+ * the response object.
+ */
+ private boolean dispatchRequest(
+ JSONObject request, JSONObject response) throws JSONException {
+
+ String action = request.getString("action");
+ String origin = request.getString("origin");
+
+ logger.info("Received message id=" +
+ response.get("msgid") + " action=" + action);
+
+ if ("".equals(origin)) {
+ response.put("status", 404);
+ response.put("message", "'origin' parameter required");
+ return false;
+ }
+
+ String key = null;
+ String content = null;
+ FileIO fileIO = new FileIO(profileDirectory, origin);
+
+ switch (action) {
+
+ case "printers":
+ response.put("content",
+ new PrintManager().getPrintersAsMaps());
+ break;
+
+ case "printer-options":
+
+ String printer = request.optString("printer", null);
+ JSONObject options =
+ new PrintManager().getPrintersOptions(printer);
+
+ if (options == null) {
+ response.put("status", 400);
+ if (printer == null) {
+ response.put("message", "No default printer found");
+ } else {
+ response.put("message", "No such printer: " + printer);
+ }
+ } else {
+ response.put("content", options);
+ }
+
+ break;
+
+ case "print":
+ // Confirm a minimal data set to enqueue print requests.
+ content = request.getString("content");
+ String contentType = request.getString("contentType");
+
+ if (content == null || "".equals(content)) {
+ response.put("status", 400);
+ response.put("message", "Empty print message");
+
+ } else {
+ Hatch.enqueuePrintRequest(request);
+ // Responses to print requests are generated asynchronously
+ // and delivered from the FX print thread via reply().
+ return true;
+ }
+
+ case "keys": // Return stored keys
+ key = request.optString("key");
+ response.put("content", fileIO.keys(key));
+ break;
+
+ case "get":
+ key = request.getString("key");
+ String val = fileIO.get(key);
+
+ if (val != null) {
+ // Translate the JSON string stored by set() into a
+ // Java object that can be added to the response.
+ Object jsonBlob = new JSONTokener(val).nextValue();
+ response.put("content", jsonBlob);
+ }
+ break;
+
+ case "set" :
+ key = request.getString("key");
+
+ // JSON-ify the thing stored under "content"
+ String json = JSONObject.valueToString(request.get("content"));
+
+ if (!fileIO.set(key, json)) {
+ response.put("status", 500);
+ response.put("message", "Unable to set key: " + key);
+ }
+ break;
+
+ case "remove":
+ key = request.getString("key");
+
+ if (!fileIO.remove(key)) {
+ response.put("status", 500);
+ response.put("message", "Unable to remove key: " + key);
+ }
+ break;
+
+ default:
+ response.put("status", 404);
+ response.put("message", "Action not found: " + action);
+ }
+
+ return false;
+ }
+
+ /**
+ * Most replies are delivered from within dispatchRequest, but some
+ * like printing require the reply be delivered from another thread.
+ */
+ protected static void reply(JSONObject response) {
+ io.sendMessage(response);
+ }
+
+ public void run() {
+
+ configure();
+ io.listen(); // STDIN/STDOUT handler
+
+ while (true) {
+
+ boolean skipReply = false;
+ JSONObject response = new JSONObject();
+
+ // Status values overidden as needed by the dispatch handler.
+ response.put("status", 200);
+ response.put("message", "OK");
+
+ try {
+ JSONObject request = io.recvMessage();
+
+ response.put("clientid", request.getLong("clientid"));
+ response.put("msgid", request.getLong("msgid"));
+
+ skipReply = dispatchRequest(request, response);
+
+ } catch (JSONException je) {
+ response.put("status", 400);
+ response.put("message", "Bad Request: " + je.toString());
+ }
+
+ if (!skipReply) reply(response);
+ }
+ }
+}
+
--- /dev/null
+/* -----------------------------------------------------------------------
+ * Copyright 2016 King County Library System
+ * Bill Erickson <berickxx@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * -----------------------------------------------------------------------
+ */
+package org.evergreen_ils.hatch;
+
+import java.util.logging.Logger;
+import org.json.*;
+
+
+public class TestHatch {
+ static MessageIO io;
+ static final Logger logger = Logger.getLogger("org.evergreen_ils.hatch");
+ static final String origin = "https://test.hatch.evergreen-ils.org";
+
+ public static void pause() {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {}
+ }
+
+ public static void doSends() {
+ int msgid = 1;
+ int clientid = 1;
+ JSONObject obj;
+
+ // get a list of stored keys
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "keys");
+ io.sendMessage(obj);
+
+ pause();
+
+ // store a string
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "set");
+ obj.put("key", "eg.hatch.test.key1");
+ obj.put("content", "Rando content, now with cheese and AljamĂa");
+ io.sendMessage(obj);
+
+ pause();
+
+ // store a value
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "get");
+ obj.put("key", "eg.hatch.test.key1");
+ io.sendMessage(obj);
+
+ // store an array
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "set");
+ obj.put("key", "eg.hatch.test.key2");
+ JSONArray arr = new JSONArray();
+ arr.put(123);
+ arr.put("23 Skidoo");
+ obj.put("content", arr);
+ io.sendMessage(obj);
+
+ pause();
+
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "get");
+ obj.put("key", "eg.hatch.test.key2");
+ io.sendMessage(obj);
+
+ pause();
+
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "keys");
+ io.sendMessage(obj);
+
+ pause();
+
+ // get a list of printers
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "printers");
+ io.sendMessage(obj);
+
+ pause();
+
+ // get a list of printers
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "printer-options");
+ io.sendMessage(obj);
+
+ pause();
+
+ /*
+ // Printing tests
+
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "print");
+ obj.put("contentType", "text/plain");
+ obj.put("content", "Hello, World!");
+ obj.put("showDialog", true); // avoid auto-print while testing
+ io.sendMessage(obj);
+
+ pause();
+
+ obj = new JSONObject();
+ obj.put("msgid", msgid++);
+ obj.put("clientid", clientid);
+ obj.put("origin", origin);
+ obj.put("action", "print");
+ obj.put("contentType", "text/html");
+ obj.put("content", "<html><body><b>HELLO WORLD</b><img src='" +
+ "http://evergreen-ils.org/wp-content/uploads/2013/09/copy-Evergreen_Logo_sm072.jpg"
+ + "'/></body></html>");
+ obj.put("showDialog", true); // avoid auto-print while testing
+
+ JSONObject settings = new JSONObject();
+ settings.put("copies", 2);
+ obj.put("settings", settings);
+ io.sendMessage(obj);
+
+ pause();
+
+ */
+ }
+
+ /**
+ * Log all received message as a JSON string
+ */
+ public static void doReceive() {
+ while (true) {
+ JSONObject resp = io.recvMessage();
+ logger.info("TestJSON:doReceive(): " + resp.toString());
+ }
+ }
+
+ public static void main (String[] args) {
+ io = new MessageIO();
+ io.listen();
+
+ if (args.length > 0 && args[0].equals("receive")) {
+ doReceive();
+ } else {
+ doSends();
+ }
+ }
+}
+