BufferSizeManager.java

/*
** Module   : BufferSizeManager.java
** Abstract : singleton that keeps the "reference" buffer sizes and allows
**            to tell when an opened stream has reached its buffer size limit
**            after it has been broken. 
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
** 001 SVL 20090612   @42689 Created initial version. Tracks opened streams
**                           and in the case of error allows to tell when we
**                           have exceeded the buffer size limit.
** 002 SVL 20090616   @42698 Make getStreamInfo() more tolerant.
** 003 SVL 20090707   @43086 Store stream info only for ProcessStreams that
**                           have been broken.
** 004 GES 20111012          Moved to context-local instead of singleton. This
**                           allows multiple client sessions to be hosted in
**                           the server.
** 005 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 006 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.util.logging.*;
import org.w3c.dom.*;

import java.util.*;
import java.net.URL;
import java.io.*;

import com.goldencode.p2j.directory.Base64;
import com.goldencode.util.XmlHelper;
import com.goldencode.p2j.security.*;

/**
 * BufferSizeManager indended to solve the following problem: after a process
 * stream fails, Progress can output some amount of data to the broken stream
 * without raising any error. But after some amount of data has been written
 * to this stream, data buffer is overfilled, flush is performed and STOP
 * condition may be raised. This class helps us to get the proper size of data
 * buffer in order to raise the STOP condition at the same point as Progress
 * does.
 * <p>
 * BufferSizeManager acts a singleton and tracks stream opening and closing,
 * blocking operations and output operations. It can tell whether it is time
 * to raise the STOP condition for a given stream. It makes decision basing on
 * the array of "reference" buffer sizes, which were generated from the
 * results of testcases. These testcases allowed us to get buffer sizes for
 * the different conditions. To know what buffer size may depend on, read
 * {@link BufferSizeDependency}.
 */
