StreamFactory.java

/*
** Module : StreamFactory.java
** Public : creates stream instances on a remote node
**
** Copyright (c) 2006-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20060211   @24554 Created initial version which creates file and process stream
**                           instances on a remote node.
** 002 SVL 20080219   @37121 openFileStream() returns null if filename equals "terminal".
** 003 SIY 20080317   @37456 Added support for terminal stream. Ref: #37453
** 004 SIY 20090331   @41707 Added more openFileStream() signatures and added detection of
**                           interactive terminal redirection. Ref: #41705
** 005 SIY 20090401   @41714 Fixed regression introduced by #004: wrong order of page size setting calls.
** 006 SIY 20090401   @41715 Do not call setPageSiz() if NO_PAGING is passed as a page size and 
**                           paging flag is set to false. Yet another regression from #004.
** 007 GES 20111020          Modified per-session initialization to detect when the client code
**                           is running inside the server process AND to initialize for local
**                           redirection instead of using a remote object.
** 008 CS  20130103          Implement OS-DIR support as an INPUT stream
** 009 VIG 20120918          Added printer stream support for OUTPUT TO PRINTER statement.
** 010 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 011 EVK 20140601          Empty directory name should be current directory "."
**                           Added additional methods for openFileStream.
**                           Added explicit support for the terminal stream.
** 012 CA  20170829          Added openClipboardStream - not yet supported, returns NullStream. 
** 013 HC  20171024          Runtime support for SYSTEM-DIALOG PRINT-SETUP and OUTPUT TO PRINTER.
** 014 GES 20180115          Added a signature for CLIPBOARD construction.
** 015 SBI 20180213          Linked Clipboard output streams with their implementations.
** 016 SBI 20180412          Fixed javadoc warnings.
** 017 HC  20190714          Implemented stream-by-name resolution.
** 018 OM  20210830          Added CachedInputStream and universal support for reading input streams from jar.
**     CA  20220428          Added openWebStream stubs, for WebSpeed.
** 019 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.
** 020 GBB 20231213          Adding flag hasDuplicatedLastLine.
** 021 GBB 20240826          Moving OSResourceManager to osresource package.
** 022 GBB 20240912          File & dir streams to be created as RemoteStream.
** 023 FER 20250127          Added 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 com.goldencode.p2j.security.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.osresource.*;

import java.io.*;

/**
 * Creates file and process stream instances that are on the remote node associated with the current user's
 * session and which are accessed via a {@link RemoteStream} instance.
 */
public class StreamFactory
{
   /** Special page size which causes turning paging off.*/
   private static final int NO_PAGING = -2;
   
   /** Stores context-local state variables. */
   private static final ContextContainer work = new ContextContainer();

   /**
    * Constructs a remote stream instance representing a file using a filename, this file or device will be
    * opened for reading or writing based on the given {@code write} flag. The current read/write position
    * will be at offset 0 unless in {@code write} mode and the {@code append} flag is on (in which case the
    * write-position will be at the end of the file).
    *
    * @param    filename
    *           The name of the file or device to open.  Must not be {@code null}.
    * @param    write
    *           If {@code true}, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In {@code write} mode, if {@code true}, the current file pointer is set to the end of the
    *           file. Otherwise, if in {@code write} mode, the file is truncated to 0 length. This value is
    *           ignored in {@code read} mode.
    * @param    pageSz
    *           Desired page size. {@code -1} means default paging.
    * @param    paged
    *           {@code true} means that stream is paged. 
    *
    * @return   The new stream instance or {@code null} if an error occurred while silent error mode was
    * active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights to open the
    *           file or device.
    */
   public static Stream openFileStream(String  filename,
                                       boolean write,
                                       boolean append,
                                       int     pageSz,
                                       boolean paged)
   {
      return openFileStream(false, filename, write, append, pageSz, paged);
   }
   
