/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io;
import java.io.IOException;
import java.io.InputStream;
/**
* ThroughputLimitInputStream extends InputStream to provide control over the transfer speed and limit it to a specified
* number of bytes per second.
* Whenever the bytes per second quota has been reached, the read and skip methods will lock and won't return
* until either:
* <ul>a new second commences, bringing the bytes read count back to zero for the new second
* <li>{@link #setThroughputLimit(long)} is called with a more permissive bytes per second value (different from 0),
* yielding to more bytes available for the current second.
*
* <p>Setting the throughput limit to 0 effectively blocks all read and skip calls indefinitely.
* Any calls to the read or skip methods will lock, the only way to remove this lock being to call the
* {@link #setThroughputLimit(long)} method with a value different from 0 from another thread.
*
* <p>Setting the throughput limit to -1 or any other negative values will disable any limit and make
* this ThroughputLimitInputStream behave just like a normal InputStream.
*
* <p>Finally, the {@link #setUnderlyingInputStream(java.io.InputStream)} method allows to use the
* same ThroughputLimitInputStream instance for multiple InputStream instances, keeping the bytes count for the
* current second intact and thus the throughput limit stable. This does not hold true if a new ThroughputLimitInputStream
* is created for each InputStream, the bytes count for the current second starting at 0.
*
* @author Maxence Bernard
*/
public class ThroughputLimitInputStream extends InputStream {
/** Underlying InputStream */
private InputStream in;
/** Throughput limit in bytes per second, -1 for no limit, 0 to completely block reads */
private long bpsLimit;
/** Holds the current second, allowing to detect when a new second commences */
private long currentSecond;
/** Number of bytes that have been read or skipped this second */
private long nbBytesReadThisSecond;
/**
* Creates a new ThroughputLimitInputStream with no initial throughput limit (-1 value).
*
* @param in underlying stream that is used to read data from
*/
public ThroughputLimitInputStream(InputStream in) {
this.in = in;
this.bpsLimit = -1;
}
/**
* Creates a new ThroughputLimitInputStream with an initial throughput limit.
*
* @param in underlying stream that is used to read data from
* @param bytesPerSecond initial throughput limit in bytes per second
* @see #setThroughputLimit(long)
*/
public ThroughputLimitInputStream(InputStream in, long bytesPerSecond) {
this.in = in;
this.bpsLimit = bytesPerSecond;
}
/**
* Specifies a new throughput limit expressed in bytes per second.
* The new limit will take effect the next time one of the read or skip methods are called.
*
* <p>Setting the throughput limit to 0 effectively blocks all read and skip calls indefinitely.
* Any calls to the read or skip methods will lock, the only way to remove this lock being to call the
* {@link #setThroughputLimit(long)} method with a value different from 0 from another thread.
*
* <p>Setting the throughput limit to -1 or any other negative values will disable any limit and make
* this ThroughputLimitInputStream behave just like a normal InputStream.
*
* @param bytesPerSecond new throughput limit expressed in bytes, -1 to disable it, 0 to block reads.
*/
public void setThroughputLimit(long bytesPerSecond) {
this.bpsLimit = bytesPerSecond;
// Wake up any thread waiting for data to be available to have them check the new limit counter
synchronized(this) {
notify();
}
}
/**
* Changes the underlying InputStream which data is read from, keeping the bytes count for the current second intact.
*
* <p>Note: the existing underlying InputStream will not be closed, the {@link #close()} method must be called prior
* to calling this method.
*
* @param in the new InputStream to read data from
*/
public void setUnderlyingInputStream(InputStream in) {
this.in = in;
}
/**
* Returns the number of bytes that can be read (or skipped) without exceeding the current throughput limit.
* This method blocks until at least 1 byte is available. In other words the method always returns
* strictly positive values.
*
* <p>If the current throughput limit is negative (no limit), this method returns immediately Integer.MAX_VALUE.
* <p>If the byte quota for the current second has been exceeded, this method locks and returns as soon as a new second
* has started (i.e. bytes are available), or the {@link #setThroughputLimit(long)} with a more permissive value
* has been called.
* <p>If the current throughput limit is 0, it will lock undefinitely, until {@link #setThroughputLimit(long)} has
* been called from another thread with a value different from 0.
*
* @return the number of bytes available for reading without exceeding the current throughput limit
*/
private int getNbAllowedBytes() {
// Update limit counter and retrieve number of milliseconds until next second
long msUntilNextSecond = updateLimitCounter();
long allowedBytes;
synchronized(this) {
// Loop while throughput limit has been exceeded
while((allowedBytes=bpsLimit- nbBytesReadThisSecond)<=0) {
// Throughput limit was removed, return max int value
if(bpsLimit<0)
return Integer.MAX_VALUE;
try {
// If limit is 0, wait indefinitely for a call to notify() from setThroughputLimit()
if(bpsLimit==0)
wait();
// Wait until the current second is over for more bytes to be available,
// or until a call to notify() is made from setThroughputLimit()
else {
wait(msUntilNextSecond);
}
}
catch(InterruptedException e) {
// No problem in this unlikely event, loop one more time and wait some more
}
// Update limit counter and retrieve number of milliseconds until next second
msUntilNextSecond = updateLimitCounter();
}
}
return (int)allowedBytes;
}
/**
* Checks if the current second has changed. If that's the case, updates the current second value and resets the
* number of bytes read this second. Returns the number of milliseconds until a new second starts.
*/
private long updateLimitCounter() {
long now = System.currentTimeMillis();
long nowSecond = now/1000;
// Current second has changed
if(this.currentSecond!=nowSecond) {
this.currentSecond = nowSecond;
this.nbBytesReadThisSecond = 0;
}
return 1000-(now%1000);
}
/**
* Increases the number of bytes read this second to the given number.
*
* @param nbRead number of bytes that have been read or skipped from the underlying stream.
*/
private void addToLimitCounter(long nbRead) {
updateLimitCounter();
this.nbBytesReadThisSecond += nbRead;
}
////////////////////////////////
// InputStream implementation //
////////////////////////////////
@Override
public int read() throws IOException {
// Wait until at least 1 byte is available if a limit is set
if(bpsLimit>=0)
getNbAllowedBytes();
// Read the byte from the underlying stream
int i = in.read();
// Increase read counter by 1
if(i>0)
addToLimitCounter(1);
return i;
}
@Override
public int read(byte[] bytes) throws IOException {
return this.read(bytes, 0, bytes.length);
}
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
int nbRead;
// Wait until at least 1 byte is available if a limit is set and try to read as many bytes are available
// without exceeding the throughput limit or the number specified
if(bpsLimit>=0)
nbRead = in.read(bytes, off, Math.min(getNbAllowedBytes(),len));
else
nbRead = in.read(bytes, off, len);
// Increase read counter by the number of bytes that have actually been read by the underlying stream
if(nbRead>0)
addToLimitCounter(nbRead);
return nbRead;
}
@Override
public long skip(long l) throws IOException {
long nbSkipped = in.skip(bpsLimit>=0?Math.min(getNbAllowedBytes(),l):l);
// Increase read counter by the number of bytes that have actually been skipped by the underlying stream
if(nbSkipped>0)
addToLimitCounter(nbSkipped);
return nbSkipped;
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public synchronized void mark(int i) {
in.mark(i);
}
@Override
public synchronized void reset() throws IOException {
in.reset();
}
@Override
public boolean markSupported() {
return in.markSupported();
}
}