# GUIPrinterStreamSupport.java

    /*
    ** Module   : GUIPrinterStreamSupport.java
    ** Abstract : Implementation of GUI printer stream.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 HC  20171024 Initial version.
    ** 002 ECF 20171030 Added write(byte[], int, int).
    ** 003 CA  20180210 Renamed deliverPDFPrintOutput to deliverPrintOutput and added report format
    **                  parameter.
    ** 004 SBI 20180410 Changed closeStream() due to deliverPrintOutput() was renamed to
    **                  deliverDocumentOutput().
    ** 005 OM  20181001 Tracked document output using an UUID.
    ** 006 HC  20190104 Javadoc fixes.
    ** 007 EVL 20220325 Base refactoring for Stream based classes getting single byte and array of bytes.
    ** 008 SBI 20220105 Fixed javadoc references.
    ** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */
    package com.goldencode.p2j.util;

    import com.goldencode.p2j.ui.chui.*;
    import com.goldencode.p2j.ui.client.*;
    import com.goldencode.p2j.ui.client.gui.driver.*;
    import com.goldencode.p2j.ui.client.gui.driver.web.*;

    import com.goldencode.p2j.util.logging.*;
    import org.apache.pdfbox.pdmodel.*;
    import org.apache.pdfbox.pdmodel.common.*;
    import org.apache.pdfbox.pdmodel.font.*;
    import org.apache.pdfbox.pdmodel.font.encoding.*;

    import java.io.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.*;

    /**
     * The class implements all the necessary logic behind the GUI printer streams and servers as the
     * core of the runtime support of the 4GL statement OUTPUT TO PRINTER.
     * <p>
     * The expected flow of the print outputs in the Web GUI driver is as follows:
     * <ol>
     * <li>OUTPUT TO PRINTER... PUT... and stream close creates PDF output data
     * <li>The data is delivered to the driver with
     *    {@link GuiDriver#deliverDocumentOutput(Consumer, Runnable, MediaType, String, boolean)}.
     * <li>The driver stores the delivered PDF data with
     *    {@link GuiWebDriver#enqueueDocumentOutput(DocumentOutput)}.
     * <li>The driver notifies the JS client with MSG_OPEN_MIME_RESOURCE web socket message.
     * <li>JS client reads the print output id, constructs the print output URL with the id from
     *    the message and opens new browser window pointing it to the URL.
     * <li>{@link DocumentOutputHandler} catches the request, it calls
     *    {@link GuiWebDriver#dequeueDocumentOutput(UUID)} and serves the PDF data to the JS client.
     *    When there is any error during this step (for example an intermittent network error)
     *    the handler returns the PDF output to the storage with
     *    {@link GuiWebDriver#enqueueDocumentOutput(DocumentOutput)}.
     * </ol>
     */
    public class GUIPrinterStreamSupport
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(GUIPrinterStreamSupport.class.getName());

       /** The PDF user-space line height */
       private static final float LINE_SPACING = 1f;

       /** The print options in effect */
       private final PrintOptions options;

       /** Printer name passed to OUTPUT TO PRINTER, currently not used */
       private final String printerName;

       /** The font passed to OUTPUT TO PRINTER */
       private int fontNum = -1;

       /** The effective PDFBox font */
       private PDFont font;

       /** The effective font size */
       private float fontSize;

       /** Current output line */
       private StringBuilder curLine;

       /** The page setup in effect */
       private PDFPageSetup pageSetup;

       /** The PDF document object */
       private PDDocument doc;

       /** Current PDF page object */
       private PDPage currPage;

       /** Current PDF content stream */
       private PDPageContentStream curContent;

       // the X coordinate of the origin (upper left corner) of the first line of text
       private float startX;

       // the Y coordinate of the origin (upper left corner) of the first line of text
       private float startY;

       /** The actual printer stream */
       private PrinterStream printerStream;

       /**
        * Ctor.
        *
        * @param   printerName
        *          Printer name.
        * @param   options
        *          Print options.
        */
       private GUIPrinterStreamSupport(String printerName, PrintOptions options)
       {
          this.printerName = printerName;
          this.options = options;
       }

       /**
        * Opens a printer stream and returns its remote id.
        *
        * @param   printer
        *          Printer name.
        * @param   sd
        *          Stream daemon instance.
        * @param   options
        *          Effective print options.
        */
       public static int openPrinterStream(String printer, StreamDaemon sd, PrintOptions options)
       {
          return sd.store(new GUIPrinterStreamSupport(printer, options).asStream());
       }

       /**
        * Returns an instance of {@link Stream} that provides the printer output.
        *
        * @return  See above.
        */
       public Stream asStream()
       {
          if (printerStream == null)
          {
             printerStream = new PrinterStream();
          }

          return printerStream;
       }

       /**
        * Initializes the instance. The method prepares the objects necessary for PDFBox output and starts new
        * page of the resulting PDF document.
        */
       private void init()
       {
          if (doc != null)
          {
             return;
          }

          doc = new PDDocument();

          // resolve print font
          FontDetails fd = null;

          // the font may be defined in the OUTPUT TO PRINTER statement
          if (fontNum >= 0)
          {
             Window currWindow = WindowManager.getCurrentWindow();
             fd = FontManager.getFontDetails(currWindow, fontNum);
          }

          if (fd == null)
          {
             // get the  configured print font when not in OUTPUT
             fd = FontManager.getFontDetails(null, FontManager.PRINT_FONT);
          }

          if (fd != null && fd.fontAlias != null)
          {
             fd = fd.fontAlias;
          }

          if (fd != null && fd.fontDefinition != null)
          {
             // assume true type font data
             try
             {
                font = PDTrueTypeFont.load(doc,
                                           new ByteArrayInputStream(fd.fontDefinition),
                                           WinAnsiEncoding.INSTANCE);
             }
             catch (IOException e)
             {
                LOG.log(Level.WARNING, "Print font failed to load!", e);
             }
          }

          if (font == null)
          {
             // no font data found, fallback to some default
             font = PDType1Font.COURIER;
          }

          fontSize = fd.pointSize;
          pageSetup = PDFPageSetup.fromPrintOptions(options);

          openPage();
       }

       /**
        * Renders a single line to the PDF document and optionally breaks to a new line.
        *
        * @param   addNewline
        *          When <code>true</code> new line is added after the line is rendered to the PDF document.
        */
       private void flushLine(boolean addNewline)
       {
          if (curLine == null)
          {
             return;
          }

          try
          {
             if (curLine.length() > 0)
             {
                curContent.showText(curLine.toString());
                curLine = null;
             }

             if (addNewline)
             {
                curContent.newLine();
             }
          }
          catch (IOException e)
          {
             LOG.warning("", e);
          }
       }

       /**
        * Opens new page in the PDF document.
        */
       private void openPage()
       {
          printerStream.registerPageBreakListener(out -> openPage());

          // close previous page if any
          closePage();

          currPage = new PDPage(pageSetup.pageSize);

          // Note that the right and bottom margins are used to set the correct PDF trim box
          // but don't actually trim the text rendered outside of the trim box.
          float trimWidth  = pageSetup.pageSize.getWidth() - pageSetup.leftMargin - pageSetup.rightMargin;
          float trimHeight = pageSetup.pageSize.getHeight() - pageSetup.topMargin - pageSetup.bottomMargin;
          currPage.setTrimBox(new PDRectangle(pageSetup.leftMargin, pageSetup.topMargin, trimWidth, trimHeight));
          doc.addPage(currPage);

          PDRectangle mediabox = currPage.getMediaBox();
          startX = mediabox.getLowerLeftX() + pageSetup.leftMargin;
          startY = mediabox.getUpperRightY() - pageSetup.topMargin;

          try
          {
             curContent = new PDPageContentStream(doc, currPage, PDPageContentStream.AppendMode.APPEND, true,
                                                  true);
             curContent.beginText();
             curContent.setFont(font, fontSize);
             curContent.setLeading(LINE_SPACING * fontSize);
             curContent.newLineAtOffset(startX, startY - fontSize);
          }
          catch (IOException ex)
          {
             LOG.severe("", ex);
          }
       }

       /**
        * Closes current page in the PDF document.
        */
       private void closePage()
       {
          if (curContent == null)
          {
             return;
          }

          try
          {
             flushLine(false);
             curContent.endText();
             curContent.close();
             curContent = null;
          }
          catch (IOException e)
          {
             LOG.warning("", e);
          }
       }

       /**
        * Closes the stream. The method closes the current pahe and sends the PDF data to the screen driver.
        */
       private void closeStream()
       {
          if (doc == null)
          {
             return;
          }

          closePage();

          GuiDriver driver = (GuiDriver) ThinClient.getInstance().getOutputManager().getInstanceDriver();

          Consumer<OutputStream> consumer = outputStream ->
          {
             try
             {
                doc.save(outputStream);
             }
             catch (IOException e)
             {
                throw new RuntimeException(e);
             }
          };

          Runnable finalizer = () ->
          {
             try
             {
                doc.close();
             }
             catch (IOException e)
             {
                throw new RuntimeException(e);
             }
          };

          String uuid = UUID.randomUUID().toString();
          driver.deliverDocumentOutput(consumer, finalizer, MediaType.PDF, uuid, false);
       }

       /**
        * The actual printer stream implementation.
        */
       private class PrinterStream
       extends Stream
       {
          /**
           * The number of bytes available to be immediately read without blocking.
           *
           * @return   The number of available bytes.
           *
           * @throws   IOException
           *           if an I/O error occurs.
           */
          @Override
          public long available() throws
                                  IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * The 0-based offset into the stream at which the next read or write will
           * occur.  This does not work for "streams" that require sequential
           * access.
           *
           * @return   The current position in the stream.
           *
           * @throws   UnsupportedOperationException
           *           If the requested operation is not supported.
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public long getPos() throws
                               UnsupportedOperationException,
                               IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Moves the current read/write position to the specified absolute 0-based
           * offset.  A negative offset is ignored (no action is taken).  If the
           * specified offset is larger than the length of the stream, the seek
           * position will report as the number requested, but if no subsequent
           * writes occur to the file, the file is truncated to 0 bytes.  If writes
           * do occur, all writes occur at byte 0 BUT the file actually is of a size
           * that is the requested offset + the number of bytes written!
           * <p>
           * This does not work for "streams" that require sequential access.
           *
           * @param    pos
           *           The new read/write position in the stream.
           *
           * @throws   UnsupportedOperationException
           *           If the requested operation is not supported.
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void setPos(long pos) throws
                                       UnsupportedOperationException,
                                       IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * The length of the stream in bytes.  This does not work for "streams"
           * that require sequential access.
           *
           * @return   The length of the stream.
           *
           * @throws   UnsupportedOperationException
           *           If the requested operation is not supported.
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public long getLen() throws
                               UnsupportedOperationException,
                               IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Truncates or extends the stream to the specified length if this stream
           * supports such an operation. If truncation is requested, all data
           * located after this point in the file is discarded.  If extending the
           * file is requested, the values of the data in the extended portion of
           * the file is undefined.
           *
           * @param    len
           *           The new length of the file.
           *
           * @throws   UnsupportedOperationException
           *           If the requested operation is not supported.
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void setLen(long len) throws
                                       UnsupportedOperationException,
                                       IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Write the given character to the output stream.
           *
           * @param    ch
           *           The character to be written.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void writeCh(char ch) throws
                                       IOException
          {
             init();

             if (ch == '\n')
             {
                flushLine(true);
             }
             else if (ch == '\f')
             {
                // ignore
             }
             else
             {
                if (curLine == null)
                {
                   curLine = new StringBuilder();
                }

                curLine.append(ch);
             }
          }

          /**
           * Write the given byte to the output stream.
           *
           * @param    b
           *           The byte to be written.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void writeByte(byte b) throws
                                        IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Write the given string to the output stream.
           *
           * @param    data
           *           The data to be written.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void write(String data) throws
                                         IOException
          {
             char[] ch = data.toCharArray();
             for (int i = 0; i < ch.length; i++)
             {
                writeCh(ch[i]);
             }
          }

          /**
           * Write the given byte array to the output stream.
           *
           * @param    data
           *           The data to be written.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           */
          @Override
          public void write(byte[] data) throws
                                         IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Write the specified range of bytes from the given byte array to the output stream.
           *
           * @param    data
           *           The data to be written.
           * @param    off
           *           Starting offset in data from which to read bytes to be written. Must be
           *           non-negative and {@code &lt; data.length}.
           * @param    len
           *           Length of data to be written. Must be non-negative and {@code &lt;=
           *           (data.length - offset)}.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           */
          public  void write(byte[] data, int off, int len)
          throws IOException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Peeks at the character from the current read position in the stream (reads a character from
           * the current read position in the stream without incrementing stream read position. The next
           * {@code peekCh()} and {@code readCh()} will return the same value).
           * <p>
           * The underlying stream subclass determines the content of the result. Byte oriented streams
           * such as pipes or files will return a byte while streams that generate keystrokes or
           * characters may return a DBCS or Unicode character.
           *
           * @return  The next character read from the stream, -1 on any failure and -2 upon an
           *          {@code EOF}.
           */
          @Override
          public int peekCh()
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Read a character from the current read position in the stream and increment stream pointer.
           * <p>
           * The underlying stream subclass determines the content of the result.
           * Byte oriented streams such as pipes or files will return a byte while
           * streams that generate keystrokes or characters may return a DBCS or
           * Unicode character.
           *
           * @return   The next character read from the stream, -1 on any failure
           *           and -2 upon an <code>EOF</code>.
           */
          @Override
          public int readCh()
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Read a single byte from the underlying stream.
           *
           * @return   A single byte from the stream, -1 on any failure and -2 upon an {@code EOF}.
           */
          @Override
          public int readByte()
          {
             return -2;
          }

          /**
           * Read all characters from the current read position in the stream to the
           * next line separator (as determined by the <code>File.separator</code>
           * or to the <code>EOF</code>. Any line separator character(s) and the
           * <code>EOF</code> character are not returned.
           *
           * @return   The next line read from the stream.
           *
           * @throws   IOException
           *           If an I/O error occurs.
           * @throws   EOFException
           *           If this input stream reaches the end before reading all the bytes.
           * @throws   InterruptedException
           *           If any thread interrupted the current thread before or while
           *           the current thread was waiting for a notification.
           */
          @Override
          public String readLn() throws
                                 EOFException,
                                 IOException,
                                 InterruptedException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Closes the input stream and releases OS resources associated with it.
           */
          @Override
          public void closeIn()
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Closes the output stream and releases OS resources associated with it.
           */
          @Override
          public void closeOut()
          {
             closeStream();
          }

          /**
           * Closes both the input and output streams and releases OS resources
           * associated with it.
           */
          @Override
          public void close()
          {
             closeStream();
          }

          /**
           * State of the input side of the stream.
           *
           * @return   <code>true</code> if the input side of the stream is active.
           */
          @Override
          public boolean isIn()
          {
             return false;
          }

          /**
           * State of the output side of the stream.
           *
           * @return   <code>true</code> if the output side of the stream is active.
           */
          @Override
          public boolean isOut()
          {
             return true;
          }

          /**
           * Assigns the internal stream reference to the given reference.
           *
           * @param    stream
           *           The new internal stream reference to use for all operations.
           *
           * @throws   UnsupportedOperationException
           *           If the requested operation is not supported.
           */
          @Override
          public void assign(Stream stream)
          throws
          UnsupportedOperationException
          {
             throw new UnsupportedOperationException();
          }

          /**
           * Set stream display mode to landscape
           */
          @Override
          public void setLandscape()
          {
             super.setLandscape();
             options.pageOrientation = PrintOptions.PageOrientation.LANDSCAPE;
          }

          /**
           * Set stream display mode to portrait
           */
          @Override
          public void setPortrait()
          {
             super.setPortrait();
             options.pageOrientation = PrintOptions.PageOrientation.PORTRAIT;
          }

          /**
           * Sets the stream font number. The number denotes an index into the font table
           * of the current environment.
           * The method is part of the runtime support of the FONT option of the OUTPUT TO PRINTER
           * (FWD extension to 4GL) statement. This method is only implemented by the printer stream.
           *
           * @param   num
           *          A font number.
           */
          @Override
          public void setFont(int num)
          {
             fontNum = num;
          }
       }
    }