   /**
    * Constructs a remote stream instance representing a file using a filename, this file or device will be
    * opened for reading or writing based on the given {@code write} flag. The current read/write position
    * will be at offset 0 unless in {@code write} mode and the {@code append} flag is on (in which case the
    * write-position will be at the end of the file).
    *
    * @param    hasDuplicatedLastLine
    *           Flag to indicate if the last line in the file is duplicate (true for LOB files).
    * @param    filename
    *           The name of the file or device to open.  Must not be {@code null}.
    * @param    write
    *           If {@code true}, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In {@code write} mode, if {@code true}, the current file pointer is set to the end of the
    *           file. Otherwise, if in {@code write} mode, the file is truncated to 0 length. This value is
    *           ignored in {@code read} mode.
    * @param    pageSz
    *           Desired page size. {@code -1} means default paging.
    * @param    paged
    *           {@code true} means that stream is paged. 
    *           
    * @return   The new stream instance or {@code null} if an error occurred while silent error mode was
    * active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights to open the
    *           file or device.
    */
   public static Stream openFileStream(boolean hasDuplicatedLastLine,
                                       String  filename,
                                       boolean write,
                                       boolean append,
                                       int     pageSz,
                                       boolean paged)
   throws ErrorConditionException
   {
      boolean terminal = false;
      if (filename != null)
      {
         String term = filename.toLowerCase();
         
         if (term.equals("terminal") || term.equals("term"))
         {
            filename = "terminal";
            terminal = true;
         }
      }
      
      // terminal stream without paging is not allowed
      if (terminal && !paged && pageSz == NO_PAGING)
      {
         return openTerminalStream();
      }
      
      if (!write)
      {
         // firstly attempt to locate the resource inside application's jar:
         InputStream stream = LogicalTerminal.getResourceStreamFromApplication(filename);
         if (stream != null)
         {
            // make sure these kinds of streams support stream mark/reset operations needed for some
            // operations in FWD. To work around this, we will cache the whole content in a ByteArray
            try
            {
               return new CachedInputStream(stream);
            }
            catch (IOException e)
            {
               throw new ErrorConditionException(e.getMessage());
            }
         }
      }
      
      int id = work.obtain().fileSystem.openFileStream(filename, write, append, hasDuplicatedLastLine);
      Stream res = (id < 0) ? null : new RemoteStream(id, terminal);
      
      if (res != null)
      {
         if ((pageSz < 0 && pageSz != NO_PAGING) || paged)
         {
            res.setPageSize();
         }
            
         if (pageSz >= 0)
         {
            res.setPageSize(pageSz);
         }
      }
      
      return res;
   }
   
   /**
    * Constructs a remote stream instance representing a file using a filename, this file or
    * device will be opened for reading or writing based on the given <code>write</code> flag.
    * The current read/write position will be at offset 0 unless in <code>write</code> mode and 
    * the <code>append</code> flag is on (in which case the write position will be at the end of
    * the file).
    *
    * @param    filename
    *           The name of the file or device to open.  Must not be <code>null</code>.
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    * @param    sz
    *           Desired page size. <code>-1</code> means default paging.
    *           
    * @return   The new stream instance or <code>null</code> if an error occurred while silent
    *           error mode was active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights
    *           to open the file or device.
    */
   public static Stream openFileStream(String filename, boolean write, boolean append, int sz)
   {
      return openFileStream(filename, write, append, sz, false);
   }

   /**
    * Constructs a remote stream instance representing a file using a filename, this file or
    * device will be opened for reading or writing based on the given <code>write</code> flag.
    * The current read/write position will be at offset 0 unless in <code>write</code> mode and 
    * the <code>append</code> flag is on (in which case the write position will be at the end of
    * the file).
    * <p>
    * This stream will NOT have paging active.
    *
    * @param    hasDuplicatedLastLine
    *           Flag to indicate if the last line in the file is duplicate (true for LOB files).
    * @param    filename
    *           The name of the file or device to open.  Must not be <code>null</code>.
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    *
    * @return   The new stream instance or <code>null</code> if an error occurred while silent
    *           error mode was active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights
    *           to open the file or device.
    */
   public static Stream openFileStream(boolean hasDuplicatedLastLine,
                                       String filename,
                                       boolean write,
                                       boolean append)
   {
      return openFileStream(hasDuplicatedLastLine, filename, write, append, NO_PAGING, false);
   }
   
