# 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;
             }
          }
       }
    }