TemporaryBuffer.java

  1. /*
  2.  * Copyright (C) 2008-2009, Google Inc.
  3.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. package org.eclipse.jgit.util;

  12. import java.io.BufferedOutputStream;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.FileOutputStream;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.OutputStream;
  19. import java.util.ArrayList;

  20. import org.eclipse.jgit.internal.JGitText;
  21. import org.eclipse.jgit.lib.NullProgressMonitor;
  22. import org.eclipse.jgit.lib.ProgressMonitor;

  23. /**
  24.  * A fully buffered output stream.
  25.  * <p>
  26.  * Subclasses determine the behavior when the in-memory buffer capacity has been
  27.  * exceeded and additional bytes are still being received for output.
  28.  */
  29. public abstract class TemporaryBuffer extends OutputStream {
  30.     /** Default limit for in-core storage. */
  31.     protected static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024;

  32.     /** Chain of data, if we are still completely in-core; otherwise null. */
  33.     ArrayList<Block> blocks;

  34.     /**
  35.      * Maximum number of bytes we will permit storing in memory.
  36.      * <p>
  37.      * When this limit is reached the data will be shifted to a file on disk,
  38.      * preventing the JVM heap from growing out of control.
  39.      */
  40.     private int inCoreLimit;

  41.     /** Initial size of block list. */
  42.     private int initialBlocks;

  43.     /** If {@link #inCoreLimit} has been reached, remainder goes here. */
  44.     private OutputStream overflow;

  45.     /**
  46.      * Create a new empty temporary buffer.
  47.      *
  48.      * @param limit
  49.      *            maximum number of bytes to store in memory before entering the
  50.      *            overflow output path; also used as the estimated size.
  51.      */
  52.     protected TemporaryBuffer(int limit) {
  53.         this(limit, limit);
  54.     }

  55.     /**
  56.      * Create a new empty temporary buffer.
  57.      *
  58.      * @param estimatedSize
  59.      *            estimated size of storage used, to size the initial list of
  60.      *            block pointers.
  61.      * @param limit
  62.      *            maximum number of bytes to store in memory before entering the
  63.      *            overflow output path.
  64.      * @since 4.0
  65.      */
  66.     protected TemporaryBuffer(int estimatedSize, int limit) {
  67.         if (estimatedSize > limit)
  68.             throw new IllegalArgumentException();
  69.         this.inCoreLimit = limit;
  70.         this.initialBlocks = (estimatedSize - 1) / Block.SZ + 1;
  71.         reset();
  72.     }

  73.     /** {@inheritDoc} */
  74.     @Override
  75.     public void write(int b) throws IOException {
  76.         if (overflow != null) {
  77.             overflow.write(b);
  78.             return;
  79.         }

  80.         Block s = last();
  81.         if (s.isFull()) {
  82.             if (reachedInCoreLimit()) {
  83.                 overflow.write(b);
  84.                 return;
  85.             }

  86.             s = new Block();
  87.             blocks.add(s);
  88.         }
  89.         s.buffer[s.count++] = (byte) b;
  90.     }

  91.     /** {@inheritDoc} */
  92.     @Override
  93.     public void write(byte[] b, int off, int len) throws IOException {
  94.         if (overflow == null) {
  95.             while (len > 0) {
  96.                 Block s = last();
  97.                 if (s.isFull()) {
  98.                     if (reachedInCoreLimit())
  99.                         break;

  100.                     s = new Block();
  101.                     blocks.add(s);
  102.                 }

  103.                 final int n = Math.min(s.buffer.length - s.count, len);
  104.                 System.arraycopy(b, off, s.buffer, s.count, n);
  105.                 s.count += n;
  106.                 len -= n;
  107.                 off += n;
  108.             }
  109.         }

  110.         if (len > 0)
  111.             overflow.write(b, off, len);
  112.     }

  113.     /**
  114.      * Dumps the entire buffer into the overflow stream, and flushes it.
  115.      *
  116.      * @throws java.io.IOException
  117.      *             the overflow stream cannot be started, or the buffer contents
  118.      *             cannot be written to it, or it failed to flush.
  119.      */
  120.     protected void doFlush() throws IOException {
  121.         if (overflow == null)
  122.             switchToOverflow();
  123.         overflow.flush();
  124.     }

  125.     /**
  126.      * Copy all bytes remaining on the input stream into this buffer.
  127.      *
  128.      * @param in
  129.      *            the stream to read from, until EOF is reached.
  130.      * @throws java.io.IOException
  131.      *             an error occurred reading from the input stream, or while
  132.      *             writing to a local temporary file.
  133.      */
  134.     public void copy(InputStream in) throws IOException {
  135.         if (blocks != null) {
  136.             for (;;) {
  137.                 Block s = last();
  138.                 if (s.isFull()) {
  139.                     if (reachedInCoreLimit())
  140.                         break;
  141.                     s = new Block();
  142.                     blocks.add(s);
  143.                 }

  144.                 int n = in.read(s.buffer, s.count, s.buffer.length - s.count);
  145.                 if (n < 1)
  146.                     return;
  147.                 s.count += n;
  148.             }
  149.         }

  150.         final byte[] tmp = new byte[Block.SZ];
  151.         int n;
  152.         while ((n = in.read(tmp)) > 0)
  153.             overflow.write(tmp, 0, n);
  154.     }

  155.     /**
  156.      * Obtain the length (in bytes) of the buffer.
  157.      * <p>
  158.      * The length is only accurate after {@link #close()} has been invoked.
  159.      *
  160.      * @return total length of the buffer, in bytes.
  161.      */
  162.     public long length() {
  163.         return inCoreLength();
  164.     }

  165.     private long inCoreLength() {
  166.         final Block last = last();
  167.         return ((long) blocks.size() - 1) * Block.SZ + last.count;
  168.     }

  169.     /**
  170.      * Convert this buffer's contents into a contiguous byte array.
  171.      * <p>
  172.      * The buffer is only complete after {@link #close()} has been invoked.
  173.      *
  174.      * @return the complete byte array; length matches {@link #length()}.
  175.      * @throws java.io.IOException
  176.      *             an error occurred reading from a local temporary file
  177.      */
  178.     public byte[] toByteArray() throws IOException {
  179.         final long len = length();
  180.         if (Integer.MAX_VALUE < len)
  181.             throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
  182.         final byte[] out = new byte[(int) len];
  183.         int outPtr = 0;
  184.         for (Block b : blocks) {
  185.             System.arraycopy(b.buffer, 0, out, outPtr, b.count);
  186.             outPtr += b.count;
  187.         }
  188.         return out;
  189.     }

  190.     /**
  191.      * Convert this buffer's contents into a contiguous byte array. If this size
  192.      * of the buffer exceeds the limit only return the first {@code limit} bytes
  193.      * <p>
  194.      * The buffer is only complete after {@link #close()} has been invoked.
  195.      *
  196.      * @param limit
  197.      *            the maximum number of bytes to be returned
  198.      * @return the byte array limited to {@code limit} bytes.
  199.      * @throws java.io.IOException
  200.      *             an error occurred reading from a local temporary file
  201.      * @since 4.2
  202.      */
  203.     public byte[] toByteArray(int limit) throws IOException {
  204.         final long len = Math.min(length(), limit);
  205.         if (Integer.MAX_VALUE < len)
  206.             throw new OutOfMemoryError(
  207.                     JGitText.get().lengthExceedsMaximumArraySize);
  208.         int length = (int) len;
  209.         final byte[] out = new byte[length];
  210.         int outPtr = 0;
  211.         for (Block b : blocks) {
  212.             int toCopy = Math.min(length - outPtr, b.count);
  213.             System.arraycopy(b.buffer, 0, out, outPtr, toCopy);
  214.             outPtr += toCopy;
  215.             if (outPtr == length) {
  216.                 break;
  217.             }
  218.         }
  219.         return out;
  220.     }

  221.     /**
  222.      * Send this buffer to an output stream.
  223.      * <p>
  224.      * This method may only be invoked after {@link #close()} has completed
  225.      * normally, to ensure all data is completely transferred.
  226.      *
  227.      * @param os
  228.      *            stream to send this buffer's complete content to.
  229.      * @param pm
  230.      *            if not null progress updates are sent here. Caller should
  231.      *            initialize the task and the number of work units to <code>
  232.      *            {@link #length()}/1024</code>.
  233.      * @throws java.io.IOException
  234.      *             an error occurred reading from a temporary file on the local
  235.      *             system, or writing to the output stream.
  236.      */
  237.     public void writeTo(OutputStream os, ProgressMonitor pm)
  238.             throws IOException {
  239.         if (pm == null)
  240.             pm = NullProgressMonitor.INSTANCE;
  241.         for (Block b : blocks) {
  242.             os.write(b.buffer, 0, b.count);
  243.             pm.update(b.count / 1024);
  244.         }
  245.     }

  246.     /**
  247.      * Open an input stream to read from the buffered data.
  248.      * <p>
  249.      * This method may only be invoked after {@link #close()} has completed
  250.      * normally, to ensure all data is completely transferred.
  251.      *
  252.      * @return a stream to read from the buffer. The caller must close the
  253.      *         stream when it is no longer useful.
  254.      * @throws java.io.IOException
  255.      *             an error occurred opening the temporary file.
  256.      */
  257.     public InputStream openInputStream() throws IOException {
  258.         return new BlockInputStream();
  259.     }

  260.     /**
  261.      * Same as {@link #openInputStream()} but handling destruction of any
  262.      * associated resources automatically when closing the returned stream.
  263.      *
  264.      * @return an InputStream which will automatically destroy any associated
  265.      *         temporary file on {@link #close()}
  266.      * @throws IOException
  267.      *             in case of an error.
  268.      * @since 4.11
  269.      */
  270.     public InputStream openInputStreamWithAutoDestroy() throws IOException {
  271.         return new BlockInputStream() {
  272.             @Override
  273.             public void close() throws IOException {
  274.                 super.close();
  275.                 destroy();
  276.             }
  277.         };
  278.     }

  279.     /**
  280.      * Reset this buffer for reuse, purging all buffered content.
  281.      */
  282.     public void reset() {
  283.         if (overflow != null) {
  284.             destroy();
  285.         }
  286.         if (blocks != null)
  287.             blocks.clear();
  288.         else
  289.             blocks = new ArrayList<>(initialBlocks);
  290.         blocks.add(new Block(Math.min(inCoreLimit, Block.SZ)));
  291.     }

  292.     /**
  293.      * Open the overflow output stream, so the remaining output can be stored.
  294.      *
  295.      * @return the output stream to receive the buffered content, followed by
  296.      *         the remaining output.
  297.      * @throws java.io.IOException
  298.      *             the buffer cannot create the overflow stream.
  299.      */
  300.     protected abstract OutputStream overflow() throws IOException;

  301.     private Block last() {
  302.         return blocks.get(blocks.size() - 1);
  303.     }

  304.     private boolean reachedInCoreLimit() throws IOException {
  305.         if (inCoreLength() < inCoreLimit)
  306.             return false;

  307.         switchToOverflow();
  308.         return true;
  309.     }

  310.     private void switchToOverflow() throws IOException {
  311.         overflow = overflow();

  312.         final Block last = blocks.remove(blocks.size() - 1);
  313.         for (Block b : blocks)
  314.             overflow.write(b.buffer, 0, b.count);
  315.         blocks = null;

  316.         overflow = new BufferedOutputStream(overflow, Block.SZ);
  317.         overflow.write(last.buffer, 0, last.count);
  318.     }

  319.     /** {@inheritDoc} */
  320.     @Override
  321.     public void close() throws IOException {
  322.         if (overflow != null) {
  323.             try {
  324.                 overflow.close();
  325.             } finally {
  326.                 overflow = null;
  327.             }
  328.         }
  329.     }

  330.     /**
  331.      * Clear this buffer so it has no data, and cannot be used again.
  332.      */
  333.     public void destroy() {
  334.         blocks = null;

  335.         if (overflow != null) {
  336.             try {
  337.                 overflow.close();
  338.             } catch (IOException err) {
  339.                 // We shouldn't encounter an error closing the file.
  340.             } finally {
  341.                 overflow = null;
  342.             }
  343.         }
  344.     }

  345.     /**
  346.      * A fully buffered output stream using local disk storage for large data.
  347.      * <p>
  348.      * Initially this output stream buffers to memory and is therefore similar
  349.      * to ByteArrayOutputStream, but it shifts to using an on disk temporary
  350.      * file if the output gets too large.
  351.      * <p>
  352.      * The content of this buffered stream may be sent to another OutputStream
  353.      * only after this stream has been properly closed by {@link #close()}.
  354.      */
  355.     public static class LocalFile extends TemporaryBuffer {
  356.         /** Directory to store the temporary file under. */
  357.         private final File directory;

  358.         /**
  359.          * Location of our temporary file if we are on disk; otherwise null.
  360.          * <p>
  361.          * If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks}
  362.          * and created this file instead. All output goes here through
  363.          * {@link #overflow}.
  364.          */
  365.         private File onDiskFile;

  366.         /**
  367.          * Create a new temporary buffer, limiting memory usage.
  368.          *
  369.          * @param directory
  370.          *            if the buffer has to spill over into a temporary file, the
  371.          *            directory where the file should be saved. If null the
  372.          *            system default temporary directory (for example /tmp) will
  373.          *            be used instead.
  374.          */
  375.         public LocalFile(File directory) {
  376.             this(directory, DEFAULT_IN_CORE_LIMIT);
  377.         }

  378.         /**
  379.          * Create a new temporary buffer, limiting memory usage.
  380.          *
  381.          * @param directory
  382.          *            if the buffer has to spill over into a temporary file, the
  383.          *            directory where the file should be saved. If null the
  384.          *            system default temporary directory (for example /tmp) will
  385.          *            be used instead.
  386.          * @param inCoreLimit
  387.          *            maximum number of bytes to store in memory. Storage beyond
  388.          *            this limit will use the local file.
  389.          */
  390.         public LocalFile(File directory, int inCoreLimit) {
  391.             super(inCoreLimit);
  392.             this.directory = directory;
  393.         }

  394.         @Override
  395.         protected OutputStream overflow() throws IOException {
  396.             onDiskFile = File.createTempFile("jgit_", ".buf", directory); //$NON-NLS-1$ //$NON-NLS-2$
  397.             return new BufferedOutputStream(new FileOutputStream(onDiskFile));
  398.         }

  399.         @Override
  400.         public long length() {
  401.             if (onDiskFile == null) {
  402.                 return super.length();
  403.             }
  404.             return onDiskFile.length();
  405.         }

  406.         @Override
  407.         public byte[] toByteArray() throws IOException {
  408.             if (onDiskFile == null) {
  409.                 return super.toByteArray();
  410.             }

  411.             final long len = length();
  412.             if (Integer.MAX_VALUE < len)
  413.                 throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
  414.             final byte[] out = new byte[(int) len];
  415.             try (FileInputStream in = new FileInputStream(onDiskFile)) {
  416.                 IO.readFully(in, out, 0, (int) len);
  417.             }
  418.             return out;
  419.         }

  420.         @Override
  421.         public byte[] toByteArray(int limit) throws IOException {
  422.             if (onDiskFile == null) {
  423.                 return super.toByteArray(limit);
  424.             }
  425.             final long len = Math.min(length(), limit);
  426.             if (Integer.MAX_VALUE < len) {
  427.                 throw new OutOfMemoryError(
  428.                         JGitText.get().lengthExceedsMaximumArraySize);
  429.             }
  430.             final byte[] out = new byte[(int) len];
  431.             try (FileInputStream in = new FileInputStream(onDiskFile)) {
  432.                 int read = 0;
  433.                 int chunk;
  434.                 while ((chunk = in.read(out, read, out.length - read)) >= 0) {
  435.                     read += chunk;
  436.                     if (read == out.length) {
  437.                         break;
  438.                     }
  439.                 }
  440.             }
  441.             return out;
  442.         }

  443.         @Override
  444.         public void writeTo(OutputStream os, ProgressMonitor pm)
  445.                 throws IOException {
  446.             if (onDiskFile == null) {
  447.                 super.writeTo(os, pm);
  448.                 return;
  449.             }
  450.             if (pm == null)
  451.                 pm = NullProgressMonitor.INSTANCE;
  452.             try (FileInputStream in = new FileInputStream(onDiskFile)) {
  453.                 int cnt;
  454.                 final byte[] buf = new byte[Block.SZ];
  455.                 while ((cnt = in.read(buf)) >= 0) {
  456.                     os.write(buf, 0, cnt);
  457.                     pm.update(cnt / 1024);
  458.                 }
  459.             }
  460.         }

  461.         @Override
  462.         public InputStream openInputStream() throws IOException {
  463.             if (onDiskFile == null)
  464.                 return super.openInputStream();
  465.             return new FileInputStream(onDiskFile);
  466.         }

  467.         @Override
  468.         public InputStream openInputStreamWithAutoDestroy() throws IOException {
  469.             if (onDiskFile == null) {
  470.                 return super.openInputStreamWithAutoDestroy();
  471.             }
  472.             return new FileInputStream(onDiskFile) {
  473.                 @Override
  474.                 public void close() throws IOException {
  475.                     super.close();
  476.                     destroy();
  477.                 }
  478.             };
  479.         }

  480.         @Override
  481.         public void destroy() {
  482.             super.destroy();

  483.             if (onDiskFile != null) {
  484.                 try {
  485.                     if (!onDiskFile.delete())
  486.                         onDiskFile.deleteOnExit();
  487.                 } finally {
  488.                     onDiskFile = null;
  489.                 }
  490.             }
  491.         }
  492.     }

  493.     /**
  494.      * A temporary buffer that will never exceed its in-memory limit.
  495.      * <p>
  496.      * If the in-memory limit is reached an IOException is thrown, rather than
  497.      * attempting to spool to local disk.
  498.      */
  499.     public static class Heap extends TemporaryBuffer {
  500.         /**
  501.          * Create a new heap buffer with a maximum storage limit.
  502.          *
  503.          * @param limit
  504.          *            maximum number of bytes that can be stored in this buffer;
  505.          *            also used as the estimated size. Storing beyond this many
  506.          *            will cause an IOException to be thrown during write.
  507.          */
  508.         public Heap(int limit) {
  509.             super(limit);
  510.         }

  511.         /**
  512.          * Create a new heap buffer with a maximum storage limit.
  513.          *
  514.          * @param estimatedSize
  515.          *            estimated size of storage used, to size the initial list of
  516.          *            block pointers.
  517.          * @param limit
  518.          *            maximum number of bytes that can be stored in this buffer.
  519.          *            Storing beyond this many will cause an IOException to be
  520.          *            thrown during write.
  521.          * @since 4.0
  522.          */
  523.         public Heap(int estimatedSize, int limit) {
  524.             super(estimatedSize, limit);
  525.         }

  526.         @Override
  527.         protected OutputStream overflow() throws IOException {
  528.             throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
  529.         }
  530.     }

  531.     static class Block {
  532.         static final int SZ = 8 * 1024;

  533.         final byte[] buffer;

  534.         int count;

  535.         Block() {
  536.             buffer = new byte[SZ];
  537.         }

  538.         Block(int sz) {
  539.             buffer = new byte[sz];
  540.         }

  541.         boolean isFull() {
  542.             return count == buffer.length;
  543.         }
  544.     }

  545.     private class BlockInputStream extends InputStream {
  546.         private byte[] singleByteBuffer;
  547.         private int blockIndex;
  548.         private Block block;
  549.         private int blockPos;

  550.         BlockInputStream() {
  551.             block = blocks.get(blockIndex);
  552.         }

  553.         @Override
  554.         public int read() throws IOException {
  555.             if (singleByteBuffer == null)
  556.                 singleByteBuffer = new byte[1];
  557.             int n = read(singleByteBuffer);
  558.             return n == 1 ? singleByteBuffer[0] & 0xff : -1;
  559.         }

  560.         @Override
  561.         public long skip(long cnt) throws IOException {
  562.             long skipped = 0;
  563.             while (0 < cnt) {
  564.                 int n = (int) Math.min(block.count - blockPos, cnt);
  565.                 if (0 < n) {
  566.                     blockPos += n;
  567.                     skipped += n;
  568.                     cnt -= n;
  569.                 } else if (nextBlock())
  570.                     continue;
  571.                 else
  572.                     break;
  573.             }
  574.             return skipped;
  575.         }

  576.         @Override
  577.         public int read(byte[] b, int off, int len) throws IOException {
  578.             if (len == 0)
  579.                 return 0;
  580.             int copied = 0;
  581.             while (0 < len) {
  582.                 int c = Math.min(block.count - blockPos, len);
  583.                 if (0 < c) {
  584.                     System.arraycopy(block.buffer, blockPos, b, off, c);
  585.                     blockPos += c;
  586.                     off += c;
  587.                     len -= c;
  588.                     copied += c;
  589.                 } else if (nextBlock())
  590.                     continue;
  591.                 else
  592.                     break;
  593.             }
  594.             return 0 < copied ? copied : -1;
  595.         }

  596.         private boolean nextBlock() {
  597.             if (++blockIndex < blocks.size()) {
  598.                 block = blocks.get(blockIndex);
  599.                 blockPos = 0;
  600.                 return true;
  601.             }
  602.             return false;
  603.         }
  604.     }
  605. }