Source code: org/mortbay/util/TempByteHolder.java
1 // ========================================================================
2 // Copyright (c) 2002 Mort Bay Consulting, Sydney
3 // $Id: TempByteHolder.java,v 1.6 2003/06/27 18:49:43 hlavac Exp $
4 // ========================================================================
5 package org.mortbay.util;
6
7 import java.io.File;
8 import java.io.IOException;
9 import java.io.RandomAccessFile;
10
11
12
13 /**
14 * Temporary buffer for bytes to be used in situations where bytes need to be buffered
15 * but total size of data is not known in advance and may potentially be very large.
16 * Provides easy way to access small buffered data as byte[] or String.
17 * Enables efficient memory-only handling of small data while automatically switching
18 * to temporary file storage when data gets too big to fit in memory buffer.
19 * It is highly efficient for both byte-per-byte and block I/O.
20 * This class is not a FIFO - you can't mix reading and writing infinitely as all data
21 * keep being buffered, not just unread data.
22 * Mixing reads and writes may be inefficient in some situations but is fully supported.
23 * <br>
24 * Overall usage strategy: You first write data to the buffer using OutputStream
25 * returned by getOutputStream(), then examine data size using getLength()
26 * and isLarge() and either call getBytes() to get byte[],
27 * getString() to get data as String or getInputStream() to read data using stream.
28 * Instance of TempByteHolder can be safely and efficiently reused by calling clear().
29 * When TempByteHolder is no longer needed you must call close() to ensure underlying
30 * temporary file is closed and deleted.
31 * <br><br>
32 * <i>NOTE:</i> For performance, this class is not synchronized. If you need thread safety,
33 * use synchronized wrapper.<br>
34 * This class can hold up to 2GB of data.
35 * <br><br>
36 * <i>SECURITY NOTE:</i> As data may be written to disk, don't use this for sensitive information.
37 * @author Jan Hlavatý <hlavac AT code.cz>
38 */
39 public class TempByteHolder {
40
41 byte[] _memory_buffer = null; /** buffer to use */
42
43 boolean _file_mode = false; /** false: memory buffer mode (small data)
44 true: temp file mode (large data) */
45
46 int _window_size = 0; /** size of memory buffer */
47 int _window_low = 0; /** offset of first byte in memory buffer */
48 int _window_high = 0; /** offset of first byte after memory buffer */
49 int _file_high = 0; /** offset of fist byte not yet written to temp file */
50 int _write_pos = 0; /** offset of next byte to be writen; number of bytes written */
51 int _read_pos = 0; /** offset of fist byte to be read */
52 int _file_pos = -1; /** current temp file seek offset; -1 = unknown */
53 int _mark_pos = 0; /** mark */
54
55
56 /** Instance of OutputStream is cached and reused. */
57 TempByteHolder.OutputStream _output_stream = new TempByteHolder.OutputStream();
58 /** Instance of InputStream is cached and reused. */
59 TempByteHolder.InputStream _input_stream = null; //input_stream = new TempByteHolder.InputStream();
60
61 /** Temporary directory to be used, or null for system default */
62 File _temp_directory = null;
63 /** File object representing temporary file. */
64 File _tempfilef = null;
65 /** Temporary file or null when none is used yet */
66 RandomAccessFile _tempfile = null;
67
68
69 //----- constructors -------
70
71 /**
72 * Creates a new instance of TempByteHolder allocating memory buffer of given capacity.
73 * You should use reasonably large buffer for potentionally large data to improve
74 * effect of caching for file operations (about 512 bytes).
75 * @param in_memory_capacity Size in bytes of memory buffer to allocate.
76 */
77 public TempByteHolder(int in_memory_capacity) {
78 this(new byte[in_memory_capacity],0,0);
79 }
80
81 /**
82 * Creates a new instance of TempByteHolder using passed byte[] as memory buffer.
83 * @param byte_array byte array to be used as memory buffer.
84 */
85 public TempByteHolder(byte[] byte_array) {
86 this(byte_array,0,0);
87 }
88
89 /**
90 * Creates a new instance of TempByteHolder using passed byte[] which
91 * contains prefilled data as memory buffer.
92 * @param byte_array byte array to be used as memory buffer.
93 * @param offset offset of prefilled data in buffer.
94 * @param prefilled_data_size number of bytes that contain valid data.
95 */
96 public TempByteHolder(byte[] byte_array, int offset, int prefilled_data_size) {
97 if (byte_array == null) throw new NullPointerException();
98 _window_size = byte_array.length;
99 if ((offset < 0) || (offset > _window_size)) throw new IllegalArgumentException("Bad prefilled data offset");
100 if ((offset+prefilled_data_size > _window_size)||(prefilled_data_size < 0)) throw new IllegalArgumentException("Bad prefilled data size");
101 _memory_buffer = byte_array;
102 _write_pos = prefilled_data_size;
103 _window_low = -offset;
104 _window_high = _window_size-offset;
105 }
106
107 public void finalize() {
108 try {
109 close();
110 } catch (IOException e) {
111 }
112 }
113
114 /**
115 * Erases all unread buffered data and prepares for next use cycle.
116 * If temporary file was used, it is not closed/deleted yet as it may be needed again.
117 */
118 public void clear() {
119 _file_mode = false;
120 _write_pos = 0;
121 _read_pos = 0;
122 _window_low = 0;
123 _window_high = _window_size;
124 _file_high = 0;
125 _mark_pos = 0;
126 }
127
128 /**
129 * Clears all data and closes/deletes backing temporary file if used.
130 * @throws IOException when something goes wrong.
131 */
132 public void close() throws IOException {
133 clear();
134 if (_tempfile != null) {
135 _tempfile.close();
136 _tempfile = null;
137 _tempfilef.delete();
138 _tempfilef = null;
139 }
140 }
141
142 /**
143 * Repositions InputStream at given offset within buffered data.
144 * @throws IOException when something goes wrong.
145 */
146 public void seek(int offset) throws IOException {
147 if ((offset <= _write_pos)&&(offset>=0)) {
148 _read_pos = offset;
149 } else throw new IOException("bad seek offset");
150 }
151
152 /**
153 * Truncates buffered data to specified size. Can not be used to extend data.
154 * Repositions OutputStream at the end of truncated data.
155 * If current read offset or mark is past the new end of data, it is moved at the new end.
156 */
157 public void truncate(int offset) throws IOException {
158 if ((offset < 0)||(offset > _write_pos)) throw new IOException("bad truncate offset");
159 if (_read_pos > offset) _read_pos = offset;
160 if (_mark_pos > offset) _mark_pos = offset;
161 _write_pos = offset;
162 if (_file_high > offset) _file_high = offset;
163 moveWindow(_write_pos);
164 }
165
166
167 /**
168 * Override directory to create temporary file in.
169 * Does not affect already open temp file.
170 * @param dir File object representing temporary directory.
171 * May be null which means that system default
172 * (java.io.tmpdir system property) should be used.
173 * @throws IOException
174 */
175 public void setTempDirectory(File dir) throws IOException {
176 File td = dir.getCanonicalFile();
177 if (td.isDirectory()) {
178 _temp_directory = td;
179 }
180 }
181
182
183
184 /**
185 * Returns number of bytes buffered so far.
186 * @return total number of bytes buffered. If you need number of bytes
187 * to be read, use InputStream.available() .
188 */
189 public int getLength() {
190 return _write_pos;
191 }
192
193 /**
194 * Tells whether buffered data is small enough to fit in memory buffer
195 * so that it can be returned as byte[]. Data is considered large
196 * when it will not fit into backing memory buffer.
197 * @return true when data is only accessible through InputStream interface;
198 * false when data can be also retrieved directly as byte[] or String.
199 * @see #getBytes()
200 * @see #getString(String)
201 */
202 public boolean isLarge() {
203 return _file_mode;
204 }
205
206
207 /**
208 * Returns byte[] that holds all buffered data in its first getLength() bytes.
209 * If this instance was created using (byte[]) constructor, this is the same
210 * array that has been passed to the constructor. If buffered data don't fit into
211 * memory buffer, IllegalStateException is thrown.
212 * @return byte[] with data as its first getLength() bytes.
213 * @throws IllegalStateException when data is too big to be read this way.
214 * @see #isLarge()
215 * @see #getLength()
216 * @see #getString(String)
217 * @see #getInputStream()
218 */
219 public byte[] getBytes() {
220 if (_file_mode) throw new IllegalStateException("data too large");
221 return _memory_buffer;
222 }
223
224 /**
225 * Returns buffered data as String using given character encoding.
226 * @param character_encoding Name of character encoding to use for
227 * converting bytes to String.
228 * @return Buffered data as String.
229 * @throws IllegalStateException when data is too large to be read this way.
230 * @throws java.io.UnsupportedEncodingException when this encoding is not supported.
231 */
232 public String getString(String character_encoding) throws java.io.UnsupportedEncodingException {
233 if (_file_mode) throw new IllegalStateException("data too large");
234 return new String(_memory_buffer,0,_write_pos,character_encoding);
235 }
236
237 /**
238 * Returns OutputStream filling this buffer.
239 * @return OutputStream for writing in the buffer.
240 */
241
242 public java.io.OutputStream getOutputStream() {
243 return _output_stream;
244 }
245
246
247 /**
248 * Returns InputSream for reading buffered data.
249 * @return InputSream for reading buffered data.
250 */
251 public java.io.InputStream getInputStream() {
252 if (_input_stream == null) {
253 _input_stream = new TempByteHolder.InputStream();
254 }
255 return _input_stream;
256 }
257
258
259 /**
260 * Writes efficiently whole content to output stream.
261 * @param os OutputStream to write to
262 * @throws IOException
263 */
264 public void writeTo(java.io.OutputStream os) throws IOException {
265 writeTo(os, 0, getLength());
266 }
267
268
269 /**
270 * Writes efficiently part of the content to output stream.
271 * @param os OutputStream to write to
272 * @param start_offset Offset of data fragment to be written
273 * @param length Length of data fragment to be written
274 * @throws IOException
275 */
276 public void writeTo(java.io.OutputStream os, int start_offset, int length) throws IOException {
277 int towrite = min(length, _write_pos-start_offset);
278 int writeoff = start_offset;
279 if (towrite > 0) {
280 while (towrite >= _window_size) {
281 moveWindow(writeoff);
282 os.write(_memory_buffer,0,_window_size);
283 towrite -= _window_size;
284 writeoff += _window_size;
285 }
286 if (towrite > 0) {
287 moveWindow(writeoff);
288 os.write(_memory_buffer,0,towrite);
289 }
290 }
291 }
292
293
294 /**
295 * Reads all available data from input stream.
296 * @param is
297 * @throws IOException
298 */
299 public void readFrom(java.io.InputStream is) throws IOException {
300 int howmuch = 0;
301 do {
302 _write_pos += howmuch;
303 moveWindow(_write_pos);
304 howmuch = is.read(_memory_buffer);
305 } while (howmuch != -1);
306 }
307
308
309 // ----- helper methods -------
310
311 /**
312 * Create tempfile if it does not already exist
313 */
314 private void createTempFile() throws IOException {
315 _tempfilef = File.createTempFile("org.mortbay.util.TempByteHolder-",".tmp",_temp_directory).getCanonicalFile();
316 _tempfilef.deleteOnExit();
317 _tempfile = new RandomAccessFile(_tempfilef,"rw");
318 }
319
320 /**
321 * Write chunk of data at specified offset in temp file.
322 * Marks data as big.
323 * Updates high water mark on tempfile content.
324 */
325 private void writeToTempFile(int at_offset, byte[] data, int offset, int len) throws IOException {
326 if (_tempfile == null) {
327 createTempFile();
328 _file_pos = -1;
329 }
330 _file_mode = true;
331 if (at_offset != _file_pos) {
332 _tempfile.seek((long)at_offset);
333 }
334 _tempfile.write(data,offset,len);
335 _file_pos = at_offset + len;
336 _file_high = max(_file_high,_file_pos);
337 }
338
339 /**
340 * Read chunk of data from specified offset in tempfile
341 */
342 private void readFromTempFile(int at_offset, byte[] data, int offset, int len) throws IOException {
343 if (_file_pos != at_offset) {
344 _tempfile.seek((long)at_offset);
345 }
346 _tempfile.readFully(data,offset,len);
347 _file_pos = at_offset+len;
348 }
349
350
351 /**
352 * Move file window, synchronizing data with file.
353 * Works somewhat like memory-mapping a file.
354 * This one was nightmare to write :-)
355 */
356 private void moveWindow(int start_offset) throws IOException {
357 if (start_offset != _window_low) { // only when we have to move
358
359 int end_offset = start_offset + _window_size;
360 // new window low/high = start_offset/end_offset
361 int dirty_low = _file_high;
362 int dirty_high = _write_pos;
363 int dirty_len = _write_pos - _file_high;
364 if (dirty_len > 0) { // we need to be concerned at all about dirty data.
365 // will any part of dirty data be moved out of window?
366 if ( (dirty_low < start_offset) || (dirty_high > end_offset) ) {
367 // yes, dirty data need to be saved.
368 writeToTempFile(dirty_low, _memory_buffer, dirty_low - _window_low, dirty_len);
369 }
370 }
371
372 // reposition any data from old window that will be also in new window:
373
374 int stay_low = max(start_offset,_window_low);
375 int stay_high = min(_write_pos, _window_high, end_offset);
376 // is there anything to preserve?
377 int stay_size = stay_high - stay_low;
378 if (stay_size > 0) {
379 System.arraycopy(_memory_buffer, stay_low-_window_low, _memory_buffer, stay_low-start_offset, stay_size);
380 }
381
382 // read in available data that were not in old window:
383 if (stay_low > start_offset) {
384 // read at the start of buffer
385 int toread_low = start_offset;
386 int toread_high = min(stay_low,end_offset);
387 int toread_size = toread_high - toread_low;
388 if (toread_size > 0) {
389 readFromTempFile(toread_low, _memory_buffer, toread_low-start_offset, toread_size);
390 }
391 }
392 if (stay_high < end_offset) {
393 // read at end of buffer
394 int toread_low = max(stay_high,start_offset);
395 int toread_high = min(end_offset,_file_high);
396 int toread_size = toread_high-toread_low;
397 if (toread_size > 0) {
398 readFromTempFile(toread_low, _memory_buffer, toread_low-start_offset, toread_size);
399 }
400 }
401 _window_low = start_offset;
402 _window_high = end_offset;
403 }
404 }
405
406 /** Simple minimum for 2 ints */
407 private static int min(int a, int b) {
408 return (a<b?a:b);
409 }
410
411 /** Simple maximum for 2 ints */
412 private static int max(int a, int b) {
413 return (a>b?a:b);
414 }
415
416 /** Simple minimum for 3 ints */
417 private static int min(int a, int b, int c) {
418 int r = a;
419 if (r > b) r = b;
420 if (r > c) r = c;
421 return r;
422 }
423
424 /**
425 * @return true when range 1 is fully contained in range 2
426 */
427 private static boolean contained(int range1_low, int range1_high, int range2_low, int range2_high) {
428 return ((range1_low >= range2_low)&&(range1_high <= range2_high));
429 }
430
431 /**
432 * Internal implementation of java.io.OutputStream used to fill the byte buffer.
433 */
434 class OutputStream extends java.io.OutputStream {
435
436 /**
437 * Write whole byte array into buffer.
438 * @param data byte[] to be written
439 * @throws IOException when something goes wrong.
440 */
441 public void write(byte[] data) throws IOException {
442 write(data,0,data.length);
443 }
444
445 /**
446 * Write segment of byte array to the buffer.
447 * @param data Byte array with data
448 * @param off Starting offset within the array.
449 * @param len Number of bytes to write
450 * @throws IOException when something goes wrong.
451 */
452 public void write(byte[] data, int off, int len) throws IOException {
453 int new_write_pos = _write_pos + len;
454 boolean write_pos_in_window = (_write_pos >= _window_low)&&(_write_pos < _window_high);
455
456 if (!write_pos_in_window) {
457 // either current window is full of dirty data or it is somewhere low
458 moveWindow(_write_pos); // flush buffer if necessary, move window at end
459 }
460
461 boolean end_of_data_in_window = (new_write_pos <= _window_high);
462
463 if ( end_of_data_in_window ) {
464 // if there is space in window for all data, just put it in buffer.
465 // 0 writes, window unchanged
466 System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, len);
467 _write_pos = new_write_pos;
468 } else {
469 int out_of_window = new_write_pos - _window_high;
470 if (out_of_window < _window_size) {
471 // start of data in window, rest will fit in a new window:
472 // 1 write, window moved at window_high, filled with rest of data
473
474 // fill in rest of the current window with first part of data
475 int part1_len = _window_high - _write_pos;
476 int part2_len = len - part1_len;
477
478 System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, part1_len);
479 _write_pos = _window_high;
480
481 moveWindow(_write_pos); // flush data to file
482
483 System.arraycopy(data, off+part1_len, _memory_buffer, 0, part2_len);
484 _write_pos = new_write_pos;
485
486 } else {
487 // start of data in window, rest will not fit in window (and leave some space):
488 // 2 writes; window moved at end, empty
489
490 int part1_size = _window_high - _write_pos;
491 int part2_size = len - part1_size;
492
493 if (part1_size == _window_size) {
494 // buffer was empty - no sense in splitting the write
495 // write data directly to file in one chunk
496 writeToTempFile(_write_pos, data, off, len);
497 _write_pos = new_write_pos;
498 moveWindow(_write_pos);
499
500 } else {
501 // copy part 1 to window
502 if (part1_size > 0) {
503 System.arraycopy(data, off, _memory_buffer, _write_pos-_window_low, part1_size);
504 _write_pos += part1_size;
505 moveWindow(_write_pos); // flush buffer
506 }
507 // flush window to file
508 // write part 2 directly to file
509 writeToTempFile(_write_pos, data, off+part1_size, part2_size);
510 _write_pos = new_write_pos;
511 moveWindow(_write_pos);
512 }
513 }
514 }
515 }
516
517 /**
518 * Write single byte to the buffer.
519 * @param b
520 * @throws IOException
521 */
522 public void write(int b) throws IOException {
523 if ((_write_pos >= _window_high) || (_write_pos < _window_low)) {
524 moveWindow(_write_pos);
525 }
526 // we now have space for one byte in window.
527 _memory_buffer[_write_pos - _window_low] = (byte)(b &0xFF);
528 _write_pos++;
529 }
530
531 public void flush() throws IOException {
532 moveWindow(_write_pos); // or no-op? not needed
533 }
534
535 public void close() throws IOException {
536 // no-op: this output stream does not need to be closed.
537 }
538 }
539
540
541
542
543
544 /**
545 * Internal implementation of InputStream used to read buffered data.
546 */
547 class InputStream extends java.io.InputStream {
548
549 public int read() throws IOException {
550 int ret = -1;
551 // if window does not contain read position, move it there
552 if (!contained(_read_pos,_read_pos+1, _window_low, _window_high)) {
553 moveWindow(_read_pos);
554 }
555 if (_write_pos > _read_pos) {
556 ret = (_memory_buffer[_read_pos - _window_low])&0xFF;
557 _read_pos++;
558 }
559 return ret;
560 }
561
562 public int read(byte[] buff) throws IOException {
563 return read(buff,0, buff.length);
564 }
565
566 public int read(byte[] buff, int off, int len) throws IOException {
567 // clip read to available data:
568 int read_size = min(len,_write_pos-_read_pos);
569 if (read_size > 0) {
570 if (read_size >= _window_size) {
571 // big chunk: read directly from file
572 moveWindow(_write_pos);
573 readFromTempFile(_read_pos, buff, off, read_size);
574 } else {
575 // small chunk:
576 int read_low = _read_pos;
577 int read_high = read_low + read_size;
578 // if we got all data in current window, read it from there
579 if (!contained(read_low,read_high, _window_low, _window_high)) {
580 moveWindow(_read_pos);
581 }
582 System.arraycopy(_memory_buffer, _read_pos - _window_low, buff, off, read_size);
583 }
584 _read_pos += read_size;
585 }
586 return read_size;
587 }
588
589 public long skip(long bytes) throws IOException {
590 if (bytes < 0 || bytes > Integer.MAX_VALUE) throw new IllegalArgumentException();
591 int len = (int)bytes;
592 if ( (len+_read_pos) > _write_pos ) len = _write_pos - _read_pos;
593 _read_pos+=len;
594 moveWindow(_write_pos); // invalidate window without reading data by moving it at the end
595 return (long)len;
596 }
597
598 public int available() throws IOException {
599 return _write_pos - _read_pos;
600 }
601
602
603 public void mark(int readlimit) {
604 // readlimit is ignored, we store all the data anyway
605 _mark_pos = _read_pos;
606 }
607
608 public void reset() throws IOException {
609 _read_pos = _mark_pos;
610 }
611
612 public boolean markSupported() {
613 return true;
614 }
615
616
617 }
618 }