001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io.output;
018
019import java.io.IOException;
020import java.io.OutputStream;
021
022import org.apache.commons.io.IOUtils;
023import org.apache.commons.io.function.IOConsumer;
024import org.apache.commons.io.function.IOFunction;
025
026/**
027 * An output stream which triggers an event on the first write that causes
028 * the total number of bytes written to the stream to exceed a configured threshold,
029 * and every subsequent write. The event
030 * can be used, for example, to throw an exception if a maximum has been reached,
031 * or to switch the underlying stream when the threshold is exceeded.
032 *
033 * <p>
034 * This class overrides all {@link OutputStream} methods. However, these overrides ultimately call the corresponding
035 * methods in the underlying output stream implementation.
036 * </p>
037 * <p>
038 * NOTE: This implementation may trigger the event <em>before</em> the threshold is actually reached, since it triggers
039 * when a pending write operation would cause the threshold to be exceeded.
040 * </p>
041 * <p>
042 * See also the subclass {@link DeferredFileOutputStream}.
043 * </p>
044 *
045 * @see DeferredFileOutputStream
046 */
047public class ThresholdingOutputStream extends OutputStream {
048
049    /**
050     * Noop output stream getter function.
051     */
052    private static final IOFunction<ThresholdingOutputStream, OutputStream> NOOP_OS_GETTER = os -> NullOutputStream.INSTANCE;
053
054    /**
055     * The threshold at which the event will be triggered.
056     */
057    private final int threshold;
058
059    /**
060     * Accepts reaching the threshold.
061     */
062    private final IOConsumer<ThresholdingOutputStream> thresholdConsumer;
063
064    /**
065     * Gets the output stream.
066     */
067    private final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter;
068
069    /**
070     * The number of bytes written to the output stream.
071     */
072    private long written;
073
074    /**
075     * Whether or not the configured threshold has been exceeded.
076     */
077    private boolean thresholdExceeded;
078
079    /**
080     * Constructs an instance of this class which will trigger an event at the specified threshold.
081     *
082     * @param threshold The number of bytes at which to trigger an event.
083     */
084    public ThresholdingOutputStream(final int threshold) {
085        this(threshold, IOConsumer.noop(), NOOP_OS_GETTER);
086    }
087
088    /**
089     * Constructs an instance of this class which will trigger an event at the specified threshold.
090     * A negative threshold has no meaning and will be treated as 0
091     *
092     * @param threshold The number of bytes at which to trigger an event.
093     * @param thresholdConsumer Accepts reaching the threshold.
094     * @param outputStreamGetter Gets the output stream.
095     * @since 2.9.0
096     */
097    public ThresholdingOutputStream(final int threshold, final IOConsumer<ThresholdingOutputStream> thresholdConsumer,
098        final IOFunction<ThresholdingOutputStream, OutputStream> outputStreamGetter) {
099        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}