View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.util;
46  
47  import java.io.BufferedOutputStream;
48  import java.io.File;
49  import java.io.FileInputStream;
50  import java.io.FileOutputStream;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.io.OutputStream;
54  import java.util.ArrayList;
55  
56  import org.eclipse.jgit.internal.JGitText;
57  import org.eclipse.jgit.lib.NullProgressMonitor;
58  import org.eclipse.jgit.lib.ProgressMonitor;
59  import org.eclipse.jgit.util.io.SafeBufferedOutputStream;
60  
61  /**
62   * A fully buffered output stream.
63   * <p>
64   * Subclasses determine the behavior when the in-memory buffer capacity has been
65   * exceeded and additional bytes are still being received for output.
66   */
67  public abstract class TemporaryBuffer extends OutputStream {
68  	/** Default limit for in-core storage. */
69  	protected static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024;
70  
71  	/** Chain of data, if we are still completely in-core; otherwise null. */
72  	ArrayList<Block> blocks;
73  
74  	/**
75  	 * Maximum number of bytes we will permit storing in memory.
76  	 * <p>
77  	 * When this limit is reached the data will be shifted to a file on disk,
78  	 * preventing the JVM heap from growing out of control.
79  	 */
80  	private int inCoreLimit;
81  
82  	/** Initial size of block list. */
83  	private int initialBlocks;
84  
85  	/** If {@link #inCoreLimit} has been reached, remainder goes here. */
86  	private OutputStream overflow;
87  
88  	/**
89  	 * Create a new empty temporary buffer.
90  	 *
91  	 * @param limit
92  	 *            maximum number of bytes to store in memory before entering the
93  	 *            overflow output path; also used as the estimated size.
94  	 */
95  	protected TemporaryBuffer(final int limit) {
96  		this(limit, limit);
97  	}
98  
99  	/**
100 	 * Create a new empty temporary buffer.
101 	 *
102 	 * @param estimatedSize
103 	 *            estimated size of storage used, to size the initial list of
104 	 *            block pointers.
105 	 * @param limit
106 	 *            maximum number of bytes to store in memory before entering the
107 	 *            overflow output path.
108 	 * @since 4.0
109 	 */
110 	protected TemporaryBuffer(final int estimatedSize, final int limit) {
111 		if (estimatedSize > limit)
112 			throw new IllegalArgumentException();
113 		this.inCoreLimit = limit;
114 		this.initialBlocks = (estimatedSize - 1) / Block.SZ + 1;
115 		reset();
116 	}
117 
118 	@Override
119 	public void write(final int b) throws IOException {
120 		if (overflow != null) {
121 			overflow.write(b);
122 			return;
123 		}
124 
125 		Block s = last();
126 		if (s.isFull()) {
127 			if (reachedInCoreLimit()) {
128 				overflow.write(b);
129 				return;
130 			}
131 
132 			s = new Block();
133 			blocks.add(s);
134 		}
135 		s.buffer[s.count++] = (byte) b;
136 	}
137 
138 	@Override
139 	public void write(final byte[] b, int off, int len) throws IOException {
140 		if (overflow == null) {
141 			while (len > 0) {
142 				Block s = last();
143 				if (s.isFull()) {
144 					if (reachedInCoreLimit())
145 						break;
146 
147 					s = new Block();
148 					blocks.add(s);
149 				}
150 
151 				final int n = Math.min(s.buffer.length - s.count, len);
152 				System.arraycopy(b, off, s.buffer, s.count, n);
153 				s.count += n;
154 				len -= n;
155 				off += n;
156 			}
157 		}
158 
159 		if (len > 0)
160 			overflow.write(b, off, len);
161 	}
162 
163 	/**
164 	 * Dumps the entire buffer into the overflow stream, and flushes it.
165 	 *
166 	 * @throws IOException
167 	 *             the overflow stream cannot be started, or the buffer contents
168 	 *             cannot be written to it, or it failed to flush.
169 	 */
170 	protected void doFlush() throws IOException {
171 		if (overflow == null)
172 			switchToOverflow();
173 		overflow.flush();
174 	}
175 
176 	/**
177 	 * Copy all bytes remaining on the input stream into this buffer.
178 	 *
179 	 * @param in
180 	 *            the stream to read from, until EOF is reached.
181 	 * @throws IOException
182 	 *             an error occurred reading from the input stream, or while
183 	 *             writing to a local temporary file.
184 	 */
185 	public void copy(final InputStream in) throws IOException {
186 		if (blocks != null) {
187 			for (;;) {
188 				Block s = last();
189 				if (s.isFull()) {
190 					if (reachedInCoreLimit())
191 						break;
192 					s = new Block();
193 					blocks.add(s);
194 				}
195 
196 				int n = in.read(s.buffer, s.count, s.buffer.length - s.count);
197 				if (n < 1)
198 					return;
199 				s.count += n;
200 			}
201 		}
202 
203 		final byte[] tmp = new byte[Block.SZ];
204 		int n;
205 		while ((n = in.read(tmp)) > 0)
206 			overflow.write(tmp, 0, n);
207 	}
208 
209 	/**
210 	 * Obtain the length (in bytes) of the buffer.
211 	 * <p>
212 	 * The length is only accurate after {@link #close()} has been invoked.
213 	 *
214 	 * @return total length of the buffer, in bytes.
215 	 */
216 	public long length() {
217 		return inCoreLength();
218 	}
219 
220 	private long inCoreLength() {
221 		final Block last = last();
222 		return ((long) blocks.size() - 1) * Block.SZ + last.count;
223 	}
224 
225 	/**
226 	 * Convert this buffer's contents into a contiguous byte array.
227 	 * <p>
228 	 * The buffer is only complete after {@link #close()} has been invoked.
229 	 *
230 	 * @return the complete byte array; length matches {@link #length()}.
231 	 * @throws IOException
232 	 *             an error occurred reading from a local temporary file
233 	 * @throws OutOfMemoryError
234 	 *             the buffer cannot fit in memory
235 	 */
236 	public byte[] toByteArray() throws IOException {
237 		final long len = length();
238 		if (Integer.MAX_VALUE < len)
239 			throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
240 		final byte[] out = new byte[(int) len];
241 		int outPtr = 0;
242 		for (final Block b : blocks) {
243 			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
244 			outPtr += b.count;
245 		}
246 		return out;
247 	}
248 
249 	/**
250 	 * Convert this buffer's contents into a contiguous byte array. If this size
251 	 * of the buffer exceeds the limit only return the first {@code limit} bytes
252 	 * <p>
253 	 * The buffer is only complete after {@link #close()} has been invoked.
254 	 *
255 	 * @param limit
256 	 *            the maximum number of bytes to be returned
257 	 *
258 	 * @return the byte array limited to {@code limit} bytes.
259 	 * @throws IOException
260 	 *             an error occurred reading from a local temporary file
261 	 * @throws OutOfMemoryError
262 	 *             the buffer cannot fit in memory
263 	 *
264 	 * @since 4.2
265 	 */
266 	public byte[] toByteArray(int limit) throws IOException {
267 		final long len = Math.min(length(), limit);
268 		if (Integer.MAX_VALUE < len)
269 			throw new OutOfMemoryError(
270 					JGitText.get().lengthExceedsMaximumArraySize);
271 		final byte[] out = new byte[(int) len];
272 		int outPtr = 0;
273 		for (final Block b : blocks) {
274 			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
275 			outPtr += b.count;
276 		}
277 		return out;
278 	}
279 
280 	/**
281 	 * Send this buffer to an output stream.
282 	 * <p>
283 	 * This method may only be invoked after {@link #close()} has completed
284 	 * normally, to ensure all data is completely transferred.
285 	 *
286 	 * @param os
287 	 *            stream to send this buffer's complete content to.
288 	 * @param pm
289 	 *            if not null progress updates are sent here. Caller should
290 	 *            initialize the task and the number of work units to <code>
291 	 *            {@link #length()}/1024</code>.
292 	 * @throws IOException
293 	 *             an error occurred reading from a temporary file on the local
294 	 *             system, or writing to the output stream.
295 	 */
296 	public void writeTo(final OutputStream os, ProgressMonitor pm)
297 			throws IOException {
298 		if (pm == null)
299 			pm = NullProgressMonitor.INSTANCE;
300 		for (final Block b : blocks) {
301 			os.write(b.buffer, 0, b.count);
302 			pm.update(b.count / 1024);
303 		}
304 	}
305 
306 	/**
307 	 * Open an input stream to read from the buffered data.
308 	 * <p>
309 	 * This method may only be invoked after {@link #close()} has completed
310 	 * normally, to ensure all data is completely transferred.
311 	 *
312 	 * @return a stream to read from the buffer. The caller must close the
313 	 *         stream when it is no longer useful.
314 	 * @throws IOException
315 	 *             an error occurred opening the temporary file.
316 	 */
317 	public InputStream openInputStream() throws IOException {
318 		return new BlockInputStream();
319 	}
320 
321 	/** Reset this buffer for reuse, purging all buffered content. */
322 	public void reset() {
323 		if (overflow != null) {
324 			destroy();
325 		}
326 		if (blocks != null)
327 			blocks.clear();
328 		else
329 			blocks = new ArrayList<Block>(initialBlocks);
330 		blocks.add(new Block(Math.min(inCoreLimit, Block.SZ)));
331 	}
332 
333 	/**
334 	 * Open the overflow output stream, so the remaining output can be stored.
335 	 *
336 	 * @return the output stream to receive the buffered content, followed by
337 	 *         the remaining output.
338 	 * @throws IOException
339 	 *             the buffer cannot create the overflow stream.
340 	 */
341 	protected abstract OutputStream overflow() throws IOException;
342 
343 	private Block last() {
344 		return blocks.get(blocks.size() - 1);
345 	}
346 
347 	private boolean reachedInCoreLimit() throws IOException {
348 		if (inCoreLength() < inCoreLimit)
349 			return false;
350 
351 		switchToOverflow();
352 		return true;
353 	}
354 
355 	private void switchToOverflow() throws IOException {
356 		overflow = overflow();
357 
358 		final Block last = blocks.remove(blocks.size() - 1);
359 		for (final Block b : blocks)
360 			overflow.write(b.buffer, 0, b.count);
361 		blocks = null;
362 
363 		overflow = new SafeBufferedOutputStream(overflow, Block.SZ);
364 		overflow.write(last.buffer, 0, last.count);
365 	}
366 
367 	public void close() throws IOException {
368 		if (overflow != null) {
369 			try {
370 				overflow.close();
371 			} finally {
372 				overflow = null;
373 			}
374 		}
375 	}
376 
377 	/** Clear this buffer so it has no data, and cannot be used again. */
378 	public void destroy() {
379 		blocks = null;
380 
381 		if (overflow != null) {
382 			try {
383 				overflow.close();
384 			} catch (IOException err) {
385 				// We shouldn't encounter an error closing the file.
386 			} finally {
387 				overflow = null;
388 			}
389 		}
390 	}
391 
392 	/**
393 	 * A fully buffered output stream using local disk storage for large data.
394 	 * <p>
395 	 * Initially this output stream buffers to memory and is therefore similar
396 	 * to ByteArrayOutputStream, but it shifts to using an on disk temporary
397 	 * file if the output gets too large.
398 	 * <p>
399 	 * The content of this buffered stream may be sent to another OutputStream
400 	 * only after this stream has been properly closed by {@link #close()}.
401 	 */
402 	public static class LocalFile extends TemporaryBuffer {
403 		/** Directory to store the temporary file under. */
404 		private final File directory;
405 
406 		/**
407 		 * Location of our temporary file if we are on disk; otherwise null.
408 		 * <p>
409 		 * If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks}
410 		 * and created this file instead. All output goes here through
411 		 * {@link #overflow}.
412 		 */
413 		private File onDiskFile;
414 
415 		/**
416 		 * Create a new temporary buffer, limiting memory usage.
417 		 *
418 		 * @param directory
419 		 *            if the buffer has to spill over into a temporary file, the
420 		 *            directory where the file should be saved. If null the
421 		 *            system default temporary directory (for example /tmp) will
422 		 *            be used instead.
423 		 */
424 		public LocalFile(final File directory) {
425 			this(directory, DEFAULT_IN_CORE_LIMIT);
426 		}
427 
428 		/**
429 		 * Create a new temporary buffer, limiting memory usage.
430 		 *
431 		 * @param directory
432 		 *            if the buffer has to spill over into a temporary file, the
433 		 *            directory where the file should be saved. If null the
434 		 *            system default temporary directory (for example /tmp) will
435 		 *            be used instead.
436 		 * @param inCoreLimit
437 		 *            maximum number of bytes to store in memory. Storage beyond
438 		 *            this limit will use the local file.
439 		 */
440 		public LocalFile(final File directory, final int inCoreLimit) {
441 			super(inCoreLimit);
442 			this.directory = directory;
443 		}
444 
445 		protected OutputStream overflow() throws IOException {
446 			onDiskFile = File.createTempFile("jgit_", ".buf", directory); //$NON-NLS-1$ //$NON-NLS-2$
447 			return new BufferedOutputStream(new FileOutputStream(onDiskFile));
448 		}
449 
450 		public long length() {
451 			if (onDiskFile == null) {
452 				return super.length();
453 			}
454 			return onDiskFile.length();
455 		}
456 
457 		public byte[] toByteArray() throws IOException {
458 			if (onDiskFile == null) {
459 				return super.toByteArray();
460 			}
461 
462 			final long len = length();
463 			if (Integer.MAX_VALUE < len)
464 				throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
465 			final byte[] out = new byte[(int) len];
466 			final FileInputStream in = new FileInputStream(onDiskFile);
467 			try {
468 				IO.readFully(in, out, 0, (int) len);
469 			} finally {
470 				in.close();
471 			}
472 			return out;
473 		}
474 
475 		public void writeTo(final OutputStream os, ProgressMonitor pm)
476 				throws IOException {
477 			if (onDiskFile == null) {
478 				super.writeTo(os, pm);
479 				return;
480 			}
481 			if (pm == null)
482 				pm = NullProgressMonitor.INSTANCE;
483 			final FileInputStream in = new FileInputStream(onDiskFile);
484 			try {
485 				int cnt;
486 				final byte[] buf = new byte[Block.SZ];
487 				while ((cnt = in.read(buf)) >= 0) {
488 					os.write(buf, 0, cnt);
489 					pm.update(cnt / 1024);
490 				}
491 			} finally {
492 				in.close();
493 			}
494 		}
495 
496 		@Override
497 		public InputStream openInputStream() throws IOException {
498 			if (onDiskFile == null)
499 				return super.openInputStream();
500 			return new FileInputStream(onDiskFile);
501 		}
502 
503 		@Override
504 		public void destroy() {
505 			super.destroy();
506 
507 			if (onDiskFile != null) {
508 				try {
509 					if (!onDiskFile.delete())
510 						onDiskFile.deleteOnExit();
511 				} finally {
512 					onDiskFile = null;
513 				}
514 			}
515 		}
516 	}
517 
518 	/**
519 	 * A temporary buffer that will never exceed its in-memory limit.
520 	 * <p>
521 	 * If the in-memory limit is reached an IOException is thrown, rather than
522 	 * attempting to spool to local disk.
523 	 */
524 	public static class Heap extends TemporaryBuffer {
525 		/**
526 		 * Create a new heap buffer with a maximum storage limit.
527 		 *
528 		 * @param limit
529 		 *            maximum number of bytes that can be stored in this buffer;
530 		 *            also used as the estimated size. Storing beyond this many
531 		 *            will cause an IOException to be thrown during write.
532 		 */
533 		public Heap(final int limit) {
534 			super(limit);
535 		}
536 
537 		/**
538 		 * Create a new heap buffer with a maximum storage limit.
539 		 *
540 		 * @param estimatedSize
541 		 *            estimated size of storage used, to size the initial list of
542 		 *            block pointers.
543 		 * @param limit
544 		 *            maximum number of bytes that can be stored in this buffer.
545 		 *            Storing beyond this many will cause an IOException to be
546 		 *            thrown during write.
547 		 * @since 4.0
548 		 */
549 		public Heap(final int estimatedSize, final int limit) {
550 			super(estimatedSize, limit);
551 		}
552 
553 		@Override
554 		protected OutputStream overflow() throws IOException {
555 			throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
556 		}
557 	}
558 
559 	static class Block {
560 		static final int SZ = 8 * 1024;
561 
562 		final byte[] buffer;
563 
564 		int count;
565 
566 		Block() {
567 			buffer = new byte[SZ];
568 		}
569 
570 		Block(int sz) {
571 			buffer = new byte[sz];
572 		}
573 
574 		boolean isFull() {
575 			return count == buffer.length;
576 		}
577 	}
578 
579 	private class BlockInputStream extends InputStream {
580 		private byte[] singleByteBuffer;
581 		private int blockIndex;
582 		private Block block;
583 		private int blockPos;
584 
585 		BlockInputStream() {
586 			block = blocks.get(blockIndex);
587 		}
588 
589 		@Override
590 		public int read() throws IOException {
591 			if (singleByteBuffer == null)
592 				singleByteBuffer = new byte[1];
593 			int n = read(singleByteBuffer);
594 			return n == 1 ? singleByteBuffer[0] & 0xff : -1;
595 		}
596 
597 		@Override
598 		public long skip(long cnt) throws IOException {
599 			long skipped = 0;
600 			while (0 < cnt) {
601 				int n = (int) Math.min(block.count - blockPos, cnt);
602 				if (0 < n) {
603 					blockPos += n;
604 					skipped += n;
605 					cnt -= n;
606 				} else if (nextBlock())
607 					continue;
608 				else
609 					break;
610 			}
611 			return skipped;
612 		}
613 
614 		@Override
615 		public int read(byte[] b, int off, int len) throws IOException {
616 			if (len == 0)
617 				return 0;
618 			int copied = 0;
619 			while (0 < len) {
620 				int c = Math.min(block.count - blockPos, len);
621 				if (0 < c) {
622 					System.arraycopy(block.buffer, blockPos, b, off, c);
623 					blockPos += c;
624 					off += c;
625 					len -= c;
626 					copied += c;
627 				} else if (nextBlock())
628 					continue;
629 				else
630 					break;
631 			}
632 			return 0 < copied ? copied : -1;
633 		}
634 
635 		private boolean nextBlock() {
636 			if (++blockIndex < blocks.size()) {
637 				block = blocks.get(blockIndex);
638 				blockPos = 0;
639 				return true;
640 			}
641 			return false;
642 		}
643 	}
644 }