UnnamedStreams.java
/*
** Module : UnnamedStreams.java
** Abstract : support for the unnamed in and unnamed out streams
**
** Copyright (c) 2006-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------
** 001 GES 20060207 @24331 Created initial version, supports unnamed
** input and unnamed output.
** 002 GES 20060215 @24678 New features to redirect the terminal and
** handle stacking of top level block defaults.
** 003 GES 20060227 @24773 Redirection clearing flow control change.
** 004 GES 20060323 @25208 Stream related processing can be called on
** the unnamed streams when they are not
** redirected. These all need to be protected
** from null pointers by using a special "safe"
** getter that always returns a stream instance
** even if that instance is essentially bogus.
** 005 GES 20061009 @30223 Support for headless mode.
** 006 NVS 20070817 @34882 Quick return for no-op call of assignOut().
** 007 NVS 20070829 @34974 Added isOut() method that checks for the
** redirection status of the unnamed stream.
** 008 EVL 20071003 @35355 Fix to register callback to automatically
** close input or output stream for internal
** block as well as for external.
** 009 EVL 20071010 @35416 Additional check for in/out stream to be
** valid for the input/output method.
** 010 CA 20071121 @35920 Implemented the correct behavior for the
** situation when there is no explicit
** "output/input close" statement. Refactored
** the assignIn/Out methods to call
** assignIn/OutWorker(Stream,boolean).
** 011 ECF 20071213 @36378 Fixed #010 (@35920). New instance variables
** closerIn/closerOut were incorrectly added to
** ContextContainer inner class, when they were
** required in WorkArea. This was causing these
** stacks to be shared across contexts, which
** resulted in abnormal ends of client sessions.
** 012 CA 20080304 @37296 Mark the stream as unnamed when a new stream
** stream is opened, in assignOutWorker.
** 013 SIY 20080408 @37869 Do not close current stream if switching to
** terminal stream. Otherwise explicit opening of
** terminal stream closes previously opened
** non-terminal stream.
** Inferred generic type arguments.
** Ref: #37868
** 014 SVL 20080603 @38471 Fixed assignOutWorker() for the case when the
** output is redirected to a non-terminal then
** to a terminal stream.
** 015 SVL 20090612 @42674 If a StopCondition has been raised into
** assignOutWorker(), we let the old stream to be
** closed in a usual way.
** 016 CA 20131013 Added no-op deleted() and scopeDeleted() methods, requried by the
** changes in Scopeable and Finalizable interfaces.
** 017 EVL 20131205 Adding support for batch mode error messages.
** 018 OM 20160128 Use TransactionManager._isHeadless() in initialize().
** 019 CA 20190326 Implemented TransactionHelper instead of direct usage of the static
** API. This allows the elimination of context local usage in TM.
** 020 CA 20190707 Stub support for OUTPUT TO ... LOB-DIR.
** 021 CA 20200930 Refactored to remove the singleton scopeable instance (which forced the scope
** notifications to use context-local state).
** CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation.
** OM 20211006 Streams are closed on procedure return instead of TM's scope finished.
** RFB 20220114 Clearing of the closer stacks is the shared responsibility of executeOnReturn
** and scopeFinished. Once something non-null is added to the closer stack, then
** it's the job of executeOnReturn to pop the stack, otherwise, it's on the
** scopeFinished. Ref #5880.
** CA 20220901 Refactored scope notification support: ScopeableFactory was removed, and the
** registration is now specific to each type of scopeable. For each case, the block
** will be registered for scope support (for that particular scopeable) only when
** the scopeable is 'active' (i.e. unnamed streams or accumulators are used). This
** allows a lazy registration of scopeables, to avoid the unnecessary overhead of
** processing all the scopeables for each and every block.
** AL2 20230613 Added procedure helper to the contextual state. Use it where possible.
** 022 CA 20230703 If an unnamed stream is explicitly closed, deregister its 'executeOnReturn'
** cleaner.
** 023 CA 20230929 Javadoc fixes.
** 024 CA 20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
*/
/*
** 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.util.Stack;
import com.goldencode.p2j.security.ContextLocal;
import com.goldencode.p2j.ui.*;
/**
* Stores state and provides functional support for the unnamed in and unnamed out streams.
*/
public class UnnamedStreams
implements Scopeable
{
/** Stores context-local state variables. */
private static final ContextContainer work = new ContextContainer();
/** The state for this instance. */
private final WorkArea wa = new WorkArea();
/**
* Private constructor so external code cannot instantiate this class.
*/
private UnnamedStreams()
{
}
/**
* Gets the stream representing the unnamed input stream.
*
* @return The unnamed input stream or <code>null</code> if the unnamed
* input stream has not been explicitly redirected.
*/
public static Stream input()
{
WorkArea wa = work.get().wa;
Stream result = wa.in;
if (result != null && result.isValid())
return result;
else
{
wa.in = null;
return null;
}
}
/**
* Gets the stream representing the unnamed input stream or a safe stream
* instance that can be used for operations that should effectively be
* <code>NOP</code>.
*
* @return The unnamed input stream to which input is redirected or
* a <code>NullStream</code> instance if the unnamed
* input stream has not been explicitly redirected.
*/
public static Stream safeInput()
{
return makeSafe(work.get().wa.in);
}
/**
* Assigns the stream representing the unnamed input stream. Closes any
* unnamed input stream that is already open.
* Calls assignInWorker(Stream,true).
*
* @param in
* The unnamed input stream to set or <code>null</code> if any
* current input stream redirection is to be cleared.
*/
public static void assignIn(Stream in)
{
// Batch mode special processing.
if (in == null)
{
// Trying to return input 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();
}
}
assignInWorker(work.get().wa, in, true);
}
/**
* Closes the stream representing the unnamed input stream. This will
* restore the default input stream for the current top level scope, which
* may be <code>null</code> (if the current redirection when the scope
* opened was <code>null</code>).
*/
public static void closeIn()
{
closeInInt(work.get().wa);
}
/**
* Gets the stream representing the unnamed output stream.
*
* @return The unnamed output stream or <code>null</code> if the unnamed
* output stream has not been explicitly redirected.
*/
public static Stream output()
{
WorkArea wa = work.get().wa;
Stream result = wa.out;
if (result != null && result.isValid())
return result;
else
{
wa.out = null;
return null;
}
}
/**
* Gets the stream representing the unnamed output stream or a safe stream
* instance that can be used for operations that should effectively be
* <code>NOP</code>.
*
* @return The unnamed output stream to which output is redirected or
* a <code>NullStream</code> instance if the unnamed
* output stream has not been explicitly redirected.
*/
public static Stream safeOutput()
{
return makeSafe(work.get().wa.out);
}
/**
* Assigns the stream representing the unnamed output stream. Closes any
* stream that is already open. Calls assignOutWorker(Stream,true).
*
* @param out
* The unnamed output stream to set or <code>null</code> if any
* current output stream redirection is to be cleared.
* @param lobDir
* The folder where to write any CLOB or BLOB fields.
*/
public static void assignOut(Stream out, String lobDir)
{
// TODO: manager lobDir
assignOut(out);
}
/**
* Assigns the stream representing the unnamed output stream. Closes any
* stream that is already open. Calls assignOutWorker(Stream,true).
*
* @param out
* The unnamed output stream to set or <code>null</code> if any
* current output stream redirection is to be cleared.
*/
public static void assignOut(Stream out)
{
// Batch mode special processing.
if (out == null)
{
// Trying to return 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();
}
}
assignOutWorker(work.get().wa, out, true);
}
/**
* Closes the stream representing the unnamed output stream. This will
* restore the default output stream for the current top level scope, which
* may be <code>null</code> (if the current redirection when the scope
* opened was <code>null</code>).
*/
public static void closeOut()
{
closeOutInt(work.get().wa);
}
/**
* Assigns the streams representing both the unnamed output stream and the
* unnamed input stream. Closes the input or output streams if either
* are already open.
*
* @param stream
* The unnamed output stream to set or <code>null</code> if any
* current output stream redirection is to be cleared.
*/
public static void assignBoth(Stream stream)
{
assignIn(stream);
assignOut(stream);
work.get().wa.both = true;
}
/**
* Closes both the unnamed input stream and the unnamed output stream.
*/
public static void closeBoth()
{
closeIn();
closeOut();
work.get().wa.both = false;
}
/**
* Provides a notification that a new scope is about to be entered. Used
* to store the new default input and output streams for this scope.
*
* @param block
* The explicit block definition which required this notification.
*/
@Override
public void scopeStart(BlockDefinition block)
{
wa.lastIn.push(wa.in);
wa.lastOut.push(wa.out);
wa.closerIn.push(null);
wa.closerOut.push(null);
}
/**
* Provides a notification that a scope is about to be exited. Used to
* clear the default input and output streams back to the previous scope's
* versions, unless it is the responsibility of executeOnReturn.
*/
public void scopeFinished()
{
wa.lastIn.pop();
wa.lastOut.pop();
if (wa.closerIn.peek() == null)
{
wa.closerIn.pop();
}
if (wa.closerOut.peek() == null)
{
wa.closerOut.pop();
}
}
/**
* Provides notification that the external procedure scope has been deleted.
* <p>
* This is a no-op for unnamed streams.
*/
@Override
public void scopeDeleted()
{
// no-op
}
/**
* Get the {@link ScopeId} for the instance.
*
* @return {@link ScopeId#UNNAMED_STREAMS}.
*/
@Override
public ScopeId getScopeId()
{
return ScopeId.UNNAMED_STREAMS;
}
/**
* State of the output side of the unnamed stream.
*
* @return <code>true</code> if the output side of the stream is active.
*/
public static boolean isOut()
{
return work.get().wa.out != null;
}
/**
* Returns the given stream if it is not <code>null</code>, otherwise it
* returns a safe stream instance that can be used for operations that
* should effectively be a <code>NOP</code>.
*
* @param safe
* The stream to check.
*
* @return The unnamed input stream given or a <code>NullStream</code>
* instance if the given stream is <code>null</code>.
*/
private static Stream makeSafe(Stream safe)
{
// we aren't redirected
if (safe == null)
{
WorkArea wa = work.get().wa;
// setup the bogus stream the first time in
if (wa.bogus == null)
{
wa.bogus = new NullStream();
}
safe = wa.bogus;
}
return safe;
}
/**
* Assigns the stream representing the unnamed input stream. Closes any
* unnamed input stream that is already open.
*
* @param wa
* The context-local state.
* @param in
* The unnamed input stream to set or <code>null</code> if any
* current input stream redirection is to be cleared.
* @param register
* If <code>true</code> and the stream parameter is non-null,
* then a call to registerTopLevelFinalizable will be performed.
*/
private static void assignInWorker(WorkArea wa, Stream in, boolean register)
{
wa.tm.registerTopLevelScopeable(work.get());
// default input for this top level scope
Stream def = wa.lastIn.peek();
if (wa.in != null)
{
if (wa.in == def && in != null)
{
// don't close the default stream when we are switching to
// another (non-terminal) stream
}
else
{
if (wa.both)
{
wa.in.closeIn();
}
else
{
wa.in.close();
}
}
}
wa.both = false;
wa.in = in;
if (register && in != null)
{
// the InputCloser must be executed BEFORE the Stream is finalized this is required because the
// RemoteStream.close will set the stream ID to -1 and so the UnnamedStreams.closeIn method will
// never be executed if there is no registered input closer, we register it
if (wa.closerIn.peek() == null)
{
InputCloser closer = new InputCloser(wa);
wa.pm.executeOnReturn(closer, closer.weight());
// add the closer to the stack
wa.closerIn.pop();
wa.closerIn.push(closer);
}
wa.pm.executeOnReturn(in::finished, in.weight());
}
}
/**
* Assigns the stream representing the unnamed output stream. Closes any
* stream that is already open.
*
* @param wa
* The context-local state.
* @param out
* The unnamed output stream to set or <code>null</code> if any
* current output stream redirection is to be cleared.
* @param register
* If <code>true</code> and the stream parameter is non-null,
* then a call to registerTopLevelFinalizable will be performed.
*/
private static void assignOutWorker(WorkArea wa, Stream out, boolean register)
{
wa.tm.registerTopLevelScopeable(work.get());
// save ids before we make any state changes
int oldId = ((wa.out == null) ? -1 : ((RemoteStream) wa.out).getId());
int newId = ((out == null) ? -1 : ((RemoteStream) out).getId());
if (out != null)
{
out.setUnnamed(true);
}
if (newId == oldId)
{
return;
}
// default output for this top level scope
Stream def = wa.lastOut.peek();
StopConditionException exc = null;
// close the stream resources
if (wa.out != null)
{
if (wa.out == def && out != null)
{
// don't close the default stream when we are switching to
// another (non-terminal) stream
}
else if (wa.out == def && newId == -1 && register)
{
// don't close the default stream when we are switching to
// terminal stream, but since we're switching do some cleanups
}
else
{
try
{
if (wa.both)
{
wa.out.closeOut();
}
else
{
wa.out.close();
}
}
catch(StopConditionException e)
{
exc = e;
}
}
}
// we must notify the UI to redirect output/cleanup resources (must
// be AFTER closing the stream as close() can cause page footers to
// be output and thus the redirection must stay until that is done)
LogicalTerminal.redirectOutput(oldId, newId);
wa.both = false;
wa.out = out;
if (register && out != null)
{
// the OutputCloser must be executed BEFORE the Stream is finalized
// this is required because the RemoteStream.close will set the
// stream ID to -1 and so the UnnamedStreams.closeOut method will
// never be executed
// if there is no registered output closer, we register it
if (wa.closerOut.peek() == null)
{
OutputCloser closer = new OutputCloser(wa);
wa.pm.executeOnReturn(closer, closer.weight());
// add the closer to the stack
wa.closerOut.pop();
wa.closerOut.push(closer);
}
wa.pm.executeOnReturn(out::finished, out.weight());
}
if (exc != null)
{
throw exc;
}
}
/**
* Closes the stream representing the unnamed input stream. This will
* restore the default input stream for the current top level scope, which
* may be <code>null</code> (if the current redirection when the scope
* opened was <code>null</code>).
*
* @param wa
* The context-local state.
*/
private static void closeInInt(WorkArea wa)
{
assignInWorker(wa, wa.lastIn.peek(), false);
// we pop the closer from the stack
if (!wa.closerIn.empty())
{
InputCloser closer = wa.closerIn.pop();
if (closer != null)
{
wa.pm.deregisterExecuteOnReturn(closer, closer.weight());
}
wa.closerIn.push(null);
}
}
/**
* Closes the stream representing the unnamed output stream. This will
* restore the default output stream for the current top level scope, which
* may be <code>null</code> (if the current redirection when the scope
* opened was <code>null</code>).
*
* @param wa
* The context-local state.
*/
private static void closeOutInt(WorkArea wa)
{
assignOutWorker(wa, wa.lastOut.peek(), false);
// we pop the closer from the stack
if (!wa.closerOut.empty())
{
OutputCloser closer = wa.closerOut.pop();;
if (closer != null)
{
wa.pm.deregisterExecuteOnReturn(closer, closer.weight());
}
wa.closerOut.push(null);
}
}
/**
* Stores global data relating to the state of the current context.
*/
private static class WorkArea
{
/** Helper to use the TM without any context local lookups. */
private final TransactionManager.TransactionHelper tm = TransactionManager.getTransactionHelper();
/** Helper to use the PM without any context local lookups. */
private final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();
/** Flags whether the input stream represents both destinations. */
private boolean both = false;
/** A fake stream for safe getters for this user's session. */
private Stream bogus = null;
/** The current unnamed input stream for this user's session. */
private Stream in = null;
/** The current unnamed output stream for this user's session. */
private Stream out = null;
/** Track the default unnamed input stream for the top level block. */
private final Stack<Stream> lastIn = new Stack<>();
/** Track the default unnamed output stream for the top level block. */
private final Stack<Stream> lastOut = new Stack<>();
/** Track the current InputCloser instance for this block. */
public Stack<InputCloser> closerIn = new Stack<>();
/** Track the current OutputCloser instance for this block. */
public Stack<OutputCloser> closerOut = new Stack<>();
}
/**
* Simple container that stores and returns a context-local instance of
* the global work area.
*/
private static class ContextContainer
extends ContextLocal<UnnamedStreams>
{
/**
* Initializes the work area, the first time it is requested within a
* new context.
* <p>
* Initializes the closerIn and closerOut trackers, the first time it is
* requested within a new context.
*
* @return The newly instantiated work area.
*/
protected UnnamedStreams initialValue()
{
UnnamedStreams instance = new UnnamedStreams();
// make sure we always have an entry in the stacks (avoids empty
// stack exceptions)
instance.wa.lastIn.push(null);
instance.wa.lastOut.push(null);
// add a null reference to know there are no Input/OutputClosers
// registered
instance.wa.closerIn.push(null);
instance.wa.closerOut.push(null);
return instance;
}
}
/**
* Finalizable class which will perform a closeIn call only if this class
* is in the top of the closerIn stack - this way we make sure there are
* not possible two subsequent calls to the closeIn statement.
*/
private static class InputCloser
implements Finalizable,
Runnable
{
/** The context-local state. */
private final WorkArea wa;
/**
* Create a new instance and cache the context-local state.
*
* @param wa
* The context-local state.
*/
public InputCloser(WorkArea wa)
{
this.wa = wa;
}
/**
* Execute {@link #finished()}, required for {@link ProcedureManager$ProcedureHelper#executeOnReturn}.
*/
@Override
public void run()
{
finished();
}
/**
* This method will be called when the current block will be popped out
* of scope and will close the stream if there is no explicit
* "input close" statement.
*/
public void finished()
{
// Determine if the internal close is to be executed,
// and if cleanup is our reponsibility instead of scopeFinished
InputCloser topItem = wa.closerIn.peek();
if (topItem == this)
{
closeInInt(wa);
}
if (topItem != null)
{
wa.closerIn.pop();
}
}
/**
* Provides a notification that the external program scope in which the object is registered is
* is being deleted and the object's reference will be lost after this method is called.
* <p>
* This is a no-op for unnamed streams.
*/
@Override
public void deleted()
{
// no-op
}
/**
* This method is unused.
*
* @see com.goldencode.p2j.util.Finalizable#iterate()
*/
public void iterate()
{
// do nothing
}
/**
* This method is unused.
*
* @see com.goldencode.p2j.util.Finalizable#iterate()
*/
public void retry()
{
// do nothing
}
}
/**
* Finalizable class which will perform a closeOut call only if this class
* is in the top of the closerOut stack - this way we make sure there are
* not possible two subsequent calls to the closeOut statement.
*/
private static class OutputCloser
implements Finalizable,
Runnable
{
/** The context-local state. */
private final WorkArea wa;
/**
* Create a new instance and cache the context-local state.
*
* @param wa
* The context-local state.
*/
public OutputCloser(WorkArea wa)
{
this.wa = wa;
}
/**
* Execute {@link #finished()}, required for {@link ProcedureManager$ProcedureHelper#executeOnReturn}.
*/
@Override
public void run()
{
finished();
}
/**
* This method will be called when the current block will be popped out
* of scope and will close the stream if there is no explicit
* "output close" statement.
*/
public void finished()
{
// Determine if the internal close is to be executed,
// and if cleanup is our reponsibility instead of scopeFinished
OutputCloser topItem = wa.closerOut.peek();
if (topItem == this)
{
closeOutInt(wa);
}
if (topItem != null)
{
wa.closerOut.pop();
}
}
/**
* Provides a notification that the external program scope in which the object is registered is
* is being deleted and the object's reference will be lost after this method is called.
* <p>
* This is a no-op for unnamed streams.
*/
@Override
public void deleted()
{
// no-op
}
/**
* This method is unused.
*
* @see com.goldencode.p2j.util.Finalizable#iterate()
*/
public void iterate()
{
// do nothing
}
/**
* This method is unused.
*
* @see com.goldencode.p2j.util.Finalizable#iterate()
*/
public void retry()
{
// do nothing
}
}
}