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  
60  /**
61   * A fully buffered output stream.
62   * <p>
63   * Subclasses determine the behavior when the in-memory buffer capacity has been
64   * exceeded and additional bytes are still being received for output.
65   */
66  public abstract class TemporaryBuffer extends OutputStream {
67  	/** Default limit for in-core storage. */
68  	protected static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024;
69  
70  	/** Chain of data, if we are still completely in-core; otherwise null. */
71  	ArrayList<Block> blocks;
72  
73  	/**
74  	 * Maximum number of bytes we will permit storing in memory.
75  	 * <p>
76  	 * When this limit is reached the data will be shifted to a file on disk,
77  	 * preventing the JVM heap from growing out of control.
78  	 */
79  	private int inCoreLimit;
80  
81  	/** Initial size of block list. */
82  	private int initialBlocks;
83  
84  	/** If {@link #inCoreLimit} has been reached, remainder goes here. */
85  	private OutputStream overflow;
86  
87  	/**
88  	 * Create a new empty temporary buffer.
89  	 *
90  	 * @param limit
91  	 *            maximum number of bytes to store in memory before entering the
92  	 *            overflow output path; also used as the estimated size.
93  	 */
94  	protected TemporaryBuffer(int limit) {
95  		this(limit, limit);
96  	}
97  
98  	/**
99  	 * Create a new empty temporary buffer.
100 	 *
101 	 * @param estimatedSize
102 	 *            estimated size of storage used, to size the initial list of
103 	 *            block pointers.
104 	 * @param limit
105 	 *            maximum number of bytes to store in memory before entering the
106 	 *            overflow output path.
107 	 * @since 4.0
108 	 */
109 	protected TemporaryBuffer(int estimatedSize, int limit) {
110 		if (estimatedSize > limit)
111 			throw new IllegalArgumentException();
112 		this.inCoreLimit = limit;
113 		this.initialBlocks = (estimatedSize - 1) / Block.SZ + 1;
114 		reset();
115 	}
116 
117 	/** {@inheritDoc} */
118 	@Override
119 	public void write(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 	/** {@inheritDoc} */
139 	@Override
140 	public void write(byte[] b, int off, int len) throws IOException {
141 		if (overflow == null) {
142 			while (len > 0) {
143 				Block s = last();
144 				if (s.isFull()) {
145 					if (reachedInCoreLimit())
146 						break;
147 
148 					s = new Block();
149 					blocks.add(s);
150 				}
151 
152 				final int n = Math.min(s.buffer.length - s.count, len);
153 				System.arraycopy(b, off, s.buffer, s.count, n);
154 				s.count += n;
155 				len -= n;
156 				off += n;
157 			}
158 		}
159 
160 		if (len > 0)
161 			overflow.write(b, off, len);
162 	}
163 
164 	/**
165 	 * Dumps the entire buffer into the overflow stream, and flushes it.
166 	 *
167 	 * @throws java.io.IOException
168 	 *             the overflow stream cannot be started, or the buffer contents
169 	 *             cannot be written to it, or it failed to flush.
170 	 */
171 	protected void doFlush() throws IOException {
172 		if (overflow == null)
173 			switchToOverflow();
174 		overflow.flush();
175 	}
176 
177 	/**
178 	 * Copy all bytes remaining on the input stream into this buffer.
179 	 *
180 	 * @param in
181 	 *            the stream to read from, until EOF is reached.
182 	 * @throws java.io.IOException
183 	 *             an error occurred reading from the input stream, or while
184 	 *             writing to a local temporary file.
185 	 */
186 	public void copy(InputStream in) throws IOException {
187 		if (blocks != null) {
188 			for (;;) {
189 				Block s = last();
190 				if (s.isFull()) {
191 					if (reachedInCoreLimit())
192 						break;
193 					s = new Block();
194 					blocks.add(s);
195 				}
196 
197 				int n = in.read(s.buffer, s.count, s.buffer.length - s.count);
198 				if (n < 1)
199 					return;
200 				s.count += n;
201 			}
202 		}
203 
204 		final byte[] tmp = new byte[Block.SZ];
205 		int n;
206 		while ((n = in.read(tmp)) > 0)
207 			overflow.write(tmp, 0, n);
208 	}
209 
210 	/**
211 	 * Obtain the length (in bytes) of the buffer.
212 	 * <p>
213 	 * The length is only accurate after {@link #close()} has been invoked.
214 	 *
215 	 * @return total length of the buffer, in bytes.
216 	 */
217 	public long length() {
218 		return inCoreLength();
219 	}
220 
221 	private long inCoreLength() {
222 		final Block last = last();
223 		return ((long) blocks.size() - 1) * Block.SZ + last.count;
224 	}
225 
226 	/**
227 	 * Convert this buffer's contents into a contiguous byte array.
228 	 * <p>
229 	 * The buffer is only complete after {@link #close()} has been invoked.
230 	 *
231 	 * @return the complete byte array; length matches {@link #length()}.
232 	 * @throws java.io.IOException
233 	 *             an error occurred reading from a local temporary file
234 	 */
235 	public byte[] toByteArray() throws IOException {
236 		final long len = length();
237 		if (Integer.MAX_VALUE < len)
238 			throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
239 		final byte[] out = new byte[(int) len];
240 		int outPtr = 0;
241 		for (Block b : blocks) {
242 			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
243 			outPtr += b.count;
244 		}
245 		return out;
246 	}
247 
248 	/**
249 	 * Convert this buffer's contents into a contiguous byte array. If this size
250 	 * of the buffer exceeds the limit only return the first {@code limit} bytes
251 	 * <p>
252 	 * The buffer is only complete after {@link #close()} has been invoked.
253 	 *
254 	 * @param limit
255 	 *            the maximum number of bytes to be returned
256 	 * @return the byte array limited to {@code limit} bytes.
257 	 * @throws java.io.IOException
258 	 *             an error occurred reading from a local temporary file
259 	 * @since 4.2
260 	 */
261 	public byte[] toByteArray(int limit) throws IOException {
262 		final long len = Math.min(length(), limit);
263 		if (Integer.MAX_VALUE < len)
264 			throw new OutOfMemoryError(
265 					JGitText.get().lengthExceedsMaximumArraySize);
266 		final byte[] out = new byte[(int) len];
267 		int outPtr = 0;
268 		for (Block b : blocks) {
269 			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
270 			outPtr += b.count;
271 		}
272 		return out;
273 	}
274 
275 	/**
276 	 * Send this buffer to an output stream.
277 	 * <p>
278 	 * This method may only be invoked after {@link #close()} has completed
279 	 * normally, to ensure all data is completely transferred.
280 	 *
281 	 * @param os
282 	 *            stream to send this buffer's complete content to.
283 	 * @param pm
284 	 *            if not null progress updates are sent here. Caller should
285 	 *            initialize the task and the number of work units to <code>
286 	 *            {@link #length()}/1024</code>.
287 	 * @throws java.io.IOException
288 	 *             an error occurred reading from a temporary file on the local
289 	 *             system, or writing to the output stream.
290 	 */
291 	public void writeTo(OutputStream os, ProgressMonitor pm)
292 			throws IOException {
293 		if (pm == null)
294 			pm = NullProgressMonitor.INSTANCE;
295 		for (Block b : blocks) {
296 			os.write(b.buffer, 0, b.count);
297 			pm.update(b.count / 1024);
298 		}
299 	}
300 
301 	/**
302 	 * Open an input stream to read from the buffered data.
303 	 * <p>
304 	 * This method may only be invoked after {@link #close()} has completed
305 	 * normally, to ensure all data is completely transferred.
306 	 *
307 	 * @return a stream to read from the buffer. The caller must close the
308 	 *         stream when it is no longer useful.
309 	 * @throws java.io.IOException
310 	 *             an error occurred opening the temporary file.
311 	 */
312 	public InputStream openInputStream() throws IOException {
313 		return new BlockInputStream();
314 	}
315 
316 	/**
317 	 * Same as {@link #openInputStream()} but handling destruction of any
318 	 * associated resources automatically when closing the returned stream.
319 	 *
320 	 * @return an InputStream which will automatically destroy any associated
321 	 *         temporary file on {@link #close()}
322 	 * @throws IOException
323 	 *             in case of an error.
324 	 * @since 4.11
325 	 */
326 	public InputStream openInputStreamWithAutoDestroy() throws IOException {
327 		return new BlockInputStream() {
328 			@Override
329 			public void close() throws IOException {
330 				super.close();
331 				destroy();
332 			}
333 		};
334 	}
335 
336 	/**
337 	 * Reset this buffer for reuse, purging all buffered content.
338 	 */
339 	public void reset() {
340 		if (overflow != null) {
341 			destroy();
342 		}
343 		if (blocks != null)
344 			blocks.clear();
345 		else
346 			blocks = new ArrayList<>(initialBlocks);
347 		blocks.add(new Block(Math.min(inCoreLimit, Block.SZ)));
348 	}
349 
350 	/**
351 	 * Open the overflow output stream, so the remaining output can be stored.
352 	 *
353 	 * @return the output stream to receive the buffered content, followed by
354 	 *         the remaining output.
355 	 * @throws java.io.IOException
356 	 *             the buffer cannot create the overflow stream.
357 	 */
358 	protected abstract OutputStream overflow() throws IOException;
359 
360 	private Block last() {
361 		return blocks.get(blocks.size() - 1);
362 	}
363 
364 	private boolean reachedInCoreLimit() throws IOException {
365 		if (inCoreLength() < inCoreLimit)
366 			return false;
367 
368 		switchToOverflow();
369 		return true;
370 	}
371 
372 	private void switchToOverflow() throws IOException {
373 		overflow = overflow();
374 
375 		final Block last = blocks.remove(blocks.size() - 1);
376 		for (Block b : blocks)
377 			overflow.write(b.buffer, 0, b.count);
378 		blocks = null;
379 
380 		overflow = new BufferedOutputStream(overflow, Block.SZ);
381 		overflow.write(last.buffer, 0, last.count);
382 	}
383 
384 	/** {@inheritDoc} */
385 	@Override
386 	public void close() throws IOException {
387 		if (overflow != null) {
388 			try {
389 				overflow.close();
390 			} finally {
391 				overflow = null;
392 			}
393 		}
394 	}
395 
396 	/**
397 	 * Clear this buffer so it has no data, and cannot be used again.
398 	 */
399 	public void destroy() {
400 		blocks = null;
401 
402 		if (overflow != null) {
403 			try {
404 				overflow.close();
405 			} catch (IOException err) {
406 				// We shouldn't encounter an error closing the file.
407 			} finally {
408 				overflow = null;
409 			}
410 		}
411 	}
412 
413 	/**
414 	 * A fully buffered output stream using local disk storage for large data.
415 	 * <p>
416 	 * Initially this output stream buffers to memory and is therefore similar
417 	 * to ByteArrayOutputStream, but it shifts to using an on disk temporary
418 	 * file if the output gets too large.
419 	 * <p>
420 	 * The content of this buffered stream may be sent to another OutputStream
421 	 * only after this stream has been properly closed by {@link #close()}.
422 	 */
423 	public static class LocalFile extends TemporaryBuffer {
424 		/** Directory to store the temporary file under. */
425 		private final File directory;
426 
427 		/**
428 		 * Location of our temporary file if we are on disk; otherwise null.
429 		 * <p>
430 		 * If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks}
431 		 * and created this file instead. All output goes here through
432 		 * {@link #overflow}.
433 		 */
434 		private File onDiskFile;
435 
436 		/**
437 		 * Create a new temporary buffer, limiting memory usage.
438 		 *
439 		 * @param directory
440 		 *            if the buffer has to spill over into a temporary file, the
441 		 *            directory where the file should be saved. If null the
442 		 *            system default temporary directory (for example /tmp) will
443 		 *            be used instead.
444 		 */
445 		public LocalFile(File directory) {
446 			this(directory, DEFAULT_IN_CORE_LIMIT);
447 		}
448 
449 		/**
450 		 * Create a new temporary buffer, limiting memory usage.
451 		 *
452 		 * @param directory
453 		 *            if the buffer has to spill over into a temporary file, the
454 		 *            directory where the file should be saved. If null the
455 		 *            system default temporary directory (for example /tmp) will
456 		 *            be used instead.
457 		 * @param inCoreLimit
458 		 *            maximum number of bytes to store in memory. Storage beyond
459 		 *            this limit will use the local file.
460 		 */
461 		public LocalFile(File directory, int inCoreLimit) {
462 			super(inCoreLimit);
463 			this.directory = directory;
464 		}
465 
466 		@Override
467 		protected OutputStream overflow() throws IOException {
468 			onDiskFile = File.createTempFile("jgit_", ".buf", directory); //$NON-NLS-1$ //$NON-NLS-2$
469 			return new BufferedOutputStream(new FileOutputStream(onDiskFile));
470 		}
471 
472 		@Override
473 		public long length() {
474 			if (onDiskFile == null) {
475 				return super.length();
476 			}
477 			return onDiskFile.length();
478 		}
479 
480 		@Override
481 		public byte[] toByteArray() throws IOException {
482 			if (onDiskFile == null) {
483 				return super.toByteArray();
484 			}
485 
486 			final long len = length();
487 			if (Integer.MAX_VALUE < len)
488 				throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
489 			final byte[] out = new byte[(int) len];
490 			try (FileInputStream in = new FileInputStream(onDiskFile)) {
491 				IO.readFully(in, out, 0, (int) len);
492 			}
493 			return out;
494 		}
495 
496 		@Override
497 		public void writeTo(OutputStream os, ProgressMonitor pm)
498 				throws IOException {
499 			if (onDiskFile == null) {
500 				super.writeTo(os, pm);
501 				return;
502 			}
503 			if (pm == null)
504 				pm = NullProgressMonitor.INSTANCE;
505 			try (FileInputStream in = new FileInputStream(onDiskFile)) {
506 				int cnt;
507 				final byte[] buf = new byte[Block.SZ];
508 				while ((cnt = in.read(buf)) >= 0) {
509 					os.write(buf, 0, cnt);
510 					pm.update(cnt / 1024);
511 				}
512 			}
513 		}
514 
515 		@Override
516 		public InputStream openInputStream() throws IOException {
517 			if (onDiskFile == null)
518 				return super.openInputStream();
519 			return new FileInputStream(onDiskFile);
520 		}
521 
522 		@Override
523 		public InputStream openInputStreamWithAutoDestroy() throws IOException {
524 			if (onDiskFile == null) {
525 				return super.openInputStreamWithAutoDestroy();
526 			}
527 			return new FileInputStream(onDiskFile) {
528 				@Override
529 				public void close() throws IOException {
530 					super.close();
531 					destroy();
532 				}
533 			};
534 		}
535 
536 		@Override
537 		public void destroy() {
538 			super.destroy();
539 
540 			if (onDiskFile != null) {
541 				try {
542 					if (!onDiskFile.delete())
543 						onDiskFile.deleteOnExit();
544 				} finally {
545 					onDiskFile = null;
546 				}
547 			}
548 		}
549 	}
550 
551 	/**
552 	 * A temporary buffer that will never exceed its in-memory limit.
553 	 * <p>
554 	 * If the in-memory limit is reached an IOException is thrown, rather than
555 	 * attempting to spool to local disk.
556 	 */
557 	public static class Heap extends TemporaryBuffer {
558 		/**
559 		 * Create a new heap buffer with a maximum storage limit.
560 		 *
561 		 * @param limit
562 		 *            maximum number of bytes that can be stored in this buffer;
563 		 *            also used as the estimated size. Storing beyond this many
564 		 *            will cause an IOException to be thrown during write.
565 		 */
566 		public Heap(int limit) {
567 			super(limit);
568 		}
569 
570 		/**
571 		 * Create a new heap buffer with a maximum storage limit.
572 		 *
573 		 * @param estimatedSize
574 		 *            estimated size of storage used, to size the initial list of
575 		 *            block pointers.
576 		 * @param limit
577 		 *            maximum number of bytes that can be stored in this buffer.
578 		 *            Storing beyond this many will cause an IOException to be
579 		 *            thrown during write.
580 		 * @since 4.0
581 		 */
582 		public Heap(int estimatedSize, int limit) {
583 			super(estimatedSize, limit);
584 		}
585 
586 		@Override
587 		protected OutputStream overflow() throws IOException {
588 			throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
589 		}
590 	}
591 
592 	static class Block {
593 		static final int SZ = 8 * 1024;
594 
595 		final byte[] buffer;
596 
597 		int count;
598 
599 		Block() {
600 			buffer = new byte[SZ];
601 		}
602 
603 		Block(int sz) {
604 			buffer = new byte[sz];
605 		}
606 
607 		boolean isFull() {
608 			return count == buffer.length;
609 		}
610 	}
611 
612 	private class BlockInputStream extends InputStream {
613 		private byte[] singleByteBuffer;
614 		private int blockIndex;
615 		private Block block;
616 		private int blockPos;
617 
618 		BlockInputStream() {
619 			block = blocks.get(blockIndex);
620 		}
621 
622 		@Override
623 		public int read() throws IOException {
624 			if (singleByteBuffer == null)
625 				singleByteBuffer = new byte[1];
626 			int n = read(singleByteBuffer);
627 			return n == 1 ? singleByteBuffer[0] & 0xff : -1;
628 		}
629 
630 		@Override
631 		public long skip(long cnt) throws IOException {
632 			long skipped = 0;
633 			while (0 < cnt) {
634 				int n = (int) Math.min(block.count - blockPos, cnt);
635 				if (0 < n) {
636 					blockPos += n;
637 					skipped += n;
638 					cnt -= n;
639 				} else if (nextBlock())
640 					continue;
641 				else
642 					break;
643 			}
644 			return skipped;
645 		}
646 
647 		@Override
648 		public int read(byte[] b, int off, int len) throws IOException {
649 			if (len == 0)
650 				return 0;
651 			int copied = 0;
652 			while (0 < len) {
653 				int c = Math.min(block.count - blockPos, len);
654 				if (0 < c) {
655 					System.arraycopy(block.buffer, blockPos, b, off, c);
656 					blockPos += c;
657 					off += c;
658 					len -= c;
659 					copied += c;
660 				} else if (nextBlock())
661 					continue;
662 				else
663 					break;
664 			}
665 			return 0 < copied ? copied : -1;
666 		}
667 
668 		private boolean nextBlock() {
669 			if (++blockIndex < blocks.size()) {
670 				block = blocks.get(blockIndex);
671 				blockPos = 0;
672 				return true;
673 			}
674 			return false;
675 		}
676 	}
677 }