org.browsermob.proxy.jetty.util.TempByteHolder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of browsermob-proxy Show documentation
Show all versions of browsermob-proxy Show documentation
A programmatic HTTP/S designed for performance and functional testing
// ========================================================================
// $Id: TempByteHolder.java,v 1.8 2004/10/23 09:03:22 gregwilkins Exp $
// Copyright 2002-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
package org.browsermob.proxy.jetty.util;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* Temporary buffer for bytes to be used in situations where bytes need to be buffered
* but total size of data is not known in advance and may potentially be very large.
* Provides easy way to access small buffered data as byte[] or String.
* Enables efficient memory-only handling of small data while automatically switching
* to temporary file storage when data gets too big to fit in memory buffer.
* It is highly efficient for both byte-per-byte and block I/O.
* This class is not a FIFO - you can't mix reading and writing infinitely as all data
* keep being buffered, not just unread data.
* Mixing reads and writes may be inefficient in some situations but is fully supported.
*
* Overall usage strategy: You first write data to the buffer using OutputStream
* returned by getOutputStream(), then examine data size using getLength()
* and isLarge() and either call getBytes() to get byte[],
* getString() to get data as String or getInputStream() to read data using stream.
* Instance of TempByteHolder can be safely and efficiently reused by calling clear().
* When TempByteHolder is no longer needed you must call close() to ensure underlying
* temporary file is closed and deleted.
*
* NOTE: For performance, this class is not synchronized. If you need thread safety,
* use synchronized wrapper.
* This class can hold up to 2GB of data.
*
* SECURITY NOTE: As data may be written to disk, don't use this for sensitive information.
* @author Jan Hlavatý <hlavac AT code.cz>
*/
public class TempByteHolder {
byte[] _memory_buffer = null; /** buffer to use */
boolean _file_mode = false; /** false: memory buffer mode (small data)
true: temp file mode (large data) */
int _window_size = 0; /** size of memory buffer */
int _window_low = 0; /** offset of first byte in memory buffer */
int _window_high = 0; /** offset of first byte after memory buffer */
int _file_high = 0; /** offset of fist byte not yet written to temp file */
int _write_pos = 0; /** offset of next byte to be writen; number of bytes written */
int _read_pos = 0; /** offset of fist byte to be read */
int _file_pos = -1; /** current temp file seek offset; -1 = unknown */
int _mark_pos = 0; /** mark */
/** Instance of OutputStream is cached and reused. */
TempByteHolder.OutputStream _output_stream = new TempByteHolder.OutputStream();
/** Instance of InputStream is cached and reused. */
TempByteHolder.InputStream _input_stream = null; //input_stream = new TempByteHolder.InputStream();
/** Temporary directory to be used, or null for system default */
File _temp_directory = null;
/** File object representing temporary file. */
File _tempfilef = null;
/** Temporary file or null when none is used yet */
RandomAccessFile _tempfile = null;
//----- constructors -------
/**
* Creates a new instance of TempByteHolder allocating memory buffer of given capacity.
* You should use reasonably large buffer for potentionally large data to improve
* effect of caching for file operations (about 512 bytes).
* @param in_memory_capacity Size in bytes of memory buffer to allocate.
*/
public TempByteHolder(int in_memory_capacity) {
this(new byte[in_memory_capacity],0,0);
}
/**
* Creates a new instance of TempByteHolder using passed byte[] as memory buffer.
* @param byte_array byte array to be used as memory buffer.
*/
public TempByteHolder(byte[] byte_array) {
this(byte_array,0,0);
}
/**
* Creates a new instance of TempByteHolder using passed byte[] which
* contains prefilled data as memory buffer.
* @param byte_array byte array to be used as memory buffer.
* @param offset offset of prefilled data in buffer.
* @param prefilled_data_size number of bytes that contain valid data.
*/
public TempByteHolder(byte[] byte_array, int offset, int prefilled_data_size) {
if (byte_array == null) throw new NullPointerException();
_window_size = byte_array.length;
if ((offset < 0) || (offset > _window_size)) throw new IllegalArgumentException("Bad prefilled data offset");
if ((offset+prefilled_data_size > _window_size)||(prefilled_data_size < 0)) throw new IllegalArgumentException("Bad prefilled data size");
_memory_buffer = byte_array;
_write_pos = prefilled_data_size;
_window_low = -offset;
_window_high = _window_size-offset;
}
protected void finalize() {
try {
close();
} catch (IOException e) {
}
}
/**
* Erases all unread buffered data and prepares for next use cycle.
* If temporary file was used, it is not closed/deleted yet as it may be needed again.
*/
public void clear() {
_file_mode = false;
_write_pos = 0;
_read_pos = 0;
_window_low = 0;
_window_high = _window_size;
_file_high = 0;
_mark_pos = 0;
}
/**
* Clears all data and closes/deletes backing temporary file if used.
* @throws IOException when something goes wrong.
*/
public void close() throws IOException {
clear();
if (_tempfile != null) {
_tempfile.close();
_tempfile = null;
_tempfilef.delete();
_tempfilef = null;
}
}
/**
* Repositions InputStream at given offset within buffered data.
* @throws IOException when something goes wrong.
*/
public void seek(int offset) throws IOException {
if ((offset <= _write_pos)&&(offset>=0)) {
_read_pos = offset;
} else throw new IOException("bad seek offset");
}
/**
* Truncates buffered data to specified size. Can not be used to extend data.
* Repositions OutputStream at the end of truncated data.
* If current read offset or mark is past the new end of data, it is moved at the new end.
*/
public void truncate(int offset) throws IOException {
if ((offset < 0)||(offset > _write_pos)) throw new IOException("bad truncate offset");
if (_read_pos > offset) _read_pos = offset;
if (_mark_pos > offset) _mark_pos = offset;
_write_pos = offset;
if (_file_high > offset) _file_high = offset;
moveWindow(_write_pos);
}
/**
* Override directory to create temporary file in.
* Does not affect already open temp file.
* @param dir File object representing temporary directory.
* May be null which means that system default
* (java.io.tmpdir system property) should be used.
* @throws IOException
*/
public void setTempDirectory(File dir) throws IOException {
File td = dir.getCanonicalFile();
if (td.isDirectory()) {
_temp_directory = td;
}
}
/**
* Returns number of bytes buffered so far.
* @return total number of bytes buffered. If you need number of bytes
* to be read, use InputStream.available() .
*/
public int getLength() {
return _write_pos;
}
/**
* Tells whether buffered data is small enough to fit in memory buffer
* so that it can be returned as byte[]. Data is considered large
* when it will not fit into backing memory buffer.
* @return true when data is only accessible through InputStream interface;
* false when data can be also retrieved directly as byte[] or String.
* @see #getBytes()
* @see #getString(String)
*/
public boolean isLarge() {
return _file_mode;
}
/**
* Returns byte[] that holds all buffered data in its first getLength() bytes.
* If this instance was created using (byte[]) constructor, this is the same
* array that has been passed to the constructor. If buffered data don't fit into
* memory buffer, IllegalStateException is thrown.
* @return byte[] with data as its first getLength() bytes.
* @throws IllegalStateException when data is too big to be read this way.
* @see #isLarge()
* @see #getLength()
* @see #getString(String)
* @see #getInputStream()
*/
public byte[] getBytes() {
if (_file_mode) throw new IllegalStateException("data too large");
return _memory_buffer;
}
/**
* Returns buffered data as String using given character encoding.
* @param character_encoding Name of character encoding to use for
* converting bytes to String.
* @return Buffered data as String.
* @throws IllegalStateException when data is too large to be read this way.
* @throws java.io.UnsupportedEncodingException when this encoding is not supported.
*/
public String getString(String character_encoding) throws java.io.UnsupportedEncodingException {
if (_file_mode) throw new IllegalStateException("data too large");
return new String(_memory_buffer,0,_write_pos,character_encoding);
}
/**
* Returns OutputStream filling this buffer.
* @return OutputStream for writing in the buffer.
*/
public java.io.OutputStream getOutputStream() {
return _output_stream;
}
/**
* Returns InputSream for reading buffered data.
* @return InputSream for reading buffered data.
*/
public java.io.InputStream getInputStream() {
if (_input_stream == null) {
_input_stream = new TempByteHolder.InputStream();
}
return _input_stream;
}
/**
* Writes efficiently whole content to output stream.
* @param os OutputStream to write to
* @throws IOException
*/
public void writeTo(java.io.OutputStream os) throws IOException {
writeTo(os, 0, getLength());
}
/**
* Writes efficiently part of the content to output stream.
* @param os OutputStream to write to
* @param start_offset Offset of data fragment to be written
* @param length Length of data fragment to be written
* @throws IOException
*/
public void writeTo(java.io.OutputStream os, int start_offset, int length) throws IOException {
int towrite = min(length, _write_pos-start_offset);
int writeoff = start_offset;
if (towrite > 0) {
while (towrite >= _window_size) {
moveWindow(writeoff);
os.write(_memory_buffer,0,_window_size);
towrite -= _window_size;
writeoff += _window_size;
}
if (towrite > 0) {
moveWindow(writeoff);
os.write(_memory_buffer,0,towrite);
}
}
}
/**
* Reads all available data from input stream.
* @param is
* @throws IOException
*/
public void readFrom(java.io.InputStream is) throws IOException {
int howmuch = 0;
do {
_write_pos += howmuch;
moveWindow(_write_pos);
howmuch = is.read(_memory_buffer);
} while (howmuch != -1);
}
// ----- helper methods -------
/**
* Create tempfile if it does not already exist
*/
private void createTempFile() throws IOException {
_tempfilef = File.createTempFile("org.browsermob.proxy.jetty.util.TempByteHolder-",".tmp",_temp_directory).getCanonicalFile();
_tempfilef.deleteOnExit();
_tempfile = new RandomAccessFile(_tempfilef,"rw");
}
/**
* Write chunk of data at specified offset in temp file.
* Marks data as big.
* Updates high water mark on tempfile content.
*/
private void writeToTempFile(int at_offset, byte[] data, int offset, int len) throws IOException {
if (_tempfile == null) {
createTempFile();
_file_pos = -1;
}
_file_mode = true;
if (at_offset != _file_pos) {
_tempfile.seek((long)at_offset);
}
_tempfile.write(data,offset,len);
_file_pos = at_offset + len;
_file_high = max(_file_high,_file_pos);
}
/**
* Read chunk of data from specified offset in tempfile
*/
private void readFromTempFile(int at_offset, byte[] data, int offset, int len) throws IOException {
if (_file_pos != at_offset) {
_tempfile.seek((long)at_offset);
}
_tempfile.readFully(data,offset,len);
_file_pos = at_offset+len;
}
/**
* Move file window, synchronizing data with file.
* Works somewhat like memory-mapping a file.
* This one was nightmare to write :-)
*/
private void moveWindow(int start_offset) throws IOException {
if (start_offset != _window_low) { // only when we have to move
int end_offset = start_offset + _window_size;
// new window low/high = start_offset/end_offset
int dirty_low = _file_high;
int dirty_high = _write_pos;
int dirty_len = _write_pos - _file_high;
if (dirty_len > 0) { // we need to be concerned at all about dirty data.
// will any part of dirty data be moved out of window?
if ( (dirty_low < start_offset) || (dirty_high > end_offset) ) {
// yes, dirty data need to be saved.
writeToTempFile(dirty_low, _memory_buffer, dirty_low - _window_low, dirty_len);
}
}
// reposition any data from old window that will be also in new window:
int stay_low = max(start_offset,_window_low);
int stay_high = min(_write_pos, _window_high, end_offset);
// is there anything to preserve?
int stay_size = stay_high - stay_low;
if (stay_size > 0) {
System.arraycopy(_memory_buffer, stay_low-_window_low, _memory_buffer, stay_low-start_offset, stay_size);
}
// read in available data that were not in old window:
if (stay_low > start_offset) {
// read at the start of buffer
int toread_low = start_offset;
int toread_high = min(stay_low,end_offset);
int toread_size = toread_high - toread_low;
if (toread_size > 0) {
readFromTempFile(toread_low, _memory_buffer, toread_low-start_offset, toread_size);
}
}
if (stay_high < end_offset) {
// read at end of buffer
int toread_low = max(stay_high,start_offset);
int toread_high = min(end_offset,_file_high);
int toread_size = toread_high-toread_low;
if (toread_size > 0) {
readFromTempFile(toread_low, _memory_buffer, toread_low-start_offset, toread_size);
}
}
_window_low = start_offset;
_window_high = end_offset;
}
}
/** Simple minimum for 2 ints */
private static int min(int a, int b) {
return (ab?a:b);
}
/** Simple minimum for 3 ints */
private static int min(int a, int b, int c) {
int r = a;
if (r > b) r = b;
if (r > c) r = c;
return r;
}
/**
* @return true when range 1 is fully contained in range 2
*/
private static boolean contained(int range1_low, int range1_high, int range2_low, int range2_high) {
return ((range1_low >= range2_low)&&(range1_high <= range2_high));
}
/**
* Internal implementation of java.io.OutputStream used to fill the byte buffer.
*/
class OutputStream extends java.io.OutputStream {
/**
* Write whole byte array into buffer.
* @param data byte[] to be written
* @throws IOException when something goes wrong.
*/
public void write(byte[] data) throws IOException {
write(data,0,data.length);
}
/**
* Write segment of byte array to the buffer.
* @param data Byte array with data
* @param off Starting offset within the array.
* @param len Number of bytes to write
* @throws IOException when something goes wrong.
*/
public void write(byte[] data, int off, int len) throws IOException {
int new_write_pos = _write_pos + len;
boolean write_pos_in_window = (_write_pos >= _window_low)&&(_write_pos < _window_high);
if (!write_pos_in_window) {
// either current window is full of dirty data or it is somewhere low
moveWindow(_write_pos); // flush buffer if necessary, move window at end
}
boolean end_of_data_in_window = (new_write_pos <= _window_high);
if ( end_of_data_in_window ) {
// if there is space in window for all data, just put it in buffer.
// 0 writes, window unchanged
System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, len);
_write_pos = new_write_pos;
} else {
int out_of_window = new_write_pos - _window_high;
if (out_of_window < _window_size) {
// start of data in window, rest will fit in a new window:
// 1 write, window moved at window_high, filled with rest of data
// fill in rest of the current window with first part of data
int part1_len = _window_high - _write_pos;
int part2_len = len - part1_len;
System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, part1_len);
_write_pos = _window_high;
moveWindow(_write_pos); // flush data to file
System.arraycopy(data, off+part1_len, _memory_buffer, 0, part2_len);
_write_pos = new_write_pos;
} else {
// start of data in window, rest will not fit in window (and leave some space):
// 2 writes; window moved at end, empty
int part1_size = _window_high - _write_pos;
int part2_size = len - part1_size;
if (part1_size == _window_size) {
// buffer was empty - no sense in splitting the write
// write data directly to file in one chunk
writeToTempFile(_write_pos, data, off, len);
_write_pos = new_write_pos;
moveWindow(_write_pos);
} else {
// copy part 1 to window
if (part1_size > 0) {
System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, part1_size);
_write_pos += part1_size;
moveWindow(_write_pos); // flush buffer
}
// flush window to file
// write part 2 directly to file
writeToTempFile(_write_pos, data, off+part1_size, part2_size);
_write_pos = new_write_pos;
moveWindow(_write_pos);
}
}
}
}
/**
* Write single byte to the buffer.
* @param b
* @throws IOException
*/
public void write(int b) throws IOException {
if ((_write_pos >= _window_high) || (_write_pos < _window_low)) {
moveWindow(_write_pos);
}
// we now have space for one byte in window.
_memory_buffer[_write_pos - _window_low] = (byte)(b &0xFF);
_write_pos++;
}
public void flush() throws IOException {
moveWindow(_write_pos); // or no-op? not needed
}
public void close() throws IOException {
// no-op: this output stream does not need to be closed.
}
}
/**
* Internal implementation of InputStream used to read buffered data.
*/
class InputStream extends java.io.InputStream {
public int read() throws IOException {
int ret = -1;
// if window does not contain read position, move it there
if (!contained(_read_pos,_read_pos+1, _window_low, _window_high)) {
moveWindow(_read_pos);
}
if (_write_pos > _read_pos) {
ret = (_memory_buffer[_read_pos - _window_low])&0xFF;
_read_pos++;
}
return ret;
}
public int read(byte[] buff) throws IOException {
return read(buff,0, buff.length);
}
public int read(byte[] buff, int off, int len) throws IOException {
// clip read to available data:
int read_size = min(len,_write_pos-_read_pos);
if (read_size > 0) {
if (read_size >= _window_size) {
// big chunk: read directly from file
moveWindow(_write_pos);
readFromTempFile(_read_pos, buff, off, read_size);
} else {
// small chunk:
int read_low = _read_pos;
int read_high = read_low + read_size;
// if we got all data in current window, read it from there
if (!contained(read_low,read_high, _window_low, _window_high)) {
moveWindow(_read_pos);
}
System.arraycopy(_memory_buffer, _read_pos - _window_low, buff, off, read_size);
}
_read_pos += read_size;
}
return read_size;
}
public long skip(long bytes) throws IOException {
if (bytes < 0 || bytes > Integer.MAX_VALUE) throw new IllegalArgumentException();
int len = (int)bytes;
if ( (len+_read_pos) > _write_pos ) len = _write_pos - _read_pos;
_read_pos+=len;
moveWindow(_write_pos); // invalidate window without reading data by moving it at the end
return (long)len;
}
public int available() throws IOException {
return _write_pos - _read_pos;
}
public void mark(int readlimit) {
// readlimit is ignored, we store all the data anyway
_mark_pos = _read_pos;
}
public void reset() throws IOException {
_read_pos = _mark_pos;
}
public boolean markSupported() {
return true;
}
}
}