public class BufferSizeManager
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(BufferSizeManager.class);
   
   /** Context local instance of this class. */
   private static final ContextLocal<BufferSizeManager> local =
      new ContextLocal<BufferSizeManager>();

   /** Map which stores information about opened streams. */
   private final Map<ProcessStream, StreamInfo> streams =
                              new HashMap<ProcessStream, StreamInfo>();
   /**
    * Determines whether a stream (any process stream) was ever broken
    * during the session.
    */
   private boolean aStreamWasBroken = false;

   /**
    * Multidimensional array which stores "reference" buffer sizes for
    * flush operations.
    */
   private Object flushArray = null;

   /**
    * Multidimensional array which stores "reference" buffer sizes for
    * close operations.
    */
   private Object closeArray = null;

   /**
    * "type of dependency -&gt; index of the corresponding dimension" for the
    * flush operations array. E.g. UNBUFFERED -&gt; 3 will mean that the 3rd
    * dimension of the <code>flushArray</code> determines whether the stream
    * is buffered or unbuffered one.
    */
   private Map<BufferSizeDependency, Integer> flushParamIndexes =
                                 new HashMap<BufferSizeDependency, Integer>();

   /**
    * "type of dependency -&gt; index of the corresponding dimension" for the
    * close operations array. E.g. UNBUFFERED -&gt; 3 will mean that the 3rd
    * dimension of the <code>closeArray</code> determines whether the stream
    * is buffered or unbuffered one.
    */
   private Map<BufferSizeDependency, Integer> closeParamIndexes =
                                 new HashMap<BufferSizeDependency, Integer>();

   /**
    * "type of dependency -&gt; mapping 'dependency value -&gt; index inside the
    * dimension'" for the flush operations array. E.g. UNBUFFERED -&gt;
    * [true -&gt; 0, false -&gt; 1] will mean that if we set the index inside the
    * dimension which corresponds the "UNBUFFERED" dependency to 0 - we will
    * get buffer size for an unbuffered stream, and if we will set it to 1 -
    * we will get data for a buffered stream.
    */
   private Map<BufferSizeDependency, Map<Object, Integer>> flushValuesIndexes
                  = new HashMap<BufferSizeDependency, Map<Object, Integer>>();

   /**
    * "type of dependency -&gt; mapping 'dependency value -&gt; index inside the
    * dimension'" for the close operations array. E.g. UNBUFFERED -&gt;
    * [true -&gt; 0, false -&gt; 1] will mean that if we set the index inside the
    * dimension which corresponds the "UNBUFFERED" dependency to 0 - we will
    * get buffer size for an unbuffered stream, and if we will set it to 1 -
    * we will get data for a buffered stream.
    */
   private Map<BufferSizeDependency, Map<Object, Integer>> closeValuesIndexes
                  = new HashMap<BufferSizeDependency, Map<Object, Integer>>();

   /** Determines whether "reference" buffer sizes were successfully loaded */
   private boolean initialized = false;

   /**
    * Private c'tor which performs BufferSizeManager initialization.
    */
   private BufferSizeManager()
   {
      try
      {
         URL url = ClassLoader.getSystemResource(
                                  "com/goldencode/p2j/util/buffer_sizes.xml");
         if (url == null)
            throw new Exception("Cannot find buffer_sizes.xml!");

         Document dom = XmlHelper.parse(url.openStream());
         Element root = dom.getDocumentElement();

         flushArray = readReferenceArray(root,
                                        "flush_array_parameters",
                                        flushParamIndexes,
                                        flushValuesIndexes,
                                        "flush_array_data");
         closeArray = readReferenceArray(root,
                                        "close_array_parameters",
                                        closeParamIndexes,
                                        closeValuesIndexes,
                                        "close_array_data");
         initialized = true;
      }
      
      catch (Exception e)
      {
         LOG.severe("Exception in BufferSizeManager constructor", e);
      }
   }

   /**
    * Returns singleton instance of this class.
    *
    * @return See above.
    */
   public static synchronized BufferSizeManager getInstance()
   {
      BufferSizeManager instance = local.get();
      
      if (instance == null)
      {
         instance = new BufferSizeManager();
         local.set(instance);
      }
      
      return instance;
   }

   /**
    * Notify that a process stream has been broken.
    *
    * @param stream
    *        The stream that was broken.
    */
   public void notifyBroken(ProcessStream stream)
   {
      aStreamWasBroken = true;
   }

   /**
    * Notify that an output error has occured in the process stream.
    *
    * @param stream
    *        The stream that has failed to perform output.
    */
   public void notifyOutputError(ProcessStream stream)
   {
      streams.put(stream, new StreamInfo(stream));
   }

   /**
    * Notify that a blocking operation has been performed.
    *
    * @param blockingOperation
    *        Blocking operation that was performed.
    */
   public void notifyBlockingOperation(BlockingOperation blockingOperation)
   {
      for (StreamInfo streamInfo : streams.values())
      {
         streamInfo.notifyBlockingOperation(blockingOperation);
      }
   }

   /**
    * Notify that a stream has been closed.
    *
    * @param stream
    *        The stream that was closed.
    */
   public void notifyCloseStream(ProcessStream stream)
   {
      streams.remove(stream);
   }

   /**
    * Notify that an output operation has been performed.
    *
    * @param put
    *        <code>true</code> if a PUT operation was performed,
    *        <code>false</code> if a DISPLAY operation was performed
    * @param stream
    *        Target stream of the output operation.
    * @param bytesWritten
    *        Number of bytes that has been written by this operation.
    */
   public void notifyOutputOperation(boolean put,
                                     Stream stream,
                                     int bytesWritten)
   {
      if (stream instanceof ProcessStream)
      {
         StreamInfo info = streams.get((ProcessStream) stream);
         if (info != null)
         {
            OutputOperationType type =
                                 getOutputDataEnumByNumber(put, bytesWritten);
            info.notifyOutputOperation(type, bytesWritten);
         }
      }
   }

   /**
    * Reset buffer size manager to its initial state (this does not reset
    * its configuration).
    */
   public void reset()
   {
      streams.clear();
      aStreamWasBroken = false;
   }

   /**
    * Determines whether we have exceeded the buffer size limit for a given
    * stream and the STOP condition should be raised.
    *
    * @param  close
    *         <code>true</code> if we are performing a close operation,
    *         <code>false</code> if we are performing a flush operation.
    * @param  stream
    *         The stream we are questioning for.
    *
    * @return <code>true</code> if we have exceeded the buffer size limit.
    */
   public boolean shouldRaiseStop(boolean close, ProcessStream stream)
   {
      StreamInfo info = streams.get(stream);

      if (info == null)
      {
         throw new IllegalStateException(
                        "Notification about stream output error is missing!");
      }

      return info.shouldRaiseStop(close);
   }

   /**
    * Get the buffer size for the given stream and output operation.
    *
    * @param  close
    *         <code>true</code> if we are performing a close operation,
    *         <code>false</code> if we are performing a flush operation.
    * @param  info
    *         Stream info object for the stream which buffer size should be
    *         returned.
    * @param  outputOperationType
    *         Output operation for which the buffer size should be calculated.
    *
    * @return See above.
    */
   private int getBufferSize(boolean close,
                             StreamInfo info,
                             OutputOperationType outputOperationType)
   {
      Map<BufferSizeDependency, Object> map =
                                  new HashMap<BufferSizeDependency, Object>();
      map.put(BufferSizeDependency.BLOCKING_OPERATION,
              info.getBlockingOperations());
      map.put(BufferSizeDependency.BROKEN_STREAM, aStreamWasBroken);
      map.put(BufferSizeDependency.INOUT, info.stream.isIn());
      map.put(BufferSizeDependency.OUTPUT_DATA, outputOperationType);
      map.put(BufferSizeDependency.OUTPUT_DESTINATION, null);
      map.put(BufferSizeDependency.UNBUFFERED, info.stream.isUnbuffered());
      map.put(BufferSizeDependency.UNNAMED, info.stream.isUnnamed());

      if (close)
      {
         map.put(BufferSizeDependency.CLOSE_OPERATION, null);
      }

      return getBufferSize(close, map);
   }

   /**
    * Get buffer size for the given stream state.
    *
    * @param  close
    *         <code>true</code> if we are performing a close operation,
    *         <code>false</code> if we are performing a flush operation.
    * @param  parameters
    *         Parameters which specify the state of the stream.
    *
    * @return See above.
    */
   private int getBufferSize(boolean close,
                             Map<BufferSizeDependency, Object> parameters)
   {
      int defaultBufferSize = getDefaultBufferSize(close, parameters);
      return getBufferSize(parameters,
                           close ? closeArray : flushArray,
                           close ? closeParamIndexes : flushParamIndexes,
                           close ? closeValuesIndexes : flushValuesIndexes,
                           defaultBufferSize);
   }

   /**
    * Get the buffer size for the given stream state from the given array of
    * "reference" buffer sizes.
    *
    * @param  parameters
    *         Parameters which specify the state of the stream.
    * @param  dataArray
    *         Array which contains the "reference" buffer sizes.
    * @param  paramMap
    *         Target map which will store the information about array
    *         dimensions. See {@link #flushParamIndexes}.
    * @param  valuesMap
    *         Target map which will store the information about indexes
    *         inside dimensions. See {@link #flushValuesIndexes}.
    * @param  defaultBufferSize
    *         Buffer size value to be used if we haven't found an appropriate
    *         value in the array of "reference" buffer sizes.    
    *
    * @return See above.
    */
   private int getBufferSize(
                    Map<BufferSizeDependency, Object> parameters,
                    Object dataArray,
                    Map<BufferSizeDependency, Integer> paramMap,
                    Map<BufferSizeDependency, Map<Object, Integer>> valuesMap,
                    int defaultBufferSize)
   {
      int size = paramMap.size();

      if (size != parameters.size())
         throw new
             IllegalArgumentException("Invalid number of parameters in map!");

      if (!initialized)
      {
         return defaultBufferSize;
      }

      int[] indexes = new int[size];

      Set<BlockingOperation> blockingOberations =
            (Set<BlockingOperation>)
                      parameters.get(BufferSizeDependency.BLOCKING_OPERATION);

      if (blockingOberations == null)
      {
         blockingOberations = new HashSet<BlockingOperation>();
      }

      if (blockingOberations.isEmpty())
      {
         blockingOberations.add(BlockingOperation.NO_BLOCKING_OPERATION);
      }

      Integer min = null;
      for (BlockingOperation blockingOperation : blockingOberations)
      {
         BufferSizeDependency dependency =
                             BufferSizeDependency.BLOCKING_OPERATION;
         indexes[paramMap.get(dependency)] =
                             valuesMap.get(dependency).get(blockingOperation);

         for (Map.Entry<BufferSizeDependency, Object> entry :
                                                        parameters.entrySet())
         {
            dependency = entry.getKey();
            if (dependency.equals(BufferSizeDependency.BLOCKING_OPERATION))
               continue;
            Object val = entry.getValue();

            indexes[paramMap.get(dependency)] =
                             valuesMap.get(dependency).get(val);
         }

         Integer res = (Integer) getElement(dataArray, indexes);

         if (res != null && (min == null || min > res))
         {
            min = res;
         }
      }

      if (min == null)
         min = defaultBufferSize;

      return min;
   }

   /**
    * Get the default buffer size for the given set of stream parameters.
    *
    * @param  close
    *         <code>true</code> if the buffer size for a close operation is
    *         requested, <code>false</code> if the buffer size for a flush
    *         operation is requested.
    * @param  parameters
    *         Parameters which describe the current state of the stream.
    *
    * @return See above.
    */
   private int getDefaultBufferSize(
                                 boolean close,
                                 Map<BufferSizeDependency, Object> parameters)
   {
      if (close)
         return (Boolean) parameters.get(BufferSizeDependency.BROKEN_STREAM) ?
                                         512 :
                                         512;
      else
         return (Boolean) parameters.get(BufferSizeDependency.BROKEN_STREAM) ?
                                         512 :
                                         5000;
   }

   /**
    * Read the array of "reference" buffer size from the given XML tree.
    *
    * @param  root
    *         The root node of the source XML tree.
    * @param  paramNode
    *         The name of the node which contains the array metadata.
    * @param  paramMap
    *         Target map which will store the information about array
    *         dimensions. See {@link #flushParamIndexes}.
    * @param  valuesMap
    *         Target map which will store the information about indexes
    *         inside dimensions. See {@link #flushValuesIndexes}.
    * @param  dataNode
    *         The name of the node which contains the array data.
    *
    * @return The array of "reference" buffer sizes.
    *
    * @throws IOException
    *         If we couldn't read the required array.
    * @throws ClassNotFoundException
    *         If we couldn't read the required array.
    */
   private Object readReferenceArray(
                    Element root,
                    String paramNode,
                    Map<BufferSizeDependency, Integer> paramMap,
                    Map<BufferSizeDependency, Map<Object, Integer>> valuesMap,
                    String dataNode)
   throws IOException,
          ClassNotFoundException
   {
      StringBuilder sb = new StringBuilder();
      sb.append(paramNode);
      sb.append("/parameters");
      String path = sb.toString();

      String[] dimensions = getNodeStrings(root, path);

      for (int i = 0; i < dimensions.length; i++)
      {
         String dimension = dimensions[i];

         BufferSizeDependency dependency =
                                      BufferSizeDependency.valueOf(dimension);
         paramMap.put(dependency, i);

         sb = new StringBuilder();
         sb.append(paramNode);
         sb.append("/values");
         sb.append("/");
         sb.append(dimension);
         path = sb.toString();

         String[] values = getNodeStrings(root, path);
         Map<Object, Integer> map = new HashMap<Object, Integer>();

         for (int j = 0; j < values.length; j++)
         {
            String value = values[j];

            switch (dependency)
            {
               case OUTPUT_DESTINATION:
               case CLOSE_OPERATION:
                  map.put(null, j);
                  break;

               case BROKEN_STREAM:
               case UNBUFFERED:
               case UNNAMED:
               case INOUT:
                  map.put(Boolean.valueOf(value), j);
                  break;

               case BLOCKING_OPERATION:
                  map.put(BlockingOperation.valueOf(value), j);
                  break;

               case OUTPUT_DATA:
                  map.put(OutputOperationType.valueOf(value), j);
                  break;
            }
         }

         valuesMap.put(dependency, map);
      }

      byte[] data = getNodeByteArray(root, dataNode);

      ByteArrayInputStream bais = new ByteArrayInputStream(data);
      ObjectInputStream ois = new ObjectInputStream(bais);
      Object dataArray = ois.readObject();
      ois.close();
      bais.close();

      return dataArray;
   }

   /**
    * Get XML node by the given path.
    *
    * @param  root
    *         The root node of the source XML.
    * @param  path
    *         Path to the required node (names of the nodes starting from the
    *         root node (exclusive), separated by "/", e.g.
    *         "parameters/param1/description").
    *
    * @return The required node.
    */
   private Element getNodeByPath(Element root, String path)
   {
      String[] elements = path.split("/");
      Element parentNode = root;

      elementCycle:
      for (String element : elements)
      {
         NodeList list = parentNode.getElementsByTagName("node");
         int len = list.getLength();
         for(int i = 0; i < len; i++)
         {
            Element node = (Element) list.item(i);
            if (element.equals(node.getAttribute("name")))
            {
               parentNode = node;
               continue elementCycle;
            }
         }

         return null;
      }

      return parentNode;
   }

   /**
    * Get the string values which are stored under the given node.
    *
    * @param  root
    *         The root node of the source XML.
    * @param  path
    *         Path to the required node (names of the nodes starting from the
    *         root node (exclusive), separated by "/", e.g.
    *         "parameters/names").
    *
    * @return See above.
    */
   private String[] getNodeStrings(Element root, String path)
   {
      Element parentNode = getNodeByPath(root, path);
      if (parentNode == null)
         throw new IllegalArgumentException("Cannot find node " + path + "!");

      NodeList list = parentNode.getElementsByTagName("node-attribute");
      int len = list.getLength();
      String[] res = new String[len];
      for(int i = 0; i < len; i++)
      {
         Element node = (Element) list.item(i);
         res[i] = node.getAttribute("value");
      }

      return res;
   }

   /**
    * Get the byte array which is stored under the given node.
    *
    * @param  root
    *         The root node of the source XML.
    * @param  path
    *         Path to the required node (names of the nodes starting from the
    *         root node (exclusive), separated by "/", e.g.
    *         "parameters/data").
    *
    * @return See above.
    */
   private byte[] getNodeByteArray(Element root, String path)
   {
      Element node = getNodeByPath(root, path);
      if (node == null)
         throw new IllegalArgumentException("Cannot find node " + path + "!");

      node = (Element)node.getElementsByTagName("node-attribute").item(0);
      String data = node.getAttribute("value");
      return Base64.base64ToByteArray(data);
   }

   /**
    * Return the element of the given multidimensional array which position
    * position is specified by the given indexes.
    *
    * @param  array
    *         Array from which the required element will be retrieved. 
    * @param  indexes
    *         Array of indexes which specify the position of the required
    *         element in the array.
    *
    * @return See above.
    */
   private Object getElement(Object array, int[] indexes)
   {
      Object[] arr = (Object[]) array;

      int maxIndex = indexes.length - 1;
      for (int i = 0; i < maxIndex; i++)
      {
         arr = (Object[]) arr[indexes[i]];
      }

      return arr[indexes[maxIndex]];
   }

   /**
    * Get the OutputOperationType enumeration value for the given operation
    * type and number of bytes written by this operation. See
    * {@link OutputOperationType}
    *
    * @param  put
    *         <code>true</code> if a PUT operation was performed,
    *         <code>false</code> if a DISPLAY operation was performed
    *
    * @param  bytesWritten
    *         Number of bytes that has been written by this operation.
    * 
    * @return See above.
    */
   private OutputOperationType getOutputDataEnumByNumber(boolean put,
                                                         int bytesWritten)
   {
      if (put)
      {
         if (bytesWritten < 15)
            return OutputOperationType.PUT_10;
         else if (bytesWritten < 50)
            return OutputOperationType.PUT_20;
         else if (bytesWritten < 65)
            return OutputOperationType.PUT_60;
         else if (bytesWritten < 85)
            return OutputOperationType.PUT_80;
         else if (bytesWritten < 95)
            return OutputOperationType.PUT_90;
         else if (bytesWritten < 150)
            return OutputOperationType.PUT_100;
         else
            return OutputOperationType.PUT_200;
      }
      else
      {
         if (bytesWritten < 15)
            return OutputOperationType.DISPLAY_10;
         else if (bytesWritten < 25)
            return OutputOperationType.DISPLAY_20;
         else if (bytesWritten < 35)
            return OutputOperationType.DISPLAY_30;
         else if (bytesWritten < 45)
            return OutputOperationType.DISPLAY_40;
         else if (bytesWritten < 200)
            return OutputOperationType.DISPLAY_80;
         else if (bytesWritten < 425)
            return OutputOperationType.DISPLAY_250;
         else
            return OutputOperationType.DISPLAY_450;
      }
   }

   /**
    * Stores information about the current state of the output stream and acts
    * as a helper for determining whether we have exceeded the buffer size.
    */
   private class StreamInfo
   {
      /** The stream to which this information relates. */
      private ProcessStream stream;

      /**
       * The set blocking operations that has been called while the stream
       * was opened.
       */
      private Set<BlockingOperation> blockingOperations = null;

      /**
       * Buffer sizes (which correspond the current stream state) for a flush
       * action for different output operation types. 
       */
      private Map<OutputOperationType, Integer> flushLimits = null;

      /**
       * Buffer sizes (which correspond the current stream state) for a close
       * action for different output operation types.
       */
      private Map<OutputOperationType, Integer> closeLimits = null;

      /**
       * Maximum buffer size among the values contained into
       * <code>flushLimits</code> map.
       */
      private int maxFlushLimit;

      /**
       * Maximum buffer size among the values contained into
       * <code>closeLimits</code> map.
       */
      private int maxCloseLimit;

      /**
       * The list of output operations that has been performed using the
       * corresponding stream.
       */
      private List<OutputOperation> outputOperations = null;

      /**
       * Indicates whether the contained information should be refreshed
       * because of changes into the stream state.
       */
      private boolean pendingRefresh = true;

      /**
       * Cached sum of pseudo-bytes that have been written. See
       * {@link #calculatePseudoBytesWritten(java.util.Map, int)}
       */
      private int tempPseudoBytes = 0;

      /**
       * Maximum element (inclusive index) of <code>outputOperations</code>
       * array that has already been added to <code>tempPseudoBytes</code>.   
       */
      private int maxCalcutatedIndex = -1;

      /**
       * Default c'tor.
       *
       * @param stream
       *        The stream to which this information relates.
       */
      public StreamInfo(ProcessStream stream)
      {
         this.stream = stream;
      }

      /**
       * Notify that a blocking operation has been performed while this stream
       * was open.
       *
       * @param blockingOperation
       *        Blocking operation that was performed.
       */
      public void notifyBlockingOperation(BlockingOperation blockingOperation)
      {
         if (blockingOperations == null)
            blockingOperations = new HashSet<BlockingOperation>();

         if (blockingOperations.add(blockingOperation))
         {
            pendingRefresh = true;
         }
      }

      /**
       * Notify that an output operation has been performed using this stream.
       *
       * @param type
       *        Type of the performed output operation.
       * @param bytes
       *        Number of bytes that has been written by the output operation.  
       */
      public void notifyOutputOperation(OutputOperationType type, int bytes)
      {
         if (outputOperations == null)
            outputOperations = new ArrayList<OutputOperation>();

         outputOperations.add(new OutputOperation(type, bytes));
      }

      /**
       * Get the set of blocking operation that has been performed while the
       * stream was open.
       *
       * @return See above.
       */
      public Set<BlockingOperation> getBlockingOperations()
      {
         return blockingOperations;
      }

      /**
       * Determine whether the stream has exceeded the buffer size and the
       * STOP condition should be raised.
       *
       * @param  close
       *         <code>true</code> if we are performing a close operation,
       *         <code>false</code> if we are performing a flush operation.
       * @return See above.
       */
      public boolean shouldRaiseStop(boolean close)
      {
         if (pendingRefresh)
            refreshInformation();

         int limit = close ? maxCloseLimit : maxFlushLimit;
         int bytes = calculatePseudoBytesWritten(close ? closeLimits :
                                                         flushLimits,
                                                 limit);
         return bytes > limit;
      }

      /**
       * Calculates the number of pseudo-bytes written through this stream.
       * <p>
       * Writing different number of bytes in a single operation may lead to
       * different buffer sizes. E.g. if we are using only PUT "xxxx"
       * operations we can output 40 bytes in this way, but if we are using
       * only PUT "xxxxxxxxxx" operations we can output only 20 bytes.
       * <p>
       * Consider we have the following sequence of operations:<br>
       * <code>PUT "xxxx"<br>
       *    PUT "xxxx"<br>
       *    PUT "xxxx"<br>
       *    PUT "xxxx"<br>
       *    PUT "xxxx"<br>
       *    PUT "xxxxxxxxxx"<br></code>
       * We have 5 * 4 + 1 * 10 = 30 "real" bytes written.<br>
       * But we have 5 * 4 * (40 / 40) + 1 * 10 * (40 / 20) = 40 pseudo-bytes
       * written which equals the buffer size limit for this case. 
       *
       * @param  operationsMap
       *         Map which contains current buffer sizes for different output
       *         operation types. 
       * @param  maxValue
       *         The maximum buffer size among the values contained in the
       *         <code>operationsMap</code> 
       *
       * @return See above.
       */
      private int calculatePseudoBytesWritten(
                              Map<OutputOperationType, Integer> operationsMap,
                              int maxValue)
      {
         int res = tempPseudoBytes;

         if (outputOperations != null)
         {
            int len = outputOperations.size();
            for (int i = maxCalcutatedIndex + 1 ; i < len; i++)
            {
               OutputOperation operation = outputOperations.get(i);
               int divisor = operationsMap.get(operation.outputOperationType);
               if (divisor == 0)
                  divisor = 1;

               res += operation.bytesWritten * maxValue / divisor;
            }
         }

         tempPseudoBytes = res;
         return res;
      }

      /**
       * Refresh the contained information according to the current stream
       * state.
       */
      private void refreshInformation()
      {
         if (flushLimits == null)
            flushLimits = new HashMap<OutputOperationType, Integer>();

         if (closeLimits == null)
            closeLimits = new HashMap<OutputOperationType, Integer>();

         int maxFlush = -1;
         int maxClose = -1;
         for (OutputOperationType output : OutputOperationType.values())
         {
            int val = getBufferSize(false, this, output);
            flushLimits.put(output, val);

            if (val > maxFlush)
               maxFlush = val;

            val = getBufferSize(true, this, output);
            closeLimits.put(output, val);

            if (val > maxClose)
               maxClose = val;
         }

         maxFlushLimit = maxFlush;
         maxCloseLimit = maxClose;

         tempPseudoBytes = 0;
         maxCalcutatedIndex = -1;
      }

      /**
       * Container which describes a PUT/DISPLAY operation.
       */
      private class OutputOperation
      {
         /** Type of the output operation. */
         public OutputOperationType outputOperationType;

         /** Number of bytes that has been written by this operation. */
         public int bytesWritten;

         /**
          * Default c'tor.
          *
          * @param outputOperationType
          *        Type of the output operation.
          * @param bytesWritten
          *        Number of bytes that has been written by this operation.  
          */
         public OutputOperation(OutputOperationType outputOperationType,
                                int bytesWritten)
         {
            this.outputOperationType = outputOperationType;
            this.bytesWritten = bytesWritten;
         }
      }
   }
}