StreamDaemon.java

/*
** Module : StreamDaemon.java
** Public : low level stream primitives in a form that is suitable for use remotely
**
** Copyright (c) 2006-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 GES 20060211   @24555 Created initial version which provides low level stream primitives
**                           in a form that is suitable for use remotely.
** 002 GES 20060212   @24565 Added external lookup of stream by id.
** 003 GES 20060215   @24677 Special close hooks for redirected streams and writeByte().
** 004 GES 20060226   @24755 Added methods to remote the paging state.
** 005 GES 20060817   @28659 Redirected cleanup processing to the client.
** 006 GES 20061102   @30905 During cleanup processing, the stream may extend its last page,
**                           causing page footers to emit.  If this is the case AND the terminal
**                           is not currently redirected, then we force a temporary redirection.
**                           This may be the case if the explicit stream forms of terminal
**                           redirection are in use (as opposed to the implicit terminal
**                           redirection that occurs when the unnamed stream is not the
**                           terminal).
** 007 NVS 20070724   @34679 Now this class maintains the reverse lookup map for stream IDs by
**                           stream as well. The corresponding method has been added.
** 008 NVS 20070725   @34691 Implemented methods isIn() and isOut(), which are newly added to the
**                           LowLevelStream. Fixed resource leaks: streams map needed removal of
**                           the closed streams.
** 009 NVS 20070731   @34757 Implemented advancePage() method, which is newly added to the
**                           LowLevelStream.
** 010 NVS 20070802   @34777 Implemented flush() method, which is newly added to the
**                           LowLevelStream.
** 011 NVS 20070906   @34973 Correction to an error message.
** 012 SIY 20070928   @35327 Added support for necessary methods. Ref: #35314
** 013 SIY 20070928   @35408 Fix for delimiter handling regression. Ref: #35404
** 014 SIY 20071017   @35491 Added setControl() method. Ref: #35486
** 015 CA  20071228   @36617 Added method putWorkerUnformatted to be used put statement with
**                           unformatted values.
** 016 CA  20080304   @37295 Added method setUnnamed.
** 017 SIY 20080317   @37454 Added support for terminal stream. Ref: #37453
** 018 GES 20080412   @38022 Added method setConvert().
** 019 GES 20080425   @38124 Refactored to eliminate round trips to the client by passing more
**                           data in a single invocation. This improves performance.
** 020 SVL 20090612   @42672 Added setUnbuffered() method. If a StopCondition has been raised
**                           into closeOut() or close(), we let the stream to be closed in a
**                           usual way. The "Attempt to write to closed stream" error has been
**                           added to the putWorker().
** 021 GES 20090624   @42894 Eliminate certain placeholder error processing when the stream is
**                           closed. More correct processing is now done on the server side.
** 022 SIY 20090925   @44031 Added getNextLineNum() method.
** 023 SVL 20110403          Added write(byte[]) function.
** 024 GES 20110812          Added readChunk() method.
** 025 GES 20111025          Modified API export code to capture and use the
**                           authentication token so that multiple registrations
**                           can be done per JVM session. This occurs when the
**                           client code is running in the server process and
**                           there is a CTRL-C that causes the client to
**                           restart.
** 026 GES 20111111          Modified interface export to allow registration
**                           to be exclusively local or remote as needed.
** 027 CS  20130103          Implement OS-DIR support as an INPUT stream
** 028 VIG 20130921          Added support of printer stream functionality
**                           (for OUTPUT TO PRINTER statement) 
** 029 CA  20140730          Protected stream close/cleanup code from raised conditions, to allow
**                           close/cleanup fully complete even if conditions were raised.
** 030 CA  20140805          ThinClient is explicitly imported, to isolate ChUI class usage 
**                           outside of ChUI packages.
** 031 GES 20141030          Added setConvertSource/Target() methods.
** 032 EVK 20141012          Added explicit terminal stream support.
**                           Updated openFileStream to open terminal stream if filename is "term"
**                           or "terminal".
** 033 OM  20170613          Added setBinary() method. Added peekCh() method.
** 034 HC  20171024          Runtime support for SYSTEM-DIALOG PRINT-SETUP and OUTPUT TO PRINTER.
** 035 ECF 20171026          Added write(byte[], int, int) method.
** 036 ECF 20180110          Normalize file separators when opening a stream.
** 038 SBI 20180213          Implemented openClipboardStream().
** 039 SBI 20180518          Added PathResolver usages in order to resolve a legacy file system
**                           path on the host system.
** 040 SBI 20180529          Changed openFileStream and openDirStream in order to resolve case
**                           insensitive path names.
** 041 HC  20190712          Implemented stream-by-name resolution.
** 042 GES 20190714          Code cleanup.
**     HC  20190716          Reworked the stream-by-name resolution, this involved reverting the
**                           stream-by-name changes done in entry 041.
** 043 RFB 20200420          Added setKeepMessages method for support of the KEEP-MESSAGES option.
**                           Updated javadoc.
** 044 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.
**     TJD 20220504          Java 11 compatibility minor changes
**     EVL 20221212          Adding LOB-DIR runtime support.
** 045 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.
** 046 GBB 20231213          Adding flag hasDuplicatedLastLine.
** 047 GBB 20240826          Moving OSResourceManager to osresource package.
** 048 GBB 20240912          Moving file / dir stream methods to FileSystem. 
**                           Dependency on ThinClient replaced by the new interface Terminal.
** 049 FER 20250127          Addd openNullStream() method.
*/

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

