RemoteStream.java

/*
** Module : RemoteStream.java
** Public : represents a stream instance that is on a remote node and 
**          redirects all operations to that instance
**
** Copyright (c) 2006-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
** 001 GES 20060211   @24553 Created initial version which provides
**                           represents a stream instance that is on a
**                           remote node and redirects all operations to
**                           that instance.
** 002 GES 20060217   @24673 Added writeByte().
** 003 GES 20060226   @24753 Added methods to remote the paging state.
** 004 GES 20060302   @24858 Changes to enable removal from the
**                           finalizable lists.
** 005 GES 20060817   @28660 Redirected cleanup processing to the client.
** 006 GES 20061206   @31582 Reduce the number of trips to the client
**                           since page size can only be set from the
**                           server, it can be cached and reused locally.
** 007 NVS 20070725   @34689 Implemented methods isIn() and isOut(), which
**                           are newly added to the base class as abstract.
** 008 NVS 20070731   @34754 Implemented advancePage() which overrides the
**                           method in the base class. This implementation
**                           delegates the call to the remote stream.
** 009 NVS 20070802   @34775 Implemented flush() which overrides the
**                           method in the base class. This implementation
**                           delegates the call to the remote stream.
** 010 SIY 20070928   @35325 Fix for PUT/DISPLAY handling regressions.
**                           Ref: #35314
** 011 SIY 20071003   @35354 Fix for NPE during remote call.
**                           Ref: #35347.
** 012 EVL 20071005   @35375 Fixed isOut() method.
** 013 SIY 20071009   @35407 Fix for delimiter handling regression.
**                           Ref: #35404
** 014 EVL 20071010   @35419 Change to isIn()/isOut to check the stream is
**                           valid.
** 015 SIY 20071017   @35489 Promoted setControl() method, added forgotten
**                           endOutput() method.
**                           Ref: #35486
** 016 GES 20071218   @36430 Switched to a safer method of determining if
**                           close was called from within finished()
**                           processing. This is needed because the
**                           stream level close can be called during
**                           finished processing of any arbitrary class
**                           and the previous method was only safe if
**                           called from within Stream.finished.
** 017 CA  20071228   @36616 Added method putUnformatted.
** 018 CA  20080304   @37294 Added method setUnnamed.
** 019 SIY 20080317   @37459 Added support for terminal stream.
**                           Ref: #37453
** 020 GES 20080412   @38020 Added method setConvert().
** 021 GES 20080425   @38122 Refactored to eliminate round trips to the
**                           client by passing more data in a single
**                           invocation. This improves performance.
** 022 GES 20080430   @38177 Removed unnecessary calls to endOutput().
** 023 SVL 20090612   @42676 Added setUnbuffered() method.
** 024 SIY 20090925   @44033 Added getNextLineNum() method. 
** 025 SIY 20091009   @44129 Added caching of isOut/isIn in order to minimize
**                           round-trips to client.
** 026 SVL 20110403          Added write(byte[]) function.
** 027 GES 20110812          Added readChunk() method.
** 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 EVL 20160225          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 031 OM  20170313          Propagated [binary] property to peer stream. Added peekCh() method.
** 032 HC  20171024          Added FONT option to the OUTPUT TO PRINTER statement this is a FWD
**                           extension to the 4GL language.
** 033 ECF 20171026          Added write(byte[], int, int) method.
** 034 RFB 20200419          Added setKeepMessages in support of I/O option KEEP-MESSAGES.
** 035 CA  20210317          Improved performance of the readBlock(memptr) API.
**     CA  20210709          Added APIs to read pristine 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.
**     EVL 20221212          Adding LOB-DIR runtime support.
** 036 PMP 20240509          Modified putWorker method to check for null field entries.
** 037 GBB 20240912          Accessing LowLevelStream via OSResourceManager. WorkArea field renamed.
*/

/*
** 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 java.io.*;

import com.goldencode.p2j.security.ContextLocal;
import com.goldencode.p2j.util.osresource.*;

/**
 * A stream class which represents a stream instance that is on a remote node 
 * and which redirects all local method calls into operations to that
 * instance.
 *
 * @author    GES
 */
