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 &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
   {
      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);
      }
   }
}