   /**
    * Constructs a remote stream instance representing a file using a filename, this file or
    * device will be opened for reading or writing based on the given <code>write</code> flag.
    * The current read/write position will be at offset 0 unless in <code>write</code> mode and 
    * the <code>append</code> flag is on (in which case the write position will be at the end of
    * the file).
    * <p>
    * This stream will NOT have paging active.
    *
    * @param    filename
    *           The name of the file or device to open.  Must not be <code>null</code>.
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    *           
    * @return   The new stream instance or <code>null</code> if an error occurred while silent
    *           error mode was active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights
    *           to open the file or device.
    */
   public static Stream openFileStream(String filename, boolean write, boolean append)
   {
      return openFileStream(filename, write, append, NO_PAGING, false);
   }

   /**
    * Constructs a remote stream instance representing a file using a filename, this file or
    * device will be opened for reading or writing based on the given <code>write</code> flag.
    * The current read/write position will be at offset 0 unless in <code>write</code> mode and 
    * the <code>append</code> flag is on (in which case the write position will be at the end of
    * the file).
    *
    * @param    filename
    *           The name of the file or device to open.  Must not be <code>null</code>.
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    * @param    paged
    *           <code>true</code> means that stream is paged with the default page size. 
    *           
    * @return   The new stream instance or <code>null</code> if an error occurred while silent
    *           error mode was active.
    *
    * @throws   ErrorConditionException
    *           If the file does not exist OR if the current user does not have the proper rights
    *           to open the file or device.
    */
   public static Stream openFileStream(String  filename,
                                       boolean write,
                                       boolean append,
                                       boolean paged)
   {
      return openFileStream(filename, write, append, NO_PAGING, paged);
   }
   
   /**
    * Constructs a remote stream instance representing a stream result of 
    * INPUT FROM OS-DIR statement.
    * 
    * @param  dir
    *         The <code>String</code> variable representing the directory for 
    *         which OS-DIR is applied.
    * @param  noAttrList
    *         This indicates if type index will no be output to stream If
    *         <code>true</code> the index will not be output to the stream,
    *         if <code>false</code> it will be output.
    *        
    * @return The <code>Stream</code> wrapping the <code>OsDirStream</code> 
    *         containing the result of OS-DIR implementation, or null if
    *         an error has occurred.
    */
   public static Stream openDirStream(String dir, boolean noAttrList)
   {
      String fixDir = "".equals(dir) ? "." : dir;
      return new RemoteStream(work.obtain().fileSystem.openDirStream(fixDir, noAttrList));
   }
   
   /**
    * Constructs a remote stream instance representing a stream result of 
    * INPUT FROM OS-DIR statement.
    * <p>
    * When no boolean variable representing the noAttrList which decides if 
    * file type will be added to the stream is present the file type will be 
    * added by default.
    * 
    * @param  dir
    *         The <code>String</code> variable representing the directory for 
    *         which OS-DIR is applied.
    *        
    * @return The <code>Stream</code> wrapping the <code>OsDirStream</code> 
    *         containing the result of OS-DIR implementation, or null if
    *         an error has occurred.
    */
   public static Stream openDirStream(String dir)
   {
      return openDirStream(dir, false);
   }
   
