ThresholdingOutputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.io.output;

  18. import java.io.IOException;
  19. import java.io.OutputStream;

  20. import org.apache.commons.io.function.IOConsumer;
  21. import org.apache.commons.io.function.IOFunction;

  22. /**
  23.  * An output stream which triggers an event on the first write that causes
  24.  * the total number of bytes written to the stream to exceed a configured threshold,
  25.  * and every subsequent write. The event
  26.  * can be used, for example, to throw an exception if a maximum has been reached,
  27.  * or to switch the underlying stream when the threshold is exceeded.
  28.  *
  29.  * <p>
  30.  * This class overrides all {@link OutputStream} methods. However, these overrides ultimately call the corresponding
  31.  * methods in the underlying output stream implementation.
  32.  * </p>
  33.  * <p>
  34.  * NOTE: This implementation may trigger the event <em>before</em> the threshold is actually reached, since it triggers
  35.  * when a pending write operation would cause the threshold to be exceeded.
  36.  * </p>
  37.  * <p>
  38.  * See also the subclass {@link DeferredFileOutputStream}.
  39.  * </p>
  40.  *
  41.  * @see DeferredFileOutputStream
  42.  */
  43. public class ThresholdingOutputStream extends OutputStream {

  44.     /**
  45.      * Noop output stream getter function.
  46.      */
  47.     private static final IOFunction<ThresholdingOutputStream, OutputStream> NOOP_OS_GETTER = os -> NullOutputStream.INSTANCE;

  48.     /**
  49.      * The threshold at which the event will be triggered.
  50.      */
  51.     private final int threshold;

  52.     /**
  53.      * Accepts reaching the threshold.
  54.      */
  55.     private final IOConsumer<ThresholdingOutputStream> thresholdConsumer;

  56.     /**
  57.      * Gets the output stream.
  58.      */
  59.     private final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter;

  60.     /**
  61.      * The number of bytes written to the output stream.
  62.      */
  63.     private long written;

  64.     /**
  65.      * Whether or not the configured threshold has been exceeded.
  66.      */
  67.     private boolean thresholdExceeded;

  68.     /**
  69.      * Constructs an instance of this class which will trigger an event at the specified threshold.
  70.      *
  71.      * @param threshold The number of bytes at which to trigger an event.
  72.      */
  73.     public ThresholdingOutputStream(final int threshold) {
  74.         this(threshold, IOConsumer.noop(), NOOP_OS_GETTER);
  75.     }

  76.     /**
  77.      * Constructs an instance of this class which will trigger an event at the specified threshold.
  78.      * A negative threshold has no meaning and will be treated as 0
  79.      *
  80.      * @param threshold The number of bytes at which to trigger an event.
  81.      * @param thresholdConsumer Accepts reaching the threshold.
  82.      * @param outputStreamGetter Gets the output stream.
  83.      * @since 2.9.0
  84.      */
  85.     public ThresholdingOutputStream(final int threshold, final IOConsumer<ThresholdingOutputStream> thresholdConsumer,
  86.         final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter) {
  87.         this.threshold = threshold < 0 ? 0 : threshold;
  88.         this.thresholdConsumer = thresholdConsumer == null ? IOConsumer.noop() : thresholdConsumer;
  89.         this.outputStreamGetter = outputStreamGetter == null ? NOOP_OS_GETTER : outputStreamGetter;
  90.     }

  91.     /**
  92.      * Checks to see if writing the specified number of bytes would cause the configured threshold to be exceeded. If
  93.      * so, triggers an event to allow a concrete implementation to take action on this.
  94.      *
  95.      * @param count The number of bytes about to be written to the underlying output stream.
  96.      * @throws IOException if an error occurs.
  97.      */
  98.     protected void checkThreshold(final int count) throws IOException {
  99.         if (!thresholdExceeded && written + count > threshold) {
  100.             thresholdExceeded = true;
  101.             thresholdReached();
  102.         }
  103.     }

  104.     /**
  105.      * Closes this output stream and releases any system resources associated with this stream.
  106.      *
  107.      * @throws IOException if an error occurs.
  108.      */
  109.     @Override
  110.     public void close() throws IOException {
  111.         try {
  112.             flush();
  113.         } catch (final IOException ignored) {
  114.             // ignore
  115.         }
  116.         // TODO for 4.0: Replace with getOutputStream()
  117.         getStream().close();
  118.     }

  119.     /**
  120.      * Flushes this output stream and forces any buffered output bytes to be written out.
  121.      *
  122.      * @throws IOException if an error occurs.
  123.      */
  124.     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
  125.     @Override
  126.     public void flush() throws IOException {
  127.         // TODO for 4.0: Replace with getOutputStream()
  128.         getStream().flush();
  129.     }

  130.     /**
  131.      * Gets the number of bytes that have been written to this output stream.
  132.      *
  133.      * @return The number of bytes written.
  134.      */
  135.     public long getByteCount() {
  136.         return written;
  137.     }

  138.     /**
  139.      * Gets the underlying output stream, to which the corresponding {@link OutputStream} methods in this class will
  140.      * ultimately delegate.
  141.      *
  142.      * @return The underlying output stream.
  143.      * @throws IOException if an error occurs.
  144.      * @since 2.14.0
  145.      */
  146.     protected OutputStream getOutputStream() throws IOException {
  147.         return outputStreamGetter.apply(this);
  148.     }

  149.     /**
  150.      * Gets the underlying output stream, to which the corresponding {@link OutputStream} methods in this class will
  151.      * ultimately delegate.
  152.      *
  153.      * @return The underlying output stream.
  154.      * @throws IOException if an error occurs.
  155.      * @deprecated Use {@link #getOutputStream()}.
  156.      */
  157.     @Deprecated
  158.     protected OutputStream getStream() throws IOException {
  159.         return getOutputStream();
  160.     }

  161.     /**
  162.      * Gets the threshold, in bytes, at which an event will be triggered.
  163.      *
  164.      * @return The threshold point, in bytes.
  165.      */
  166.     public int getThreshold() {
  167.         return threshold;
  168.     }

  169.     /**
  170.      * Tests whether or not the configured threshold has been exceeded for this output stream.
  171.      *
  172.      * @return {@code true} if the threshold has been reached; {@code false} otherwise.
  173.      */
  174.     public boolean isThresholdExceeded() {
  175.         return written > threshold;
  176.     }

  177.     /**
  178.      * Resets the byteCount to zero. You can call this from {@link #thresholdReached()} if you want the event to be
  179.      * triggered again.
  180.      */
  181.     protected void resetByteCount() {
  182.         this.thresholdExceeded = false;
  183.         this.written = 0;
  184.     }

  185.     /**
  186.      * Sets the byteCount to count. Useful for re-opening an output stream that has previously been written to.
  187.      *
  188.      * @param count The number of bytes that have already been written to the output stream
  189.      * @since 2.5
  190.      */
  191.     protected void setByteCount(final long count) {
  192.         this.written = count;
  193.     }

  194.     /**
  195.      * Indicates that the configured threshold has been reached, and that a subclass should take whatever action
  196.      * necessary on this event. This may include changing the underlying output stream.
  197.      *
  198.      * @throws IOException if an error occurs.
  199.      */
  200.     protected void thresholdReached() throws IOException {
  201.         thresholdConsumer.accept(this);
  202.     }

  203.     /**
  204.      * Writes {@code b.length} bytes from the specified byte array to this output stream.
  205.      *
  206.      * @param b The array of bytes to be written.
  207.      * @throws IOException if an error occurs.
  208.      */
  209.     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
  210.     @Override
  211.     public void write(final byte[] b) throws IOException {
  212.         checkThreshold(b.length);
  213.         // TODO for 4.0: Replace with getOutputStream()
  214.         getStream().write(b);
  215.         written += b.length;
  216.     }

  217.     /**
  218.      * Writes {@code len} bytes from the specified byte array starting at offset {@code off} to this output stream.
  219.      *
  220.      * @param b The byte array from which the data will be written.
  221.      * @param off The start offset in the byte array.
  222.      * @param len The number of bytes to write.
  223.      * @throws IOException if an error occurs.
  224.      */
  225.     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
  226.     @Override
  227.     public void write(final byte[] b, final int off, final int len) throws IOException {
  228.         // TODO we could write the sub-array up the threshold, fire the event,
  229.         // and then write the rest so the event is always fired at the precise point.
  230.         checkThreshold(len);
  231.         // TODO for 4.0: Replace with getOutputStream()
  232.         getStream().write(b, off, len);
  233.         written += len;
  234.     }

  235.     /**
  236.      * Writes the specified byte to this output stream.
  237.      *
  238.      * @param b The byte to be written.
  239.      * @throws IOException if an error occurs.
  240.      */
  241.     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
  242.     @Override
  243.     public void write(final int b) throws IOException {
  244.         checkThreshold(1);
  245.         // TODO for 4.0: Replace with getOutputStream()
  246.         getStream().write(b);
  247.         written++;
  248.     }
  249. }