public class RemoteStream
extends Stream
{
   /** The stream id on the remote node. */
   private int id = -1;
   
   /** The locally cached page size. */
   private int sz = -1;
   
   /** Stores context-local state variables. */
   private static ContextContainer work = new ContextContainer();
   
   /** Mark terminal stream. */
   private boolean term = false;
   
   /** Control caching of IsOut flag. */
   private boolean cacheIsOut = false;
   
   /** Control caching of IsIn flag. */
   private boolean cacheIsIn  = false;
   
   /** Cached IsOut flag. */
   private boolean savedIsOut = false;
   
   /** Cached IsIn flag. */
   private boolean savedIsIn  = false;
      
   /**
    * Create an instance to represent a specific remote stream. 
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.    
    */
   public RemoteStream(int id)
   {
      this.id = id;
   }
   
   /**
    * Create an instance to represent a specific remote stream.
    * 
    * @param   id
    *          Specifies the stream on which this operation is to execute.
    * @param   term
    *          If <code>true</code> then stream ID belongs to terminal
    *          stream.
    */
   public RemoteStream(int id, boolean term)
   {
      this(id);
      this.term = term;
   }
   
   /**
    * Check if given instance is bound to terminal.
    * 
    * @return  <code>true</code> if remote stream is a terminal.
    */
   public boolean isTerm()
   {
      return term;
   }
   
   /**
    * State of the input side of the stream.
    *
    * @return   <code>true</code> if the input side of the stream is active.
    */
   public boolean isIn()
   {
      if (id == -1)
         return false;
      
      if (cacheIsIn)
         return savedIsIn;
      
      cacheIsIn = true;
      
      return  savedIsIn = work.obtain().streamsAccessor.isIn(id);
   }
   
   /**
    * State of the output side of the stream.
    *
    * @return   <code>true</code> if the output side of the stream is active.
    */
   public boolean isOut()
   {
      if (id == -1)
         return false;
      
      if (cacheIsOut)
         return savedIsOut;
      
      cacheIsOut = true;
      
      return savedIsOut = work.obtain().streamsAccessor.isOut(id);
   }   
   
   /**
    * Get the stream's remote id.
    *
    * @return   The remote id used to access the stream. -1 is an invalid 
    *           stream id and indicates that the stream is not properly
    *           initialized.
    */
   public int getId()
   {
      return id;
   }
   
   /**
    * 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 work.obtain().streamsAccessor.available(id);
   }
   
   /**
    * 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 work.obtain().streamsAccessor.getPos(id);
   }
   
   /**
    * 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
   {
      work.obtain().streamsAccessor.setPos(id, 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 work.obtain().streamsAccessor.getLen(id);
   }
   
   /**
    * 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
   {
      work.obtain().streamsAccessor.setLen(id, len);
   }
   
   /**
    * Sets the stream into binary mode which disables character translations
    * during {@link #readChar}.  It is important to note that field and line
    * reading (<code>import</code>, <code>set</code>, <code>update</code>
    * <code>prompt-for</code> and <code>import unformatted</code>) are
    * unaffected by this setting (this is based on Progress testcases).
    */
   @Override
   public void setBinary()
   {
      super.setBinary();
      work.obtain().streamsAccessor.setBinary(id);
   }
   
   /**
    * Write the given character to the output stream.
    *
    * @param    ch
    *           The character to be written.
    * 
    * @throws   IOException 
    */
   public void writeCh(char ch)
   throws IOException
   {
      work.obtain().streamsAccessor.writeCh(id, 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
   {
      work.obtain().streamsAccessor.writeByte(id, 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
   {
      work.obtain().streamsAccessor.write(id, 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
   {
      work.obtain().streamsAccessor.write(id, 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 &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
   {
      work.obtain().streamsAccessor.write(id, 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 work.obtain().streamsAccessor.peekCh(id);
   }
   
   /**
    * 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 work.obtain().streamsAccessor.readCh(id);
   }
   
   /**
    * Read a single byte from the underlying stream.
    * 
    * @return   A single byte from the stream.
    */
   @Override
   public int readByte()
   {
      return work.obtain().streamsAccessor.readByte(id);
   }
   
   /**
    * 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 work.obtain().streamsAccessor.readLn(id);
   }
   
   /**
    * Read a chunk of characters 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 characters 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. 
    */
   public byte[] readChunk(int len)
   throws IOException
   {
      return work.obtain().streamsAccessor.readChunk(id, len);
   }
   
   /**
    * 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 work.obtain().streamsAccessor.readBytes(id, len);
   }
   
   /**
    * Closes the input stream and releases OS resources associated with it.
    * Any output stream that is open is unaffected.
    */
   public void closeIn()
   {
      try
      {
         cleanup(true, false);
         work.obtain().streamsAccessor.closeIn(id);
      }
      catch (IOException io)
      {
         // silently exit
      }
      finally
      {
         cacheIsOut = false;
         cacheIsIn  = false;
      }
   }
   
   /**
    * Closes the output stream and releases OS resources associated with it.
    * Any input stream that is open is unaffected.
    */
   public void closeOut()
   {
      try
      {
         try
         {
            cleanup(false, true);
         }
         finally
         {
            work.obtain().streamsAccessor.closeOut(id);
         }
      }
      catch (IOException io)
      {
         // silently exit
      }
      finally
      {
         cacheIsOut = false;
         cacheIsIn  = false;
      }
   }
   
   /**
    * 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()
   {
      try
      {
         cleanup(true, true);
         
         if (id >= 0)
         {
            work.obtain().streamsAccessor.close(id);
            id = -1;
         }
      }
      catch (IOException io)
      {
         // silently exit
      }
      finally
      {
         cacheIsOut = false;
         cacheIsIn  = 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
   {
      // cleanup
      if (id >= 0)
      {
         work.obtain().streamsAccessor.close(id);
      }
      
      // reset
      RemoteStream rs = (RemoteStream) s;
      id   = rs.id;
      term = rs.term;
   }   
   
   /**
    * 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
   {
      work.obtain().streamsAccessor.advancePage(id);
   }
   
   /**
    * Flushes the stream.
    */
   public void flush()
   {
      work.obtain().streamsAccessor.flush(id);
   }
   
   /**
    * 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
   {
      for (FieldEntry entry : data)
      {
         if (entry != null)
         {
            entry.resolveData();
         }  
      }
      
      work.obtain().streamsAccessor.putWorker(id, data, mode, delim);
   }
   
   /**
    * Set the unnamed status for this stream. 
    *
    * @param    unnamed
    *           <code>true</code> if this is an unnamed stream.
    */
   public void setUnnamed(boolean unnamed)
   {
      work.obtain().streamsAccessor.setUnnamed(id, unnamed);
   }

   /**
    * Makes the stream UNBUFFERED.
    */
   public void setUnbuffered()
   {
      work.obtain().streamsAccessor.setUnbuffered(id);
   }

   /**
    * 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);
      
      work.obtain().streamsAccessor.setConvert(id, 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);

      work.obtain().streamsAccessor.setConvertSource(id, 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);
      
      work.obtain().streamsAccessor.setConvertTarget(id, 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.
    */
   @Override
   public void setLobDirectory(String lobDir)
   {
      // save it locally as well
      super.setLobDirectory(lobDir);
      
      work.obtain().streamsAccessor.setLobDirectory(id, 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
    * (FWD extension to 4GL) statement. This method is only implemented by the printer stream.
    *
    * @param   num
    *          Font number.
    */
   @Override
   public void setFont(int num)
   {
      work.obtain().streamsAccessor.setFont(id, num);
   }

   /**
    * Set stream display mode to landscape
    */
   @Override
   public void setLandscape()
   {
      super.setLandscape();
      work.obtain().streamsAccessor.setLandscape(id);
   }

   /**
    * Sets the stream's <code>keepMessages</code> mode which causes certain UI statements that write
    * data to the default window to be suppressed when output is being redirected.
    * The types of UI statements are 4GL errors and warnings, and UI statements written
    * using MESSAGE.
    */
   @Override
   public void setKeepMessages()
   {
      super.setKeepMessages();
      work.obtain().streamsAccessor.setKeepMessages(id);
   }   
   
   /**
    * Set stream display mode to portrait
    */
   @Override
   public void setPortrait()
   {
      super.setPortrait();
      work.obtain().streamsAccessor.setPortrait(id);
   }

   /**
    * 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
   {
      work.obtain().streamsAccessor.endOutput(id, appendNewLine);
   }
   
   /**
    * Read a block of data from the stream with the block size equal to the size of the binary data array in 
    * the given <code>memptr</code> variable. The data will be written into the memptr starting at the first 
    * byte in the array.  The read data will be the smaller of the block size or the file size.  Only an error 
    * or end of file condition will cause this read to terminate before the block size has been read.
    *
    * @param    var
    *           The variable to which the result is assigned.
    *           
    * @return   The number of the bytes read, -1 on any failure and -2 upon an <code>EOF</code>.
    */
   @Override
   protected int readBlockRemote(memptr var)
   {
      // TODO: memptr may be on server-side, improve this
      
      return work.obtain().streamsAccessor.readBlock(id, var);
   }

   /**
    * 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
   {
      // DON'T call the superclass, but mark the protected members so that
      // other processing is safe
      doneIn  = in  ? in  : doneIn;
      doneOut = out ? out : doneOut;
      
      // redirect to the client
      try
      {
         work.obtain().streamsAccessor.processCleanup(id, in, out);
      }
      finally
      {
         boolean finish = (TransactionManager.currentOperation() == 
                           TransactionManager.OP_FINISHED);
         
         // external/explicit close requests must cleanup finalizable lists to
         // ensure no memory leaks occur
         if (!finish && doneIn && doneOut)
         {
            // remove from global or procedure finalizable lists (first instance
            // found is removed, there shouldn't be more than 1 registered)
            TransactionManager.removeFinalizable(this);
         }
      }
   }
   
   /**
    * Detects when a page break is needed.
    *
    * @return   <code>true</code> if a page break should be output.
    */
   boolean needsPageBreak()
   {
      return work.obtain().streamsAccessor.needsPageBreak(id);
   }
   
   /**
    * Gets the current page size in lines.
    *
    * @return   The number of lines per page.
    */
   int rawGetPageSize()
   {
      // check our cache, if not valid then obtain the latest value
      if (sz == -1)
         sz = work.obtain().streamsAccessor.rawGetPageSize(id);
      
      return sz;
   }
   
   /**
    * Sets the current page size in lines.
    *
    * @param    sz
    *           The number of lines per page.
    */
   void rawSetPageSize(int sz)
   {
      // clear the cache
      this.sz = -1;
      
      work.obtain().streamsAccessor.rawSetPageSize(id, sz);
   }
   
   /**
    * Gets the current page number.
    *
    * @return   The current page number.
    */
   int rawGetPageNum()
   {
      return work.obtain().streamsAccessor.rawGetPageNum(id);
   }
   
   /**
    * Increments the current page number.
    */
   void rawIncrementPageNum()
   {
      work.obtain().streamsAccessor.rawIncrementPageNum(id);
   }
   
   /**
    * Gets the current page's next line number.
    *
    * @return   The next line number.
    */
   int rawGetNextLineNum()
   {
      return work.obtain().streamsAccessor.rawGetNextLineNum(id);
   }

   /**
    * Gets the current page's next logical line number.
    *
    * @return   The next line number.
    */
   public integer getNextLineNum()
   {
      return new integer(work.obtain().streamsAccessor.getNextLineNum(id));
   }
   
   /**
    * Increments the current page's next line number.
    */
   void rawIncrementNextLineNum()
   {
      work.obtain().streamsAccessor.rawIncrementNextLineNum(id);
   }
   
   /**
    * Resets the current page's next line number to 1.
    */
   void rawResetNextLineNum()
   {
      work.obtain().streamsAccessor.rawResetNextLineNum(id);
   }
   
   /** 
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {  
      /** The remote stream server instance. */
      private LowLevelStream streamsAccessor = null;
   }
   
   /**
    * Simple container that stores and returns a context-local instance of
    * the global work area.
    */
   private static class ContextContainer
   extends ContextLocal<WorkArea>
   {
      /**
       * Obtains the context-local instance of the contained global work
       * area.
       *
       * @return   The work area associated with this context.
       */
      public WorkArea obtain() 
      {
         return this.get();
      }   
      
      /**
       * Initializes the work area, the first time it is requested within a
       * new context.
       *
       * @return   The newly instantiated work area.
       */
      protected synchronized WorkArea initialValue()
      {
         WorkArea wa = new WorkArea();
         
         // obtain the proxy
         wa.streamsAccessor = OSResourceManager.getStreamHelper();
         
         return wa;
      }   
   }
}