View Javadoc
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    *      https://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  
19  import java.io.IOException;
20  import java.io.OutputStream;
21  
22  import org.apache.commons.io.IOUtils;
23  import org.apache.commons.io.function.IOConsumer;
24  import org.apache.commons.io.function.IOFunction;
25  
26  /**
27   * An output stream which triggers an event on the first write that causes
28   * the total number of bytes written to the stream to exceed a configured threshold,
29   * and every subsequent write. The event
30   * can be used, for example, to throw an exception if a maximum has been reached,
31   * or to switch the underlying stream when the threshold is exceeded.
32   *
33   * <p>
34   * This class overrides all {@link OutputStream} methods. However, these overrides ultimately call the corresponding
35   * methods in the underlying output stream implementation.
36   * </p>
37   * <p>
38   * NOTE: This implementation may trigger the event <em>before</em> the threshold is actually reached, since it triggers
39   * when a pending write operation would cause the threshold to be exceeded.
40   * </p>
41   * <p>
42   * See also the subclass {@link DeferredFileOutputStream}.
43   * </p>
44   *
45   * @see DeferredFileOutputStream
46   */
47  public class ThresholdingOutputStream extends OutputStream {
48  
49      /**
50       * Noop output stream getter function.
51       */
52      private static final IOFunction<ThresholdingOutputStream, OutputStream> NOOP_OS_GETTER = os -> NullOutputStream.INSTANCE;
53  
54      /**
55       * The threshold at which the event will be triggered.
56       */
57      private final int threshold;
58  
59      /**
60       * Accepts reaching the threshold.
61       */
62      private final IOConsumer<ThresholdingOutputStream> thresholdConsumer;
63  
64      /**
65       * Gets the output stream.
66       */
67      private final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter;
68  
69      /**
70       * The number of bytes written to the output stream.
71       */
72      private long written;
73  
74      /**
75       * Whether or not the configured threshold has been exceeded.
76       */
77      private boolean thresholdExceeded;
78  
79      /**
80       * Constructs an instance of this class which will trigger an event at the specified threshold.
81       *
82       * @param threshold The number of bytes at which to trigger an event.
83       */
84      public ThresholdingOutputStream(final int threshold) {
85          this(threshold, IOConsumer.noop(), NOOP_OS_GETTER);
86      }
87  
88      /**
89       * Constructs an instance of this class which will trigger an event at the specified threshold.
90       * A negative threshold has no meaning and will be treated as 0
91       *
92       * @param threshold The number of bytes at which to trigger an event.
93       * @param thresholdConsumer Accepts reaching the threshold.
94       * @param outputStreamGetter Gets the output stream.
95       * @since 2.9.0
96       */
97      public ThresholdingOutputStream(final int threshold, final IOConsumer<ThresholdingOutputStream> thresholdConsumer,
98          final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter) {
99          this.threshold = threshold < 0 ? 0 : threshold;
100         this.thresholdConsumer = thresholdConsumer == null ? IOConsumer.noop() : thresholdConsumer;
101         this.outputStreamGetter = outputStreamGetter == null ? NOOP_OS_GETTER : outputStreamGetter;
102     }
103 
104     /**
105      * Checks to see if writing the specified number of bytes would cause the configured threshold to be exceeded. If
106      * so, triggers an event to allow a concrete implementation to take action on this.
107      *
108      * @param count The number of bytes about to be written to the underlying output stream.
109      * @throws IOException if an error occurs.
110      */
111     protected void checkThreshold(final int count) throws IOException {
112         if (!thresholdExceeded && written + count > threshold) {
113             thresholdExceeded = true;
114             thresholdReached();
115         }
116     }
117 
118     /**
119      * Closes this output stream and releases any system resources associated with this stream.
120      *
121      * @throws IOException if an error occurs.
122      */
123     @Override
124     public void close() throws IOException {
125         try {
126             flush();
127         } catch (final IOException ignored) {
128             // ignore
129         }
130         // TODO for 4.0: Replace with getOutputStream()
131         getStream().close();
132     }
133 
134     /**
135      * Flushes this output stream and forces any buffered output bytes to be written out.
136      *
137      * @throws IOException if an error occurs.
138      */
139     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
140     @Override
141     public void flush() throws IOException {
142         // TODO for 4.0: Replace with getOutputStream()
143         getStream().flush();
144     }
145 
146     /**
147      * Gets the number of bytes that have been written to this output stream.
148      *
149      * @return The number of bytes written.
150      */
151     public long getByteCount() {
152         return written;
153     }
154 
155     /**
156      * Gets the underlying output stream, to which the corresponding {@link OutputStream} methods in this class will
157      * ultimately delegate.
158      *
159      * @return The underlying output stream.
160      * @throws IOException if an error occurs.
161      * @since 2.14.0
162      */
163     protected OutputStream getOutputStream() throws IOException {
164         return outputStreamGetter.apply(this);
165     }
166 
167     /**
168      * Gets the underlying output stream, to which the corresponding {@link OutputStream} methods in this class will
169      * ultimately delegate.
170      *
171      * @return The underlying output stream.
172      * @throws IOException if an error occurs.
173      * @deprecated Use {@link #getOutputStream()}.
174      */
175     @Deprecated
176     protected OutputStream getStream() throws IOException {
177         return getOutputStream();
178     }
179 
180     /**
181      * Gets the threshold, in bytes, at which an event will be triggered.
182      *
183      * @return The threshold point, in bytes.
184      */
185     public int getThreshold() {
186         return threshold;
187     }
188 
189     /**
190      * Tests whether or not the configured threshold has been exceeded for this output stream.
191      *
192      * @return {@code true} if the threshold has been reached; {@code false} otherwise.
193      */
194     public boolean isThresholdExceeded() {
195         return written > threshold;
196     }
197 
198     /**
199      * Resets the byteCount to zero. You can call this from {@link #thresholdReached()} if you want the event to be
200      * triggered again.
201      */
202     protected void resetByteCount() {
203         this.thresholdExceeded = false;
204         this.written = 0;
205     }
206 
207     /**
208      * Sets the byteCount to count. Useful for re-opening an output stream that has previously been written to.
209      *
210      * @param count The number of bytes that have already been written to the output stream.
211      * @since 2.5
212      */
213     protected void setByteCount(final long count) {
214         this.written = count;
215     }
216 
217     /**
218      * Indicates that the configured threshold has been reached, and that a subclass should take whatever action
219      * necessary on this event. This may include changing the underlying output stream.
220      *
221      * @throws IOException if an error occurs.
222      */
223     protected void thresholdReached() throws IOException {
224         thresholdConsumer.accept(this);
225     }
226 
227     /**
228      * Writes {@code b.length} bytes from the specified byte array to this output stream.
229      *
230      * @param b The array of bytes to be written.
231      * @throws NullPointerException if the byte array is {@code null}.
232      * @throws IOException if an error occurs.
233      */
234     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
235     @Override
236     public void write(final byte[] b) throws IOException {
237         checkThreshold(b.length);
238         // TODO for 4.0: Replace with getOutputStream()
239         getStream().write(b);
240         written += b.length;
241     }
242 
243     /**
244      * Writes {@code len} bytes from the specified byte array starting at offset {@code off} to this output stream.
245      *
246      * @param b The byte array from which the data will be written.
247      * @param off The start offset in the byte array.
248      * @param len The number of bytes to write.
249      * @throws NullPointerException if the byte array is {@code null}.
250      * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}.
251      * @throws IOException if an error occurs.
252      */
253     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
254     @Override
255     public void write(final byte[] b, final int off, final int len) throws IOException {
256         IOUtils.checkFromIndexSize(b, off, len);
257         // TODO we could write the sub-array up the threshold, fire the event,
258         // and then write the rest so the event is always fired at the precise point.
259         checkThreshold(len);
260         // TODO for 4.0: Replace with getOutputStream()
261         getStream().write(b, off, len);
262         written += len;
263     }
264 
265     /**
266      * Writes the specified byte to this output stream.
267      *
268      * @param b The byte to be written.
269      * @throws IOException if an error occurs.
270      */
271     @SuppressWarnings("resource") // the underlying stream is managed by a subclass.
272     @Override
273     public void write(final int b) throws IOException {
274         checkThreshold(1);
275         // TODO for 4.0: Replace with getOutputStream()
276         getStream().write(b);
277         written++;
278     }
279 }