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 javafx.application.Application;
-import javafx.application.Platform;
-import javafx.scene.Scene;
-import javafx.scene.layout.Region;
-import javafx.scene.web.WebEngine;
-import javafx.scene.web.WebView;
-import javafx.scene.Node;
-import javafx.stage.Stage;
-import javafx.scene.transform.Scale;
-import javafx.beans.value.ChangeListener;
-import javafx.concurrent.Worker;
-import javafx.concurrent.Worker.State;
-import javafx.concurrent.Service;
-import javafx.concurrent.Task;
-import javafx.event.EventHandler;
-import javafx.concurrent.WorkerStateEvent;
-import java.util.concurrent.LinkedBlockingQueue;
-
-import org.eclipse.jetty.util.ajax.JSON;
-
import java.util.Map;
-
import java.io.FileInputStream;
/**
* 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.
- *
- * Beware: On Mac OS, the "FX Application Thread" is renamed to
- * "AppKit Thread" when the first call to print() or showPrintDialog()
- * [in PrintManager] is made. This is highly confusing when viewing logs.
+ * Reads the Hatch/Jetty configuration file and launches the Jetty
+ * websockets server instance.
*
*/
-public class Hatch extends Application {
-
- /** Browser Region for rendering and printing HTML */
- private BrowserView browser;
+public class Hatch {
- /** 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>();
-
- /**
- * Printable region containing a browser
- */
- class BrowserView extends Region {
- WebView webView = new WebView();
- WebEngine webEngine = webView.getEngine();
- public BrowserView() {
- getChildren().add(webView);
- }
- }
-
- /**
- * Service task which listens for inbound messages from the
- * servlet.
- *
- * The code blocks on the concurrent queue, so it must be
- * run in a separate thread to avoid locking the main FX thread.
- */
- private static class MsgListenService extends Service<Map<String,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;
- }
- }
- }
- };
- }
- }
-
-
- /**
- * 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);
- }
-
- /**
- * 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;
- }
-
- browser = new BrowserView();
- Scene scene = new Scene(browser);
- primaryStage.setScene(scene);
-
- 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");
-
- // Avoid nested UI event loops -- runLater
- Platform.runLater(new Runnable() {
- @Override public void run() {
- new PrintManager().print(browser.webEngine, params);
- }
- });
- }
- });
-
- logger.info("printing " + content.length() + " bytes of " + contentType);
- browser.webEngine.loadContent(content, contentType);
-
- // 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();
-
- // 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);
- }
- }
- );
- }
- });
-
- service.start();
- }
-
/**
* Hatch main.
*
- * Reads the Jetty configuration, starts the Jetty server thread,
- * then launches the JavaFX Application thread.
+ * Read the Jetty configuration and start the Jetty server thread.
*/
public static void main(String[] args) throws Exception {
// to continue running in its own thread
server.start();
- logger.info("Launching FX Application");
-
- // launch the FX Application thread
- launch(args);
+ // sit and let the server do its thing.
+ server.join();
}
}
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-// printing
-import javafx.print.*;
-import javafx.scene.web.WebEngine;
-import javafx.collections.ObservableSet;
-import javafx.collections.SetChangeListener;
-
-import javax.print.PrintService;
-import javax.print.PrintServiceLookup;
-import javax.print.attribute.Attribute;
-import javax.print.attribute.AttributeSet;
-import javax.print.attribute.PrintRequestAttributeSet;
-import javax.print.attribute.standard.Media;
-import javax.print.attribute.standard.OrientationRequested;
+import java.io.InputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import javax.print.*;
+import javax.print.event.*;
+import javax.print.attribute.*;
+import javax.print.attribute.standard.*;
+
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.image.*;
+import javax.imageio.*;
import java.lang.IllegalArgumentException;
* the Printer configuration options (if any are already set).
* @return A Map of printer settings extracted from the print dialog.
*/
+ /*
public Map<String,Object> configurePrinter(
Map<String,Object> params) throws IllegalArgumentException {
return settings;
}
}
+ */
+
+ class PrintableImage implements Printable {
+ BufferedImage image;
+ public PrintableImage(BufferedImage image) {
+ this.image = image;
+ }
+
+ public int print(Graphics g, PageFormat pf, int pageIndex) {
+ Graphics2D g2d = (Graphics2D) g;
+ g.translate((int) (pf.getImageableX()), (int) (pf.getImageableY()));
+ if (pageIndex == 0) {
+ double pageWidth = pf.getImageableWidth();
+ double pageHeight = pf.getImageableHeight();
+ double imageWidth = image.getTileWidth();
+ double imageHeight = image.getTileHeight();
+ double scaleX = pageWidth / imageWidth;
+ double scaleY = pageHeight / imageHeight;
+ double scaleFactor = Math.min(scaleX, scaleY);
+ g2d.scale(scaleFactor, scaleFactor);
+ g.drawImage(image, 0, 0, null);
+ return Printable.PAGE_EXISTS;
+ }
+ return Printable.NO_SUCH_PAGE;
+ }
+ }
/**
- * Print the requested page using the provided settings
+ * Print the requested page using the provided settings.
*
- * @param engine The WebEngine instance to print
* @param params Print request parameters
+ * @return null on success, error string on failure
*/
- public void print(WebEngine engine, Map<String,Object>params) {
+ public String print(Map<String,Object>params) {
- Long msgid = (Long) params.get("msgid");
- Boolean showDialog = (Boolean) params.get("showDialog");
+ String content = (String) params.get("content");
+ String contentType = (String) params.get("contentType");
+ String errorMessage = null;
Map<String,Object> settings =
(Map<String,Object>) params.get("config");
- HatchWebSocketHandler socket =
- (HatchWebSocketHandler) params.get("socket");
-
- PrinterJob job = null;
+ String name = (String) settings.get("printer");
+ PrintService printer = getPrinterByName(name);
+ PrintJobWatcher watcher = null;
+ // TODO
try {
- job = buildPrinterJob(settings);
- } catch(IllegalArgumentException e) {
- socket.reply(e.toString(), msgid, false);
- return;
- }
+ DocFlavor flavor = null;
+ BufferedImage bufferedImage = null;
+ InputStream stream = null;
+ byte[] bytes;
+
+ switch (contentType) {
+
+ case "text/plain":
+ flavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8;
+ bytes = content.getBytes("UTF8");
+ stream = new ByteArrayInputStream(bytes);
+ break;
- if (showDialog != null && showDialog.booleanValue()) {
- logger.info("Print dialog requested");
+ case "image/png":
+ //flavor = DocFlavor.INPUT_STREAM.PNG;
+ flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
+ bytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(content);
+ stream = new ByteArrayInputStream(bytes);
+ bufferedImage = ImageIO.read(stream);
+ break;
- if (!job.showPrintDialog(null)) {
- // job canceled by user
- logger.info("after dialog");
- job.endJob();
- socket.reply("Print job canceled", msgid);
- return;
+ default:
+ errorMessage = "Unsupported contentType " + contentType;
+ logger.warn(errorMessage);
+ return errorMessage;
}
- } 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...");
+ PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
+ aset.add(new Copies(1));
+ aset.add(Sides.ONE_SIDED);
+
+ DocPrintJob job = printer.createPrintJob();
+ watcher = new PrintJobWatcher(job);
+ SimpleDoc doc;
+ if (bufferedImage != null) {
+ doc = new SimpleDoc(
+ new PrintableImage(bufferedImage), flavor, null);
+ } else {
+ doc = new SimpleDoc(stream, flavor, null);
+ }
+ job.print(doc, aset);
+ watcher.waitForDone();
+
+ } catch (UnsupportedEncodingException uee) {
+ return "Error decoding print string";
+ } catch (PrintException pe) {
+ return "Error communicating with printer";
+ } catch (IOException ioe) {
+ }
- engine.print(job);
- logger.info("after print");
+ PrintJobEvent event = watcher.getFinalEvent();
+ switch(event.getPrintEventType()) {
+ case PrintJobEvent.JOB_COMPLETE:
+ logger.info("Printing completed successfully");
+ break;
+ case PrintJobEvent.DATA_TRANSFER_COMPLETE:
+ logger.info("Printer data transfer completed; status unknown");
+ break;
+ case PrintJobEvent.NO_MORE_EVENTS:
+ logger.info("No print events received; status unknown");
+ break;
+ case PrintJobEvent.JOB_CANCELED:
+ errorMessage = "Print job canceled";
+ break;
+ case PrintJobEvent.JOB_FAILED:
+ errorMessage = "Print job failed";
+ break;
+ case PrintJobEvent.REQUIRES_ATTENTION:
+ errorMessage = "Printer requires attention";
+ break;
+ default:
+ errorMessage = "Printing failed";
+ }
- job.endJob();
+ if (errorMessage != null) { logger.warn(errorMessage); }
- socket.reply("Print job succeeded", msgid);
+ return errorMessage;
}
+
/**
* Constructs a PrinterJob based on the provided settings.
*
* @param settings The printer configuration Map.
* @return The newly created printer job.
*/
+ /*
public PrinterJob buildPrinterJob(
Map<String,Object> settings) throws IllegalArgumentException {
return job;
}
+ */
/**
* Builds a PageLayout for the requested printer, using the
* @param printer The printer from which to spawn the PageLayout
* @return The newly constructed PageLayout object.
*/
+ /*
protected PageLayout buildPageLayout(
Map<String,Object> settings, Printer printer) {
((Number) layoutMap.get("bottomMargin")).doubleValue()
);
}
+ */
/**
* Applies the provided settings to the PrinterJob.
* @param settings The printer configuration settings map.
* @param job A PrinterJob, constructed from buildPrinterJob()
*/
+ /*
protected void applySettingsToJob(
Map<String,Object> settings, PrinterJob job) {
jobSettings.setPageRanges(builtRanges.toArray(new PageRange[0]));
}
}
+ */
/**
* Extracts and flattens the various configuration values from a
* @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();
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];
-
- return (Printer[]) printerObserver.toArray(new Printer[0]);
+ protected PrintService[] getPrinters() {
+ DocFlavor flavor = DocFlavor.INPUT_STREAM.AUTOSENSE;
+ PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
+ return PrintServiceLookup.lookupPrintServices(flavor, aset);
}
/**
* @return Map of printer information.
*/
protected List<Map<String,Object>> getPrintersAsMaps() {
- Printer[] printers = getPrinters();
+ PrintService[] printers = getPrinters();
List<Map<String,Object>> printerMaps =
new LinkedList<Map<String,Object>>();
- Printer defaultPrinter = Printer.getDefaultPrinter();
+ PrintService defaultPrinter =
+ PrintServiceLookup.lookupDefaultPrintService();
- for (Printer printer : printers) {
+ for (PrintService printer : printers) {
HashMap<String, Object> printerMap = new HashMap<String, Object>();
printerMaps.add(printerMap);
printerMap.put("name", printer.getName());
* @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) {
+ protected PrintService getPrinterByName(String name) {
+ PrintService[] printers = getPrinters();
+ for (PrintService printer : printers) {
if (printer.getName().equals(name))
return printer;
}
return null;
}
+
+
+ // PrintJobWatcher class copied and modified from:
+ // http://www.rgagnon.com/javadetails/java-print-a-text-file-with-javax.print-api.html
+ class PrintJobWatcher {
+ PrintJobEvent event = null; // set to the final PrintJobAdapter
+
+ PrintJobWatcher(DocPrintJob job) {
+
+ job.addPrintJobListener(new PrintJobAdapter() {
+ public void printJobCanceled(PrintJobEvent pje) {
+ allDone(pje);
+ }
+ public void printJobCompleted(PrintJobEvent pje) {
+ allDone(pje);
+ }
+ public void printJobFailed(PrintJobEvent pje) {
+ allDone(pje);
+ }
+ public void printJobNoMoreEvents(PrintJobEvent pje) {
+ allDone(pje);
+ }
+ void allDone(PrintJobEvent pje) {
+ synchronized (PrintJobWatcher.this) {
+ event = pje;
+ PrintJobWatcher.this.notify();
+ }
+ }
+ });
+ }
+
+ public PrintJobEvent getFinalEvent() {
+ return event;
+ }
+
+ public synchronized void waitForDone() {
+ try {
+ while (event == null) {
+ wait();
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+ }
}