   /**
    * Constructs a remote stream instance representing a stream result of 
    * INPUT FROM OS-DIR statement.
    * 
    * @param  dir
    *         The <code>character</code> variable representing the directory 
    *         for which OS-DIR is applied.
    * @param  noAttrList
    *         This indicates if type index will no be output to stream If
    *         <code>true</code> the index will not be output to the stream,
    *         if <code>false</code> it will be output.
    *        
    * @return The <code>Stream</code> wrapping the <code>OsDirStream</code> 
    *         containing the result of OS-DIR implementation, or null if
    *         an error has occurred.
    */
   public static Stream openDirStream(character dir, boolean noAttrList)
   {
      return openDirStream(dir.toStringMessage(), noAttrList);
   }
   
   /**
    * Constructs a remote stream instance representing a stream result of 
    * INPUT FROM OS-DIR statement.
    * <p>
    * When no boolean variable representing the noAttrList which decides if 
    * file type will be added to the stream is present the file type will be 
    * added by default.
    * 
    * @param  dir
    *         The <code>character</code> variable representing the directory 
    *         for which OS-DIR is applied.
    *        
    * @return The <code>Stream</code> wrapping the <code>OsDirStream</code> 
    *         containing the result of OS-DIR implementation, or null if
    *         an error has occurred.
    */
   public static Stream openDirStream(character dir)
   {
      return openDirStream(dir.toStringMessage(), false);
   }   

   /**
    * Constructs a remote stream instance representing a process' standard I/O.
    *
    * @return   The new stream instance.
    */
   public static Stream openProcessStream()
   {
      return new RemoteStream(work.obtain().streamBuilder.openProcessStream());
   }
   
   /**
    * Open a new stream as 'WEB'.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * 
    * @return   The clipboard stream
    */
   public static Stream openWebStream(boolean write)
   {
      return null;
   }

   /**
    * Open a new stream as 'WEB'.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    paged
    *           <code>true</code> if the stream is paged.
    * 
    * @return   The clipboard stream
    */
   public static Stream openWebStream(boolean write, boolean paged)
   {
      UnimplementedFeature.missing("WEB streams are not supported.");
      return null;
   }

   /**
    * Open a new stream as 'WEB'.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    * 
    * @return   The clipboard stream
    */
   public static Stream openWebStream(boolean write, int pageSz)
   {
      return null;
   }

   /**
    * Open a new stream as 'WEB'.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    * @param    paged
    *           <code>true</code> if the stream is paged.
    * 
    * @return   The clipboard stream
    */
   public static Stream openWebStream(boolean write, int pageSz, boolean paged)
   {
      return null;
   }
   
   /**
    * Open a new stream targeting the OS clipboard.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * 
    * @return   The clipboard stream
    */
   public static Stream openClipboardStream(boolean write)
   {
      return openClipboardStream(write, -1, false);
   }
   
   /**
    * Open a new stream targeting the OS clipboard.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    paged
    *           <code>true</code> if the stream is paged.
    * 
    * @return   The clipboard stream
    */
   public static Stream openClipboardStream(boolean write, boolean paged)
   {
      return openClipboardStream(write, -1, paged);
   }
   
   /**
    * Open a new stream targeting the OS clipboard.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    * 
    * @return   The clipboard stream
    */
   public static Stream openClipboardStream(boolean write, int pageSz)
   {
      return openClipboardStream(write, pageSz, true);
   }
   
