Hatch print base64 image experiment collab/berick/hatch-direct-print
authorBill Erickson <berickxx@gmail.com>
Wed, 19 Oct 2016 19:22:55 +0000 (15:22 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 19 Oct 2016 19:22:55 +0000 (15:22 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
src/org/evergreen_ils/hatch/Hatch.java
src/org/evergreen_ils/hatch/HatchWebSocketHandler.java
src/org/evergreen_ils/hatch/PrintManager.java

index 0bed93f..26215a6 100644 (file)
@@ -17,221 +17,28 @@ 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 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 {
 
@@ -247,9 +54,7 @@ public class Hatch extends Application {
         // 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();
     }
 }
index bb2ee9c..f9730ff 100644 (file)
@@ -259,15 +259,16 @@ public class HatchWebSocketHandler {
                 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;
+                try {
+                    response = new PrintManager().print(params);
+                } catch (Exception e) {
+                    response = e.toString();
+                    error = true;
+                }
+                
+                break;
 
+            /*
             case "print-config":
                 try {
                     response = new PrintManager().configurePrinter(params);
@@ -276,6 +277,7 @@ public class HatchWebSocketHandler {
                     error = true;
                 }
                 break;
+            */
 
             case "get":
                 String val = io.get(key);
index 40a4003..18961d3 100644 (file)
@@ -19,19 +19,21 @@ package org.evergreen_ils.hatch;
 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;
 
@@ -59,6 +61,7 @@ public class PrintManager {
      * 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 {
 
@@ -80,66 +83,141 @@ public class PrintManager {
             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 {
 
@@ -159,6 +237,7 @@ public class PrintManager {
 
         return job;
     }
+    */
 
     /**
      * Builds a PageLayout for the requested printer, using the
@@ -168,6 +247,7 @@ public class PrintManager {
      * @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) {
 
@@ -211,6 +291,7 @@ public class PrintManager {
             ((Number) layoutMap.get("bottomMargin")).doubleValue()
         );
     }
+    */
 
     /**
      * Applies the provided settings to the PrinterJob.
@@ -218,6 +299,7 @@ public class PrintManager {
      * @param settings The printer configuration settings map.
      * @param job A PrinterJob, constructed from buildPrinterJob()
      */
+    /*
     protected void applySettingsToJob(
             Map<String,Object> settings, PrinterJob job) {
 
@@ -284,6 +366,7 @@ public class PrintManager {
             jobSettings.setPageRanges(builtRanges.toArray(new PageRange[0]));
         }
     }
+    */
 
     /**
      * Extracts and flattens the various configuration values from a 
@@ -292,6 +375,7 @@ public class PrintManager {
      * @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();
@@ -361,18 +445,17 @@ public class PrintManager {
         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);  
     }
 
     /**
@@ -382,14 +465,15 @@ public class PrintManager {
      * @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());
@@ -411,13 +495,57 @@ public class PrintManager {
      * @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) {
+            }
+        }
+    }
 }