import com.goldencode.p2j.ui.chui.*;
import com.goldencode.p2j.util.osresource.*;

/**
 * Provides a network interface for stream creation and stream operations which are remote.
 */
public class StreamDaemon
implements StreamHelper
{
   /** Represents the terminal. */
   private Terminal terminal = null;

   /** Stores the list of active streams, keyed by integral stream id. */
   private Map<Integer, Stream> streams = new HashMap<Integer, Stream>();

   /** Same list but for reverse lookups of ID by stream */
   private Map<Stream, Integer> ids = new HashMap<Stream, Integer>();

   /** Next id to attempt to allocate. */
   private int next = 0;
   
   /**
    * Create an instance and export its API to the network.
    *
    * @param    terminal
    *           Provides access to terminal features and helpers.
    */
   public StreamDaemon(Terminal terminal)
   {
      this.terminal = terminal;
   }
   
   /**
    * Constructs a remote stream instance representing a process' standard I/O.
    *
    * @return   The stream id which is used to route operations to the newly created instance.
    */
   public int openProcessStream()
   {
      return store(new ProcessStream());
   }
   
   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @return   The stream id which is used to route operations to the newly created instance.
    */
   public int openTerminalStream()
   {
      return store(new TerminalStream(terminal));
   }

   /**
    * Constructs a remote stream instance representing the Null Stream.
    *
    * @return   The stream id which is used to route operations to the newly created instance.
    */
   public int openNullStream()
   {
      return store(new NullStream());
   }

   /**
    * Constructs the output stream into the client's system clipboard.
    *
    * @return   The stream id which is used to route operations to the newly created instance.
    */
   @Override
   public int openClipboardStream()
   {
      return store(new ClipboardStream());
   }

   /**
    * Constructs a remote printer stream instance.  This can be assigned as unnamed output
    * (OUTPUT TO PRINTER statement) or as a named stream (OUTPUT STREAM someStream TO PRINTER).
    * 
    * @param    name
    *           The name of the target printer. May be <code>null</code>.
    * @param    options
    *           Print options.
    * 
    * @return   The stream id which is used to route operations to the newly created instance.
    */
   public int openPrinterStream(String name, PrintOptions options)
   {
      if (this.terminal.isChui())
      {
         return ChUIPrinterStreamSupport.openPrinterStream(name, this);
      }
      else
      {
         return GUIPrinterStreamSupport.openPrinterStream(name, this, options);
      }
   }

   /**
    * The number of bytes available to be immediately read without blocking.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The number of available bytes.
    * 
    * @throws   IOException 
    */
   public long available(int id)
   throws IOException
   {
      Stream s = lookup(id, -1, "check available bytes from");
      
      if (s == null)
      {
         return -1;
      }
      
      return s.available();
   }   
   
   /**
    * State of the input side of the stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   <code>true</code> if the input side of the stream is active.
    */
   public boolean isIn(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.isIn();
      }

      return false;
   }
   
   /**
    * State of the output side of the stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   <code>true</code> if the output side of the stream is active.
    */
   public boolean isOut(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.isOut();
      }

      return false;
   }

   /**
    * 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.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *           
    * @throws   ErrorConditionException 
    */
   public void advancePage(int id)
   throws ErrorConditionException
   {
      Stream s = streams.get(id);
      
      if (s == null)
      {
         return;
      }

      s.advancePage();
   }

   /**
    * Flushes the stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void flush(int id)
   {
      Stream s = streams.get(id);
      
      if (s == null)
      {
         return;
      }

      s.flush();
   }
   
   /**
    * 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.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The current position in the stream.
    * 
    * @throws   IOException 
    * @throws   UnsupportedOperationException 
    */
   public long getPos(int id)
   throws UnsupportedOperationException,   
          IOException
   {
      Stream s = streams.get(id);
      
      if (s == null)
      {
         return -1;
      }
      
      return s.getPos();
   }   
   
   /**
    * Moves the current read/write position to the specified absolute 0-based
    * offset.  
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    pos
    *           The new read/write position in the stream.
    *           
    * @throws   IOException 
    * @throws   UnsupportedOperationException 
    */
   public void setPos(int id, long pos)
   throws UnsupportedOperationException,   
          IOException
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.setPos(pos);
      }
   }   
             
   
   /**
    * The length of the stream in bytes.  This does not work for "streams"
    * that require sequential access.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The length of the stream.
    * 
    * @throws   IOException 
    * @throws   UnsupportedOperationException 
    */
   public long getLen(int id)
   throws UnsupportedOperationException,   
          IOException
   {
      Stream s = lookup(id, -1, "get length from");
      
      if (s == null)
      {
         return -1;
      }
      
      return s.getLen();
   }   
      
   /**
    * Gets the current page's next logical line number.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The next line number.
    */
   public int getNextLineNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.getNextLineNum().intValue();
      }
      
      return -1;
   }
   
   /**
    * 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    id
    *           Specifies the stream on which this operation is to execute.
    * @param    len
    *           The new length of the file.
    *           
    * @throws   IOException 
    * @throws   UnsupportedOperationException 
    */
   public void setLen(int id, long len)
   throws UnsupportedOperationException,   
          IOException
   {
      Stream s = lookup(id, -1, "set length on");
      
      if (s != null)
      {
         s.setLen(len);
      }
   }   
             
   /**
    * Write the given character to the output stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    ch
    *           The character to be written.
    *           
    * @throws   IOException 
    */
   public void writeCh(int id, char ch)
   throws IOException
   {
      Stream s = lookup(id, 1387, "write to");
      
      if (s != null)
      {
         s.writeCh(ch);
      }
   }   
   
   /**
    * Write the given byte to the output stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    b
    *           The byte to be written.
    *           
    * @throws   IOException 
    */
   public void writeByte(int id, byte b)
   throws IOException
   {
      Stream s = lookup(id, 1387, "write to");
      
      if (s != null)
      {
         s.writeByte(b);
      }
   }      
   
   /**
    * Write the given string to the output stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    data
    *           The data to be written.
    *           
    * @throws   IOException 
    */
   public void write(int id, String data)
   throws IOException
   {
      Stream s = lookup(id, 1387, "write to");
      
      if (s != null)
      {
         s.write(data);
      }
   }
   
   /**
    * Write the given byte array to the output stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    data
    *           The data to be written.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(int id, byte[] data)
   throws IOException
   {
      write(id, data, 0, data.length);
   }
   
   /**
    * Write the specified range of bytes from the given byte array to the output stream.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    data
    *           The data to be written.
    * @param    offset
    *           Starting offset in data from which to read bytes to be written. Must be
    *           non-negative and {@code &lt; data.length}.
    * @param    length
    *           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(int id, byte[] data, int offset, int length)
   throws IOException
   {
      Stream s = lookup(id, 1387, "write to");
      
      if (s != null)
      {
         s.write(data, offset, length);
      }
   }
   
   /**
    * 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(int id)
   {
      Stream s = lookup(id, 1386, "read from");
      return (s == null) ? -1 : s.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.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    */
   public int readCh(int id)
   {
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return -1;
      }
      
      return s.readCh();
   }
   
   /**
    * Read a single byte from the underlying stream.
    * 
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   A single byte from the stream.
    */
   public int readByte(int id)
   {
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return -1;
      }
      
      return s.readByte();
   }
   
   /**
    * 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    id
    *           Specifies the stream on which this operation is to execute.
    * @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
   public int readBlock(int id, memptr var)
   {
      // TODO: memptr may be on server-side, improve this
      
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return -1;
      }

      return s.readBlockWorker(var);
   }
   
   /**
    * 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.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The next line read from the stream.
    * 
    * @throws   EOFException 
    * @throws   IOException 
    * @throws   InterruptedException 
    */
   public String readLn(int id)
   throws EOFException,   
          IOException,
          InterruptedException
   {
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return null;
      }
      
      return s.readLn();
   }
   
   /**
    * 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    id
    *           Specifies the stream on which this operation is to execute.
    * @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. 
    */
   public byte[] readChunk(int id, int len)
   throws IOException
   {
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return null;
      }
      
      return s.readChunk(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    id
    *           Specifies the stream on which this operation is to execute.
    * @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. 
    */
   public byte[] readBytes(int id, int len)
   throws IOException
   {
      Stream s = lookup(id, 1386, "read from");
      
      if (s == null)
      {
         return null;
      }
      
      return s.readBytes(len);
   }

   /**
    * Closes the input stream and releases OS resources associated with it.
    * Any output stream that is open is unaffected.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void closeIn(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.closeIn();

         if (!s.isOut())
         {
            cleanupMaps(s, id);
         }
      }
   }
   
   /**
    * Closes the output stream and releases OS resources associated with it.
    * Any input stream that is open is unaffected.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void closeOut(int id)
   {
      Stream s = streams.get(id);

      StopConditionException exc = null;
      
      if (s != null)
      {
         try
         {
            s.closeOut();
         }
         catch(StopConditionException e)
         {
            exc = e;
         }

         terminal.cleanupPseudoTerminal(id);

         if (!s.isIn())
         {
            cleanupMaps(s, id);
         }
      }

      if (exc != null)
         throw exc;
   }
   
   /**
    * Closes the stream and releases OS resources associated with it.  After
    * completion, further calls may not be made to use this instance, until
    * an <code>assign</code> is used to "re-open" the stream. This is safe to
    * call when no stream is open (it will do nothing in that case).
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void close(int id)
   {
      Stream s = streams.get(id);

      StopConditionException exc = null;
      
      if (s != null)
      {
         try
         {
            s.close();
         }
         catch(StopConditionException e)
         {
            exc = e;
         }

         terminal.cleanupPseudoTerminal(id);

         cleanupMaps(s, id);
      }

      if (exc != null)
         throw exc;
   }
   
   /**
    * Detects when a page break is needed.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   <code>true</code> if a page break should be output.
    */
   public boolean needsPageBreak(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.needsPageBreak();
      }
      
      return false;
   }
   
   /**
    * Gets the current page size in lines.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The number of lines per page.
    */
   public int rawGetPageSize(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.rawGetPageSize();
      }
      
      return -1;      
   }
   
   /**
    * Sets the current page size in lines.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    sz
    *           The number of lines per page.
    */
   public void rawSetPageSize(int id, int sz)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.rawSetPageSize(sz);
      }
   }
   
   /**
    * Gets the current page number.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The current page number.
    */
   public int rawGetPageNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.rawGetPageNum();
      }
      
      return -1;            
   }
   
   /**
    * Increments the current page number.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void rawIncrementPageNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.rawIncrementPageNum();
      }
   }
   
   /**
    * Gets the current page's next line number.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *
    * @return   The next line number.
    */
   public int rawGetNextLineNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         return s.rawGetNextLineNum();
      }
      
      return -1;            
   }
   
   /**
    * Increments the current page's next line number.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void rawIncrementNextLineNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.rawIncrementNextLineNum();
      }
   }
   
   /**
    * Resets the current page's next line number to 1.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void rawResetNextLineNum(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.rawResetNextLineNum();
      }      
   }
   
   /**
    * Perform common close <b>preprocessing</b> for all streams.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @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.
    *           
    * @throws   IOException 
    */
   public void processCleanup(int id, boolean in, boolean out)
   throws IOException
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         boolean remove = false;
         
         if (s.hasPageHeadersFooters() && !terminal.isRedirected())
         {
            remove = true;
            terminal.switchOutput(id, false);
         }
         
         try
         {
            s.cleanup(in, out);
         }
         finally
         {
            if (remove)
            {
               terminal.switchOutput(-1, false);
            }
         }
      }            
   }
   
   /**
    * Returns the stream associated with a given id.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    *           
    * @return   The stream or <code>null</code> if no stream is associated
    *           with the given id.
    */
   public Stream streamFromId(int id)
   {
      return streams.get(id);
   }
   
   /**
    * Returns the stream ID associated with a given stream.
    *
    * @param    s
    *           stream to operate on
    *
    * @return   The stream ID or <code>-1</code> if no stream ID is associated
    *           with the given stream.
    */
   public int idFromStream(Stream s)
   {
      return reverseLookup(s, -1, "access a");
   }
   
   /**
    * Write the current output buffer to the stream. This flushes all 
    * buffered output of the {@link Stream#putField}, {@link Stream#putSpace},
    * {@link Stream#putLineEnd} and {@link Stream#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    id
    *           Specifies the stream on which this operation is to execute.
    * @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(int id, boolean appendNewLine)
   throws ErrorConditionException,
          StopConditionException
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.endOutput(appendNewLine);
      }
   }
   
   /**
    * Output a list of fields to the stream. 
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @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(int id, FieldEntry[] data, int mode, char delim)
   throws ErrorConditionException
   {
      Stream s = streams.get(id);
   
      if (s != null)
      {
         s.putWorker(data, mode, delim);
      }
      else
      {
         ErrorManager.recordOrThrowError(1387, "Attempt to write to closed stream");
      }
   }
   
   /**
    * Set the unnamed field for the stream. 
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    unnamed
    *           <code>true</code> if it is an unnamed stream.
    */
   public void setUnnamed(int id, boolean unnamed)
   {
      Stream s = streams.get(id);
   
      if (s != null)
      {
         s.setUnnamed(unnamed);
      }
   }

   /**
    * Set the the stream UNBUFFERED.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    */
   public void setUnbuffered(int id)
   {
      Stream s = streams.get(id);

      if (s != null)
      {
         s.setUnbuffered();
      }
   }

   /**
    * Sets the stream's character conversion mode.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    convert
    *           The new character conversion mode.  <code>true</code> to
    *           enable character conversion, <code>false</code> to disable
    *           conversion.
    */
   public void setConvert(int id, boolean convert)
   {
      Stream s = streams.get(id);

      if (s != null)
      {
         s.setConvert(convert);
      }
   }
   
   /**
    * Sets the stream's source codepage and activates character conversion mode.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    cp
    *           The source codepage for character conversion mode.
    */
   public void setConvertSource(int id, String cp)
   {
      Stream s = streams.get(id);

      if (s != null)
      {
         s.setConvertSource(cp);
      }
   }
   
   /**
    * Sets the stream's target codepage and activates character conversion mode.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    cp
    *           The target codepage for character conversion mode.
    */
   public void setConvertTarget(int id, String cp)
   {
      Stream s = streams.get(id);

      if (s != null)
      {
         s.setConvertTarget(cp);
      }
   }

   /**
    * Sets the stream's lob directory located as a subdirectory for opened current file stream directory.
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    lobDir
    *           The name for subdirectory to store LOB files.
    */
   @Override
   public void setLobDirectory(int id, String lobDir)
   {
      Stream s = streams.get(id);

      if (s != null)
      {
         s.setLobDirectory(lobDir);
      }
   }
   
   /**
    * Sets the stream into binary mode which disables character translations during I/O
    * operations. 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(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.setBinary();
      }
   }

   /**
    * 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   id
    *          Specifies the stream on which this operation is to execute.
    * @param   num
    *          Font number.
    */
   @Override
   public void setFont(int id, int num)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.setFont(num);
      }
   }

   /**
    * Set stream display mode to landscape
    *
    * @param id Specifies the stream on which this operation is to execute.
    */
   @Override
   public void setLandscape(int id)
   {
      Stream s = streams.get(id);
      if (s != null)
      {
         s.setLandscape();
      }
   }

   /**
    * Set stream display mode to portrait
    *
    * @param id Specifies the stream on which this operation is to execute.
    */
   @Override
   public void setPortrait(int id)
   {
      Stream s = streams.get(id);
      if (s != null)
      {
         s.setPortrait();
      }
   }

   /**
    * Sets the stream's <code>keepMessages</code> mode which causes certain UI statements that write
    * data to the default window to be suppressed when outpu is being redirected.
    * The types of UI statements are 4GL errors and warnings, and UI statements written
    * using MESSAGE.
    *
    * @param   id
    *          Specifies the stream on which this operation is to execute.
    */
   @Override
   public void setKeepMessages(int id)
   {
      Stream s = streams.get(id);
      
      if (s != null)
      {
         s.setKeepMessages();
      }
   }   
   
   /**
    * Assign a new stream id and store the given stream reference using the
    * id as a key.
    *
    * @param    s
    *           The stream to store.
    *
    * @return   The associated id or -1 if the stream instance is
    *           <code>null</code>.
    */
   public int store(Stream s)
   {
      int result = -1;
      
      if (s != null)
      {
         // assumes that the map doesn't have 2 billion keys otherwise we
         // will get an infinite loop here
         while (result == -1)
         {
            Integer id = Integer.valueOf(next);
            
            if (!streams.containsKey(id))
            {
               streams.put(id, s);
               ids.put(s, id);
               result = next;
            }
            
            next++;
            
            // wrap to 0 instead of to negative numbers, 2 billion ids per
            // client should be enough
            if (next == Integer.MAX_VALUE)
            {
               next = 0;
            }
         }
      }
      
      return result;
   }
   
   /**
    * Accesses the stream in the map using the given id.  An error condition 
    * will be raised if the id is invalid (no stream exists by that id).
    *
    * @param    id
    *           Specifies the stream on which this operation is to execute.
    * @param    num
    *           Error number to use on failure.
    * @param    msg
    *           Error text to insert into a standardized message starting 
    *           with "Attempt to" and ending with "closed stream ###" where
    *           the ### represents the stream's id.
    *
    * @return   The local stream reference to use for all operations.
    */
   private Stream lookup(int id, int num, String msg)
   {
      Stream s = streams.get(id);
      
      if (s == null)
      {
         String errmsg = "Attempt to " + msg + " closed stream #" + id;
         ErrorManager.recordOrThrowError(num,  errmsg);
         
         // nothing safe to return in silent mode!
      }
      
      return s;
   }

   /**
    * Accesses the stream ID in the map using the given stream.
    *
    * @param    s
    *           Specifies the stream on which this operation is to execute.
    * @param    num
    *           Error number to use on failure.
    * @param    msg
    *           Error text to insert into a standardized message starting 
    *           with "Attempt to" and ending with "closed stream".
    *
    * @return   The local stream ID
    */
   private int reverseLookup(Stream s, int num, String msg)
   {
      Integer id = ids.get(s);

      if (id == null)
      {
         String errmsg = "Attempt to " + msg + " closed stream";
         ErrorManager.recordOrThrowError(num,  errmsg);
         
         return -1;
      }
      
      return id.intValue();
   }

   /**
    * Removes the traces of the specified stream from the lookup maps.
    *
    * @param    s
    *           Specifies the stream on which this operation is to execute.
    * @param    id
    *           Specified the stream's ID.
    */
   private void cleanupMaps(Stream s, int id)
   {
      streams.remove(Integer.valueOf(id));
      ids.remove(s);
   }
}