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 < data.length}.
* @param len
* Length of data to be written. Must be non-negative and {@code <=
* (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;
}
}
}