StreamWrapper.java
/*
** Module : StreamWrapper.java
** Abstract : redirects method calls to a contained stream subclass reference
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20050801 @21942 Created initial version which provides
** redirection of stream API calls to the
** contained stream.
** 002 GES 20050825 @22245 Changed class name from SharedStream to
** StreamWrapper to make this more generic.
** Added a dereference() method to obtain the
** actual stream instance being used.
** 003 GES 20060115 @23920 Added in/out state tracking.
** 004 GES 20060217 @24670 Added writeByte().
** 005 GES 20060816 @28632 Added the internal raw* methods to redirect calls to any contained
** stream. Otherwise the call will be made to the wrapper's supe
** instead of to the contained instance.
** 006 GES 20060922 @29862 Added protection logic to avoid null pointers.
** 007 NVS 20070725 @34690 Renamed isInOK() method to isIn() and isOutOK() method to isOut()
** for the sake of uniformity.
** 008 NVS 20070731 @34758 Implemented advancePage() which overrides the method in the base
** class. This implementation delegates the call to the embedded
** stream.
** 009 NVS 20070802 @34778 Implemented flush() which overrides the method in the base class.
** This implementation delegates the call to the embedded stream.
** 010 SIY 20070928 @35322 Fix for PUT/DISPLAY handling regressions. Ref: #35314
** 011 SIY 20071009 @35406 Fix for delimiter handling regression. Ref: #35404
** 012 SIY 20071017 @35488 Promoted setControl() method. Ref: #35486
** 013 CA 20071228 @36618 Implemented method putUnformatted.
** 014 NVS 20080221 @37141 Protected some setters against NPE. This fix was required by the
** fact the stream named TERM or TERMINAL refers to the interactive
** use of the terminal.
** 015 CA 20080306 @37303 Protected some getters against NPE. This fix was required by the
** fact the stream named TERM or TERMINAL refers to the interactive
** use of the terminal.
** 016 GES 20080425 @38125 Refactored to eliminate round trips to the client by passing more
** data in a single invocation. This improves performance.
** 017 GES 20080430 @38178 Removed unnecessary calls to endOutput().
** 018 SIY 20090220 @41339 Added forwarder for isTerm() method. Minor cleanups in javadoc.
** 019 SVL 20090612 @42671 Added setUnbuffered() method.
** 020 GES 20090624 @42895 Protect stream reads, writes and other state changes/inspections
** from closed streams.
** 021 SIY 20090907 @43833 Explicitly closed stream should not complain about writes into
** closed stream.
** 022 SIY 20090908 @43841 Temporarily disabled #021.
** 023 SVL 20090923 @43978 Fixed inOk/outOk usage for the case when TERMINAL is assigned to a
** stream wrapper. Removed H022.
** 024 SIY 20090925 @44032 Added getNextLineNum() method.
** 025 SVL 20110131 Added setConvert method.
** 026 SVL 20110403 Added write(byte[]) function.
** 027 EVL 20131205 Adding support for batch mode error messages.
** 028 CA 20140730 Protected stream close/cleanup code from raised conditions, to allow
** close/cleanup fully complete even if conditions were raised.
** 029 GES 20141030 Added setConvertSource/Target() methods.
** 030 GES 20140612 Minor cleanup of formatting and comments.
** 031 OM 20170622 Implemented peekCh() method.
** 032 ECF 20171026 Added write(byte[], int, int) method.
** 033 HC 20190716 Implemented stream-by-name resolution.
** 034 ME 20210325 Add readBlockRemote method as pass-through.
** EVL 20220325 Base refactoring for Stream based classes getting single byte and array of bytes.
** CA 20220515 memptr and library calls are supported on server-side, too. Added TODO's for
** client-side APIs which will need to call back to server-side, when memptr is used
** as argument.
** OM 20221003 Allow a remote stream to be identified and accesed by its name.
** EVL 20221207 Adding method to set up LOB directory for Stream object.
** 035 HC 20230427 Implemented server-side file-system resource. File operations
** performed on client can be optionally performed directly on server
** without the expensive remote call.
*/
/*
** 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.security.*;
import com.goldencode.util.*;
import java.io.*;
import java.util.*;
/**
* A stream class which acts as a container which redirects any stream
* method call to the contained stream reference. This is needed to
* support shared streams, since such streams can be constructed/initialized
* in a different scope than they are defined. This can also be used to
* maintain a single reference which has an underlying stream implementation
* which can be changed without changing the reference itself.
* <p>
* This is a generic interface "redirector" (from this instance to the
* contained reference). Upon construction, the internal reference is
* <code>null</code>. Once an {@link #assign} call has occurred, any other
* methods called on this class will succeed. They will be redirected to
* the contained reference. To obtain the reference directly, call the
* {@link #dereference} method.
* <p>
* After {@link #close} is called, the contained stream reference is
* <code>null</code> and subsequent usage will throw a
* <code>NullPointerException</code>.
*/
public class StreamWrapper
extends Stream
{
/** The backing construct for the stream. */
private Stream stream = null;
/** Status var for input. */
private boolean inOk = false;
/** Status var for output. */
private boolean outOk = false;
/** Context-local data */
private static final ContextLocal<WorkArea> work =
new ContextLocal<WorkArea>()
{
protected synchronized WorkArea initialValue()
{
return new WorkArea();
}
};
/**
* Default constructor.
*/
public StreamWrapper()
{
// nothing to do
}
/**
* Construct an instance associated with a specific legacy 4GL variable
* name.
*
* @param name
* The legacy variable name.
*/
public StreamWrapper(String name)
{
super(name);
}
/**
* Finds a stream by its name. The return value is an id of a remote stream.
*
* @param name
* Stream name.
*
* @return The remote stream id, -1 if no such stream exists.
*/
static int findRemoteStreamId(String name)
{
if (name == null)
{
return -1;
}
StreamWrapper str = work.get().streamByName.get(name.toLowerCase());
return (str != null) ? ((RemoteStream)str.stream).getId() : -1;
}
/**
* Finds a stream by its name. The return value is the reference of the remote stream.
*
* @param name
* Stream name.
*
* @return The remote stream reference, or {@code null} if no such stream exists.
*/
public static StreamWrapper getRemoteStreamRef(String name)
{
return work.get().streamByName.get(name.toLowerCase());
}
/**
* The number of bytes available to be immediately read without blocking.
*
* @return The number of available bytes.
*
* @throws IOException
*/
public long available()
throws IOException
{
return stream.available();
}
/**
* 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
* @throws IOException
*/
public long getPos()
throws UnsupportedOperationException,
IOException
{
return stream.getPos();
}
/**
* Moves the current read/write position to the specified absolute 0-based
* offset.
*
* @param pos
* The new read/write position in the stream.
*
* @throws UnsupportedOperationException
* @throws IOException
*/
public void setPos(long pos)
throws UnsupportedOperationException,
IOException
{
if (stream != null && (inOk || outOk))
stream.setPos(pos);
}
/**
* 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
* @throws IOException
*/
public long getLen()
throws UnsupportedOperationException,
IOException
{
return stream.getLen();
}
/**
* 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
* @throws IOException
*/
public void setLen(long len)
throws UnsupportedOperationException,
IOException
{
stream.setLen(len);
}
/**
* Write the given character to the output stream.
*
* @param ch
* The character to be written.
*
* @throws IOException
*/
public void writeCh(char ch)
throws IOException
{
stream.writeCh(ch);
}
/**
* Write the given byte to the output stream.
*
* @param b
* The byte to be written.
*
* @throws IOException
*/
public void writeByte(byte b)
throws IOException
{
stream.writeByte(b);
}
/**
* Write the given string to the output stream.
*
* @param data
* The data to be written.
*
* @throws IOException
*/
public void write(String data)
throws IOException
{
stream.write(data);
}
/**
* Write the given byte array to the output stream.
*
* @param data
* The data to be written.
*
* @throws IOException
* If an I/O error occurs.
*/
public void write(byte[] data)
throws IOException
{
stream.write(data);
}
/**
* 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
{
stream.write(data, off, len);
}
/**
* 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}.
*/
public int peekCh()
{
return stream.peekCh();
}
/**
* Read a character from the current read position in the stream.
* <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>.
*/
public int readCh()
{
return stream.readCh();
}
/**
* 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 stream.readByte();
}
/**
* Read a chunk of bytes from the current read position in the stream to the
* offset calculated by given length or to the <code>EOF</code>, whichever
* comes first. The <code>EOF</code> character is not returned.
*
* @param len
* The number of bytes to read.
*
* @return The chunk read from the stream or <code>null</code> if there
* is nothing to read (the <code>EOF</code> has been reached). The
* length of the returned array will be the smaller of the
* <code>len</code> parameter or the actual bytes left before the
* <code>EOF</code>.
*
* @throws IOException
* If an I/O error occurs.
*/
@Override
public byte[] readBytes(int len)
throws IOException
{
return stream.readBytes(len);
}
/**
* 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 EOFException
* @throws IOException
* @throws InterruptedException
*/
public String readLn()
throws EOFException,
IOException,
InterruptedException
{
return stream.readLn();
}
/**
* Closes the input stream and releases OS resources associated with it.
* Any output stream that is open is unaffected.
*/
public void closeIn()
{
if (stream != null)
{
stream.closeIn();
}
inOk = false;
}
/**
* Closes the output stream and releases OS resources associated with it.
* Any input stream that is open is unaffected.
*/
public void closeOut()
{
try
{
if (stream != null)
{
stream.closeOut();
}
}
finally
{
outOk = false;
}
}
/**
* Write the current output buffer to the stream. This flushes all
* buffered output of the {@link #putField}, {@link #putSpace},
* {@link #putLineEnd} and {@link #writeField} methods to the stream.
* This method also resets the current delimiter to the default.
* <p>
* This method does nothing if the stream is in <code>control</code> mode.
*
* @param appendNewLine
* Automatically add new line at the end of output.
*
* @throws ErrorConditionException
* On any error during I/O.
* @throws StopConditionException
* If any interruption occurs during I/O processing.
*/
public void endOutput(boolean appendNewLine)
throws ErrorConditionException,
StopConditionException
{
if (stream != null)
{
stream.endOutput(appendNewLine);
}
}
/**
* Closes the stream and releases OS resources associated with it. After
* completion, further calls may not be made to use this instance, until
* an {@link #assign} is used to "re-open" the stream. This is safe to
* call when no stream is open (it will do nothing in that case).
*/
public void close()
{
if (stream != null)
{
stream.close();
work.get().removeRemoteStream(getName());
stream = null;
}
inOk = false;
outOk = false;
}
/**
* Assigns the internal stream reference to the given reference. This
* must be called before any other usage of this class.
*
* @param s
* The new internal stream reference to use for all operations.
*
* @throws UnsupportedOperationException
*/
public void assign(Stream s)
throws UnsupportedOperationException
{
WorkArea wa = work.get();
// cleanup
if (stream != null)
{
wa.removeRemoteStream(getName());
stream.close();
}
// special processing for batch mode
if (s == null)
{
// trying to return input or output to terminal inside batch mode
if (ErrorManager.isInBatchMode())
{
// we have to display message and stop the client execution
ErrorManager.displayError(120, "There is no terminal", true);
ErrorManager.stopBatchSession();
}
}
wa = work.get();
if (s instanceof RemoteStream)
{
wa.addRemoteStream(getName(), this);
}
// reset
stream = s;
inOk = true;
outOk = true;
}
/**
* Check if current stream is terminal.
*
* @return <code>true</code> if stream is terminal.
*/
public boolean isTerm()
{
if (stream != null)
{
return stream.isTerm();
}
return super.isTerm();
}
/**
* Inserts a page break if the stream is paged and if the next write
* position is not on the first line of a new page.
*
* @throws ErrorConditionException
*/
public void advancePage()
throws ErrorConditionException
{
if (protectWrites())
return;
if (stream != null)
{
stream.advancePage();
}
}
/**
* Flushes the stream.
*/
public void flush()
{
if (stream != null)
{
stream.flush();
}
}
/**
* Access the wrapped stream directly. This is useful for stream
* subclasses that implement their own APIs (beyond implementing the
* required <code>Stream</code> abstract methods).
*
* @return The stream upon which this instance operates. This may be
* <code>null</code> if no stream has yet been opened (assigned
* to this instance using {@link #assign}).
*/
public Stream dereference()
{
return stream;
}
/**
* State of the input side of the stream.
*
* @return <code>true</code> if the input side of the stream is active.
*/
public boolean isIn()
{
return inOk;
}
/**
* State of the output side of the stream.
*
* @return <code>true</code> if the output side of the stream is active.
*/
public boolean isOut()
{
return outOk;
}
/**
* Check if stream is in silent mode.
*
* @return <code>true</code> if stream was explicitly closed by
* application.
*/
public boolean isSilent()
{
return super.isSilent();
}
/**
* Output a list of fields to the stream.
*
* @param data
* The list of fields to be output.
* @param mode
* <code>MODE_PUT_NORMAL</code>, <code>MODE_PUT_CONTROL</code>,
* <code>MODE_PUT_UNFORMATTED</code> or <code>MODE_EXPORT</code>.
* @param delim
* The delimiter to use in EXPORT mode.
*
* @throws ErrorConditionException
* On any error during I/O.
*/
public void putWorker(FieldEntry[] data, int mode, char delim)
throws ErrorConditionException
{
if (stream != null)
{
stream.putWorker(data, mode, delim);
}
}
/**
* Output a list of fields with default formatting to the stream.
*
* @param data
* The list of fields to be output.
*
* @throws ErrorConditionException
* On any error during I/O.
*/
public void putUnformatted(FieldEntry[] data)
throws ErrorConditionException
{
if (protectWrites())
return;
if (stream != null)
{
stream.putUnformatted(data);
}
}
/**
* Sets the stream's delimiter character for import/export of fields.
*
* @param delim
* The current delimiter <code>char</code>.
*/
public void setDelimiter(char delim)
{
// save it locally as well
super.setDelimiter(delim);
if (stream != null)
{
stream.setDelimiter(delim);
}
}
/**
* Sets the stream's delimiter character for import/export of fields.
*
* @param delim
* The current delimiter <code>char</code> (if this is not
* a single character, only the first character will be used).
* If <code>null</code> or an empty string, the delimiter is
* reset to the default of {@link #DELIM}.
*/
public void setDelimiter(String delim)
{
// save it locally as well
super.setDelimiter(delim);
if (stream != null)
{
stream.setDelimiter(delim);
}
}
/**
* Sets the stream into control mode which ensures that subsequent
* characters written to the stream will not affect line/page counters.
* <p>
* Warning: in Progress this is described as ensuring that the "position"
* (presumably the write position) will not be changed when this option
* is enabled. However, this really does not make any sense in the case
* where output characters are used for control purposes. No support is
* provided for this case.
*
* @param mode
* If <code>true</code>, all output is emitted without
* modification of line/page counters.
*/
public void setControl(boolean mode)
{
// save it locally as well
super.setControl(mode);
if (stream != null)
{
stream.setControl(mode);
}
}
/**
* Makes the stream UNBUFFERED.
*/
public void setUnbuffered()
{
if (stream != null)
{
stream.setUnbuffered();
}
}
/**
* Sets the stream's character conversion mode.
*
* @param convert
* The new character conversion mode. <code>true</code> to
* enable character conversion, <code>false</code> to disable
* conversion.
*/
public void setConvert(boolean convert)
{
// save it locally as well
super.setConvert(convert);
if (stream != null)
{
stream.setConvert(convert);
}
}
/**
* Sets the stream's source codepage and activates character conversion mode.
*
* @param cp
* The source codepage for character conversion mode.
*/
public void setConvertSource(String cp)
{
// save it locally as well
super.setConvertSource(cp);
if (stream != null)
{
stream.setConvertSource(cp);
}
}
/**
* Sets the stream's target codepage and activates character conversion mode.
*
* @param cp
* The target codepage for character conversion mode.
*/
public void setConvertTarget(String cp)
{
// save it locally as well
super.setConvertTarget(cp);
if (stream != null)
{
stream.setConvertTarget(cp);
}
}
/**
* Sets the stream's lob directory located as a subdirectory for opened current file stream directory.
*
* @param lobDir
* The name for subdirectory to store LOB files.
*/
public void setLobDirectory(String lobDir)
{
// save it locally as well
super.setLobDirectory(lobDir);
if (stream != null)
{
stream.setLobDirectory(lobDir);
}
}
/**
* 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
* statement (FWD extension to 4GL). This method is only implemented by the printer stream.
*
* @param num Font number.
*/
@Override
public void setFont(int num)
{
super.setFont(num);
if (stream != null)
{
stream.setFont(num);
}
}
/**
* Set stream display mode to landscape
*/
@Override
public void setLandscape()
{
super.setLandscape();
if (stream != null)
{
stream.setLandscape();
}
}
/**
* Set stream display mode to portrait
*/
@Override
public void setPortrait()
{
super.setPortrait();
if (stream != null)
{
stream.setPortrait();
}
}
/**
* Perform common close <b>preprocessing</b> for all streams.
*
* @param in
* <code>true</code> if this was triggered during a call to
* {@link #closeIn} or {@link #close}. Input resources are
* to be cleaned up.
* @param out
* <code>true</code> if this was triggered during a call to
* {@link #closeOut} or {@link #close}. Output resources are
* to be cleaned up.
*/
protected void cleanup(boolean in, boolean out)
throws IOException
{
if (stream != null)
{
stream.cleanup(in, out);
}
}
/**
* Detects when a page break is needed.
*
* @return <code>true</code> if a page break should be output.
*/
boolean needsPageBreak()
{
return stream.needsPageBreak();
}
/**
* Gets the current page size in lines.
*
* @return The number of lines per page.
*/
int rawGetPageSize()
{
if (stream != null)
{
return stream.rawGetPageSize();
}
return 0;
}
/**
* Sets the current page size in lines.
*
* @param sz
* The number of lines per page.
*/
void rawSetPageSize(int sz)
{
if (stream != null)
{
stream.rawSetPageSize(sz);
}
}
/**
* Gets the current page number.
*
* @return The current page number.
*/
int rawGetPageNum()
{
if (stream != null)
{
return stream.rawGetPageNum();
}
return 0;
}
/**
* Increments the current page number.
*/
void rawIncrementPageNum()
{
if (stream != null)
{
stream.rawIncrementPageNum();
}
}
/**
* Gets the current page's next line number.
*
* @return The next line number.
*/
int rawGetNextLineNum()
{
if (stream != null)
{
return stream.rawGetNextLineNum();
}
return 0;
}
/**
* Gets the current page's next logical line number.
*
* @return The next line number.
*/
public integer getNextLineNum()
{
if (stream != null)
{
return stream.getNextLineNum();
}
return new integer();
}
/**
* Increments the current page's next line number.
*/
void rawIncrementNextLineNum()
{
if (stream != null)
{
stream.rawIncrementNextLineNum();
}
}
/**
* Resets the current page's next line number to 1.
*/
void rawResetNextLineNum()
{
if (stream != null)
{
stream.rawResetNextLineNum();
}
}
/**
* Context-local data.
*/
private static class WorkArea
{
/** Stream ids indexed by stream names */
private final Map<String, StreamWrapper> streamByName = new CaseInsensitiveHashMap<>();
/**
* Stores the remote stream name-remoteStream mapping in the context. This method is a no-op when the
* supplied name is {@code null}.
*
* @param name
* Stream name.
* @param remoteStream
* Stream remoteStream.
*/
public void addRemoteStream(String name, StreamWrapper remoteStream)
{
if (name == null)
{
return;
}
streamByName.put(name.toLowerCase(), remoteStream);
}
/**
* Returns the id of a particular remote stream identified by its name. This method is a no-op when
* the supplied name is {@code null}.
*
* @param name
* Stream name, must not be {@code null}.
*/
public void removeRemoteStream(String name)
{
if (name == null)
{
return;
}
streamByName.remove(name.toLowerCase());
}
}
@Override
protected int readBlockRemote(memptr var)
{
// TODO: memptr may be on server-side, improve this
if (stream != null)
{
return stream.readBlockRemote(var);
}
else
{
return super.readBlockRemote(var);
}
}
}