001/*
002 * Copyright 2018-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2018-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2018-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util;
037
038
039
040import java.io.IOException;
041import java.io.OutputStream;
042
043
044
045/**
046 * This class provides an {@code OutputStream} implementation that uses a
047 * {@link FixedRateBarrier} to impose an upper bound on the rate (in bytes per
048 * second) at which data can be written to a wrapped {@code OutputStream}.
049 */
050@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
051public final class RateLimitedOutputStream
052       extends OutputStream
053{
054  // Indicates whether to automatically flush the stream after each write.
055  private final boolean autoFlush;
056
057  // The fixed-rate barrier that will serve as a rate limiter for this class.
058  @NotNull private final FixedRateBarrier rateLimiter;
059
060  // The output stream to which the data will actually be written.
061  @NotNull private final OutputStream wrappedStream;
062
063  // The maximum number of bytes that can be written in any single call to the
064  // rate limiter.
065  private final int maxBytesPerWrite;
066
067
068
069  /**
070   * Creates a new instance of this rate-limited output stream that wraps the
071   * provided output stream.
072   *
073   * @param  wrappedStream      The output stream to which the data will
074   *                            actually be written.  It must not be
075   *                            {@code null}.
076   * @param  maxBytesPerSecond  The maximum number of bytes per second that can
077   *                            be written using this output stream.  It must be
078   *                            greater than zero.
079   * @param  autoFlush          Indicates whether to automatically flush the
080   *                            wrapped output stream after each write.
081   */
082  public RateLimitedOutputStream(@NotNull final OutputStream wrappedStream,
083                                 final int maxBytesPerSecond,
084                                 final boolean autoFlush)
085  {
086    Validator.ensureTrue((wrappedStream != null),
087         "RateLimitedOutputStream.wrappedStream must not be null.");
088    Validator.ensureTrue((maxBytesPerSecond > 0),
089         "RateLimitedOutputStream.maxBytesPerSecond must be greater than " +
090              "zero.  The provided value was " + maxBytesPerSecond);
091
092    this.wrappedStream = wrappedStream;
093    this.autoFlush = autoFlush;
094
095    rateLimiter = new FixedRateBarrier(1000L, maxBytesPerSecond);
096    maxBytesPerWrite = Math.max(1, (maxBytesPerSecond / 100));
097  }
098
099
100
101  /**
102   * Closes this output stream and the wrapped stream.
103   *
104   * @throws  IOException  If a problem is encountered while closing the wrapped
105   *                       output stream.
106   */
107  @Override()
108  public void close()
109         throws IOException
110  {
111    wrappedStream.close();
112  }
113
114
115
116  /**
117   * Writes a single byte of data to the wrapped output stream.
118   *
119   * @param  b  The byte of data to be written.  Only the least significant
120   *            eight bits will be written.
121   *
122   * @throws  IOException  If a problem is encountered while writing to the
123   *                       wrapped stream.
124   */
125  @Override()
126  public void write(final int b)
127         throws IOException
128  {
129    rateLimiter.await();
130    wrappedStream.write(b);
131
132    if (autoFlush)
133    {
134      wrappedStream.flush();
135    }
136  }
137
138
139
140  /**
141   * Writes the contents of the provided array to the wrapped output stream.
142   *
143   * @param  b  The byte array containing the data to be written.  It must not
144   *            be {@code null}.
145   *
146   * @throws  IOException  If a problem is encountered while writing to the
147   *                       wrapped stream.
148   */
149  @Override()
150  public void write(@NotNull final byte[] b)
151         throws IOException
152  {
153    write(b, 0, b.length);
154  }
155
156
157
158  /**
159   * Writes the contents of the specified portion of the provided array to the
160   * wrapped output stream.
161   *
162   * @param  b       The byte array containing the data to be written.  It must
163   *                 not be {@code null}.
164   * @param  offset  The position in the provided array at which the data to
165   *                 write begins.  It must be greater than or equal to zero and
166   *                 less than the length of the provided array.
167   * @param  length  The number of bytes to be written.  It must not be
168   *                 negative, and the sum of offset and length must be less
169   *                 than or equal to the length of the provided array.
170   *
171   * @throws  IOException  If a problem is encountered while writing to the
172   *                       wrapped stream.
173   */
174  @Override()
175  public void write(@NotNull final byte[] b, final int offset, final int length)
176         throws IOException
177  {
178    if (length <= 0)
179    {
180      return;
181    }
182
183    if (length <= maxBytesPerWrite)
184    {
185      rateLimiter.await(length);
186      wrappedStream.write(b, offset, length);
187    }
188    else
189    {
190      int pos = offset;
191      int remainingToWrite = length;
192      while (remainingToWrite > 0)
193      {
194        final int lengthThisWrite =
195             Math.min(remainingToWrite, maxBytesPerWrite);
196        rateLimiter.await(lengthThisWrite);
197        wrappedStream.write(b, pos, lengthThisWrite);
198        pos += lengthThisWrite;
199        remainingToWrite -= lengthThisWrite;
200      }
201    }
202
203    if (autoFlush)
204    {
205      wrappedStream.flush();
206    }
207  }
208
209
210
211  /**
212   * Flushes the contents of the wrapped stream.
213   *
214   * @throws  IOException  If a problem is encountered while flushing the
215   *                       wrapped stream.
216   */
217  @Override()
218  public void flush()
219         throws IOException
220  {
221    wrappedStream.flush();
222  }
223}