   /**
    * Open a new stream targeting the OS clipboard.
    * 
    * @param    write
    *           If <code>true</code>, open in write mode, otherwise open the file for reading.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    * @param    paged
    *           <code>true</code> if the stream is paged.
    * 
    * @return   The clipboard stream
    */
   public static Stream openClipboardStream(boolean write, int pageSz, boolean paged)
   {
      int id = work.obtain().streamBuilder.openClipboardStream();
      
      if (id >= 0)
      {
         Stream remoteStream = new RemoteStream(id);
         if (paged)
         {
            if (pageSz >= 0)
            {
               remoteStream.setPageSize(pageSz);
            }
            else
            {
               remoteStream.setPageSize();
            }
         }
         return remoteStream;
      }
      
      return null;
   }
   
   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @return   The new stream instance.
    */
   public static Stream openTerminalStream()
   {
      return openTerminalStream(false, NO_PAGING, false);
   }

   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    *
    * @return   The new stream instance.
    */
   public static Stream openTerminalStream(int pageSz)
   {
      return openTerminalStream(false, pageSz, false);
   }

   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    *
    * @return   null in any case.
    */
   public static Stream openTerminalStream(boolean append)
   {
      return openTerminalStream(append, NO_PAGING, false);
   }

   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    * @param    paged
    *           <code>true</code> if the stream is paged.
    *
    * @return   null in any case.
    */
   public static Stream openTerminalStream(boolean append, boolean paged)
   {
      return openTerminalStream(append, NO_PAGING, paged);
   }

   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    *
    * @return   null in any case.
    */
   public static Stream openTerminalStream(boolean append, int pageSz)
   {
      return openTerminalStream(append, pageSz, false);
   }

   /**
    * Constructs a remote stream instance representing the terminal.
    *
    * @param    append
    *           In <code>write</code> mode, if <code>true</code>, the current file pointer is
    *           set to the end of the file. Otherwise if in <code>write</code> mode, the file
    *           is truncated to 0 length. This value is ignored in <code>read</code> mode.
    * @param    pageSz
    *           Desired page size. <code>-1</code> means default paging.
    * @param    paged
    *           <code>true</code> means that stream is paged.
    *
    * @return   null if stream is not paged.
    */
   public static Stream openTerminalStream(boolean append, int pageSz, boolean paged)
   {
      if (!paged && pageSz == NO_PAGING)
      {
         return null;
      }
      else
      {
         return new RemoteStream(work.obtain().streamBuilder.openTerminalStream(), true);
      }
   }

   /**
    * 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 static Stream openNullStream()
   {
      return new RemoteStream(work.obtain().streamBuilder.openNullStream());
   }
   
   /**
    * Constructs a remote stream instance representing the printer Stream 
    * result of the OUTPUT TO PRINTER statement. 
    * It can be assinged as unnamed output (OUTPUT TO PRINTER statement)
    * or as named stream (OUTPUT STREAM someStream TO PRINTER)
    * 
    * @return   The stream id which is used to route operations to the
    *           newly created instance.
    */   
   public static Stream openPrinterStream()
   {
      return openPrinterStream(null);
   }
   
   /**
    * Constructs a remote stream instance representing the printer Stream 
    * result of the OUTPUT TO PRINTER statement. 
    * It can be assinged as unnamed output (OUTPUT TO PRINTER statement)
    * or as named stream (OUTPUT STREAM someStream TO PRINTER)
    * 
    * @param    printerName
    *           name of target printer
    * 
    * @return   The stream id which is used to route operations to the
    *           newly created instance.
    */   
   public static Stream openPrinterStream(String printerName)
   {
      PrintingService.validatePrinterName(printerName);
      PrintOptions opts = PrintingService.getPrintOptions();
      return new RemoteStream(work.obtain().streamBuilder.openPrinterStream(printerName, opts));
   }

   /**
    * Finds an output stream by its name. Pass {@code null} or an empty string for the unnamed output stream.
    *
    * @param    name
    *           Stream name.
    *
    * @return   The remote stream instance or {@code null} if no such stream exists.
    */
   public static Stream findOutputStream(String name)
   {
      if (name == null || name.isEmpty())
      {
         return UnnamedStreams.output();
      }
      else
      {
         int id = StreamWrapper.findRemoteStreamId(name);
         return id == -1 ? null : new RemoteStream(id);
      }
   }

   /** 
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {  
      /** The remote stream builder instance. */
      private StreamBuilder streamBuilder = null;
      
      /** The file system instance. */
      private FileSystem fileSystem = 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 our proxy, server-side code is left-side by convention
         wa.streamBuilder = OSResourceManager.getStreamHelper();
         
         wa.fileSystem = OSResourceManager.getFileSystem();

         return wa;
      }   
   }
}