StreamCompressor.java

  1. /*
  2.  *  Licensed to the Apache Software Foundation (ASF) under one or more
  3.  *  contributor license agreements.  See the NOTICE file distributed with
  4.  *  this work for additional information regarding copyright ownership.
  5.  *  The ASF licenses this file to You under the Apache License, Version 2.0
  6.  *  (the "License"); you may not use this file except in compliance with
  7.  *  the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  *  Unless required by applicable law or agreed to in writing, software
  12.  *  distributed under the License is distributed on an "AS IS" BASIS,
  13.  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  *  See the License for the specific language governing permissions and
  15.  *  limitations under the License.
  16.  */
  17. package org.apache.commons.compress.archivers.zip;

  18. import java.io.Closeable;
  19. import java.io.DataOutput;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.OutputStream;
  23. import java.nio.ByteBuffer;
  24. import java.nio.channels.SeekableByteChannel;
  25. import java.util.zip.CRC32;
  26. import java.util.zip.Deflater;
  27. import java.util.zip.ZipEntry;

  28. import org.apache.commons.compress.parallel.ScatterGatherBackingStore;

  29. /**
  30.  * Encapsulates a {@link Deflater} and crc calculator, handling multiple types of output streams. Currently {@link java.util.zip.ZipEntry#DEFLATED} and
  31.  * {@link java.util.zip.ZipEntry#STORED} are the only supported compression methods.
  32.  *
  33.  * @since 1.10
  34.  */
  35. public abstract class StreamCompressor implements Closeable {

  36.     private static final class DataOutputCompressor extends StreamCompressor {
  37.         private final DataOutput raf;

  38.         DataOutputCompressor(final Deflater deflater, final DataOutput raf) {
  39.             super(deflater);
  40.             this.raf = raf;
  41.         }

  42.         @Override
  43.         protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
  44.             raf.write(data, offset, length);
  45.         }
  46.     }

  47.     private static final class OutputStreamCompressor extends StreamCompressor {
  48.         private final OutputStream os;

  49.         OutputStreamCompressor(final Deflater deflater, final OutputStream os) {
  50.             super(deflater);
  51.             this.os = os;
  52.         }

  53.         @Override
  54.         protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
  55.             os.write(data, offset, length);
  56.         }
  57.     }

  58.     private static final class ScatterGatherBackingStoreCompressor extends StreamCompressor {
  59.         private final ScatterGatherBackingStore bs;

  60.         ScatterGatherBackingStoreCompressor(final Deflater deflater, final ScatterGatherBackingStore bs) {
  61.             super(deflater);
  62.             this.bs = bs;
  63.         }

  64.         @Override
  65.         protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
  66.             bs.writeOut(data, offset, length);
  67.         }
  68.     }

  69.     private static final class SeekableByteChannelCompressor extends StreamCompressor {
  70.         private final SeekableByteChannel channel;

  71.         SeekableByteChannelCompressor(final Deflater deflater, final SeekableByteChannel channel) {
  72.             super(deflater);
  73.             this.channel = channel;
  74.         }

  75.         @Override
  76.         protected void writeOut(final byte[] data, final int offset, final int length) throws IOException {
  77.             channel.write(ByteBuffer.wrap(data, offset, length));
  78.         }
  79.     }

  80.     /**
  81.      * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs when it gets handed a huge buffer. See
  82.      * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396
  83.      *
  84.      * Using a buffer size of {@value} bytes proved to be a good compromise
  85.      */
  86.     private static final int DEFLATER_BLOCK_SIZE = 8192;
  87.     private static final int BUFFER_SIZE = 4096;

  88.     /**
  89.      * Creates a stream compressor with the given compression level.
  90.      *
  91.      * @param os       The DataOutput to receive output
  92.      * @param deflater The deflater to use for the compressor
  93.      * @return A stream compressor
  94.      */
  95.     static StreamCompressor create(final DataOutput os, final Deflater deflater) {
  96.         return new DataOutputCompressor(deflater, os);
  97.     }

  98.     /**
  99.      * Creates a stream compressor with the given compression level.
  100.      *
  101.      * @param compressionLevel The {@link Deflater} compression level
  102.      * @param bs               The ScatterGatherBackingStore to receive output
  103.      * @return A stream compressor
  104.      */
  105.     public static StreamCompressor create(final int compressionLevel, final ScatterGatherBackingStore bs) {
  106.         final Deflater deflater = new Deflater(compressionLevel, true);
  107.         return new ScatterGatherBackingStoreCompressor(deflater, bs);
  108.     }

  109.     /**
  110.      * Creates a stream compressor with the default compression level.
  111.      *
  112.      * @param os The stream to receive output
  113.      * @return A stream compressor
  114.      */
  115.     static StreamCompressor create(final OutputStream os) {
  116.         return create(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
  117.     }

  118.     /**
  119.      * Creates a stream compressor with the given compression level.
  120.      *
  121.      * @param os       The stream to receive output
  122.      * @param deflater The deflater to use
  123.      * @return A stream compressor
  124.      */
  125.     static StreamCompressor create(final OutputStream os, final Deflater deflater) {
  126.         return new OutputStreamCompressor(deflater, os);
  127.     }

  128.     /**
  129.      * Creates a stream compressor with the default compression level.
  130.      *
  131.      * @param bs The ScatterGatherBackingStore to receive output
  132.      * @return A stream compressor
  133.      */
  134.     public static StreamCompressor create(final ScatterGatherBackingStore bs) {
  135.         return create(Deflater.DEFAULT_COMPRESSION, bs);
  136.     }

  137.     /**
  138.      * Creates a stream compressor with the given compression level.
  139.      *
  140.      * @param os       The SeekableByteChannel to receive output
  141.      * @param deflater The deflater to use for the compressor
  142.      * @return A stream compressor
  143.      * @since 1.13
  144.      */
  145.     static StreamCompressor create(final SeekableByteChannel os, final Deflater deflater) {
  146.         return new SeekableByteChannelCompressor(deflater, os);
  147.     }

  148.     private final Deflater deflater;

  149.     private final CRC32 crc = new CRC32();

  150.     private long writtenToOutputStreamForLastEntry;

  151.     private long sourcePayloadLength;

  152.     private long totalWrittenToOutputStream;

  153.     private final byte[] outputBuffer = new byte[BUFFER_SIZE];

  154.     private final byte[] readerBuf = new byte[BUFFER_SIZE];

  155.     StreamCompressor(final Deflater deflater) {
  156.         this.deflater = deflater;
  157.     }

  158.     @Override
  159.     public void close() throws IOException {
  160.         deflater.end();
  161.     }

  162.     void deflate() throws IOException {
  163.         final int len = deflater.deflate(outputBuffer, 0, outputBuffer.length);
  164.         if (len > 0) {
  165.             writeCounted(outputBuffer, 0, len);
  166.         }
  167.     }

  168.     /**
  169.      * Deflates the given source using the supplied compression method
  170.      *
  171.      * @param source The source to compress
  172.      * @param method The #ZipArchiveEntry compression method
  173.      * @throws IOException When failures happen
  174.      */

  175.     public void deflate(final InputStream source, final int method) throws IOException {
  176.         reset();
  177.         int length;

  178.         while ((length = source.read(readerBuf, 0, readerBuf.length)) >= 0) {
  179.             write(readerBuf, 0, length, method);
  180.         }
  181.         if (method == ZipEntry.DEFLATED) {
  182.             flushDeflater();
  183.         }
  184.     }

  185.     private void deflateUntilInputIsNeeded() throws IOException {
  186.         while (!deflater.needsInput()) {
  187.             deflate();
  188.         }
  189.     }

  190.     void flushDeflater() throws IOException {
  191.         deflater.finish();
  192.         while (!deflater.finished()) {
  193.             deflate();
  194.         }
  195.     }

  196.     /**
  197.      * Gets the number of bytes read from the source stream
  198.      *
  199.      * @return The number of bytes read, never negative
  200.      */
  201.     public long getBytesRead() {
  202.         return sourcePayloadLength;
  203.     }

  204.     /**
  205.      * Gets the number of bytes written to the output for the last entry
  206.      *
  207.      * @return The number of bytes, never negative
  208.      */
  209.     public long getBytesWrittenForLastEntry() {
  210.         return writtenToOutputStreamForLastEntry;
  211.     }

  212.     /**
  213.      * Gets the crc32 of the last deflated file
  214.      *
  215.      * @return the crc32
  216.      */

  217.     public long getCrc32() {
  218.         return crc.getValue();
  219.     }

  220.     /**
  221.      * Gets the total number of bytes written to the output for all files
  222.      *
  223.      * @return The number of bytes, never negative
  224.      */
  225.     public long getTotalBytesWritten() {
  226.         return totalWrittenToOutputStream;
  227.     }

  228.     void reset() {
  229.         crc.reset();
  230.         deflater.reset();
  231.         sourcePayloadLength = 0;
  232.         writtenToOutputStreamForLastEntry = 0;
  233.     }

  234.     /**
  235.      * Writes bytes to ZIP entry.
  236.      *
  237.      * @param b      the byte array to write
  238.      * @param offset the start position to write from
  239.      * @param length the number of bytes to write
  240.      * @param method the comrpession method to use
  241.      * @return the number of bytes written to the stream this time
  242.      * @throws IOException on error
  243.      */
  244.     long write(final byte[] b, final int offset, final int length, final int method) throws IOException {
  245.         final long current = writtenToOutputStreamForLastEntry;
  246.         crc.update(b, offset, length);
  247.         if (method == ZipEntry.DEFLATED) {
  248.             writeDeflated(b, offset, length);
  249.         } else {
  250.             writeCounted(b, offset, length);
  251.         }
  252.         sourcePayloadLength += length;
  253.         return writtenToOutputStreamForLastEntry - current;
  254.     }

  255.     public void writeCounted(final byte[] data) throws IOException {
  256.         writeCounted(data, 0, data.length);
  257.     }

  258.     public void writeCounted(final byte[] data, final int offset, final int length) throws IOException {
  259.         writeOut(data, offset, length);
  260.         writtenToOutputStreamForLastEntry += length;
  261.         totalWrittenToOutputStream += length;
  262.     }

  263.     private void writeDeflated(final byte[] b, final int offset, final int length) throws IOException {
  264.         if (length > 0 && !deflater.finished()) {
  265.             if (length <= DEFLATER_BLOCK_SIZE) {
  266.                 deflater.setInput(b, offset, length);
  267.                 deflateUntilInputIsNeeded();
  268.             } else {
  269.                 final int fullblocks = length / DEFLATER_BLOCK_SIZE;
  270.                 for (int i = 0; i < fullblocks; i++) {
  271.                     deflater.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, DEFLATER_BLOCK_SIZE);
  272.                     deflateUntilInputIsNeeded();
  273.                 }
  274.                 final int done = fullblocks * DEFLATER_BLOCK_SIZE;
  275.                 if (done < length) {
  276.                     deflater.setInput(b, offset + done, length - done);
  277.                     deflateUntilInputIsNeeded();
  278.                 }
  279.             }
  280.         }
  281.     }

  282.     protected abstract void writeOut(byte[] data, int offset, int length) throws IOException;
  283. }