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