GzipCompressorOutputStream.java

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

  20. import java.io.IOException;
  21. import java.io.OutputStream;
  22. import java.net.URI;
  23. import java.net.URISyntaxException;
  24. import java.nio.ByteBuffer;
  25. import java.nio.ByteOrder;
  26. import java.nio.charset.StandardCharsets;
  27. import java.util.zip.CRC32;
  28. import java.util.zip.Deflater;
  29. import java.util.zip.GZIPInputStream;
  30. import java.util.zip.GZIPOutputStream;

  31. import org.apache.commons.compress.compressors.CompressorOutputStream;

  32. /**
  33.  * Compressed output stream using the gzip format. This implementation improves over the standard {@link GZIPOutputStream} class by allowing the configuration
  34.  * of the compression level and the header metadata (file name, comment, modification time, operating system and extra flags).
  35.  *
  36.  * @see <a href="https://tools.ietf.org/html/rfc1952">GZIP File Format Specification</a>
  37.  */
  38. public class GzipCompressorOutputStream extends CompressorOutputStream<OutputStream> {

  39.     /** Header flag indicating a file name follows the header */
  40.     private static final int FNAME = 1 << 3;

  41.     /** Header flag indicating a comment follows the header */
  42.     private static final int FCOMMENT = 1 << 4;

  43.     /** Deflater used to compress the data */
  44.     private final Deflater deflater;

  45.     /** The buffer receiving the compressed data from the deflater */
  46.     private final byte[] deflateBuffer;

  47.     /** Indicates if the stream has been closed */
  48.     private boolean closed;

  49.     /** The checksum of the uncompressed data */
  50.     private final CRC32 crc = new CRC32();

  51.     /**
  52.      * Creates a gzip compressed output stream with the default parameters.
  53.      *
  54.      * @param out the stream to compress to
  55.      * @throws IOException if writing fails
  56.      */
  57.     public GzipCompressorOutputStream(final OutputStream out) throws IOException {
  58.         this(out, new GzipParameters());
  59.     }

  60.     /**
  61.      * Creates a gzip compressed output stream with the specified parameters.
  62.      *
  63.      * @param out        the stream to compress to
  64.      * @param parameters the parameters to use
  65.      * @throws IOException if writing fails
  66.      *
  67.      * @since 1.7
  68.      */
  69.     public GzipCompressorOutputStream(final OutputStream out, final GzipParameters parameters) throws IOException {
  70.         super(out);
  71.         this.deflater = new Deflater(parameters.getCompressionLevel(), true);
  72.         this.deflater.setStrategy(parameters.getDeflateStrategy());
  73.         this.deflateBuffer = new byte[parameters.getBufferSize()];
  74.         writeHeader(parameters);
  75.     }

  76.     @Override
  77.     public void close() throws IOException {
  78.         if (!closed) {
  79.             try {
  80.                 finish();
  81.             } finally {
  82.                 deflater.end();
  83.                 out.close();
  84.                 closed = true;
  85.             }
  86.         }
  87.     }

  88.     private void deflate() throws IOException {
  89.         final int length = deflater.deflate(deflateBuffer, 0, deflateBuffer.length);
  90.         if (length > 0) {
  91.             out.write(deflateBuffer, 0, length);
  92.         }
  93.     }

  94.     /**
  95.      * Finishes writing compressed data to the underlying stream without closing it.
  96.      *
  97.      * @since 1.7
  98.      * @throws IOException on error
  99.      */
  100.     public void finish() throws IOException {
  101.         if (!deflater.finished()) {
  102.             deflater.finish();

  103.             while (!deflater.finished()) {
  104.                 deflate();
  105.             }

  106.             writeTrailer();
  107.         }
  108.     }

  109.     /**
  110.      * Gets the bytes encoded in the {@value GzipUtils#GZIP_ENCODING} Charset.
  111.      * <p>
  112.      * If the string cannot be encoded directly with {@value GzipUtils#GZIP_ENCODING}, then use URI-style percent encoding.
  113.      * </p>
  114.      *
  115.      * @param string The string to encode.
  116.      * @return
  117.      * @throws IOException
  118.      */
  119.     private byte[] getBytes(final String string) throws IOException {
  120.         if (GzipUtils.GZIP_ENCODING.newEncoder().canEncode(string)) {
  121.             return string.getBytes(GzipUtils.GZIP_ENCODING);
  122.         }
  123.         try {
  124.             return new URI(null, null, string, null).toASCIIString().getBytes(StandardCharsets.US_ASCII);
  125.         } catch (final URISyntaxException e) {
  126.             throw new IOException(string, e);
  127.         }
  128.     }

  129.     /**
  130.      * {@inheritDoc}
  131.      *
  132.      * @since 1.1
  133.      */
  134.     @Override
  135.     public void write(final byte[] buffer) throws IOException {
  136.         write(buffer, 0, buffer.length);
  137.     }

  138.     /**
  139.      * {@inheritDoc}
  140.      *
  141.      * @since 1.1
  142.      */
  143.     @Override
  144.     public void write(final byte[] buffer, final int offset, final int length) throws IOException {
  145.         if (deflater.finished()) {
  146.             throw new IOException("Cannot write more data, the end of the compressed data stream has been reached");
  147.         }
  148.         if (length > 0) {
  149.             deflater.setInput(buffer, offset, length);

  150.             while (!deflater.needsInput()) {
  151.                 deflate();
  152.             }

  153.             crc.update(buffer, offset, length);
  154.         }
  155.     }

  156.     @Override
  157.     public void write(final int b) throws IOException {
  158.         write(new byte[] { (byte) (b & 0xff) }, 0, 1);
  159.     }

  160.     private void writeHeader(final GzipParameters parameters) throws IOException {
  161.         final String fileName = parameters.getFileName();
  162.         final String comment = parameters.getComment();

  163.         final ByteBuffer buffer = ByteBuffer.allocate(10);
  164.         buffer.order(ByteOrder.LITTLE_ENDIAN);
  165.         buffer.putShort((short) GZIPInputStream.GZIP_MAGIC);
  166.         buffer.put((byte) Deflater.DEFLATED); // compression method (8: deflate)
  167.         buffer.put((byte) ((fileName != null ? FNAME : 0) | (comment != null ? FCOMMENT : 0))); // flags
  168.         buffer.putInt((int) (parameters.getModificationTime() / 1000));

  169.         // extra flags
  170.         final int compressionLevel = parameters.getCompressionLevel();
  171.         if (compressionLevel == Deflater.BEST_COMPRESSION) {
  172.             buffer.put((byte) 2);
  173.         } else if (compressionLevel == Deflater.BEST_SPEED) {
  174.             buffer.put((byte) 4);
  175.         } else {
  176.             buffer.put((byte) 0);
  177.         }

  178.         buffer.put((byte) parameters.getOperatingSystem());

  179.         out.write(buffer.array());

  180.         if (fileName != null) {
  181.             out.write(getBytes(fileName));
  182.             out.write(0);
  183.         }

  184.         if (comment != null) {
  185.             out.write(getBytes(comment));
  186.             out.write(0);
  187.         }
  188.     }

  189.     private void writeTrailer() throws IOException {
  190.         final ByteBuffer buffer = ByteBuffer.allocate(8);
  191.         buffer.order(ByteOrder.LITTLE_ENDIAN);
  192.         buffer.putInt((int) crc.getValue());
  193.         buffer.putInt(deflater.getTotalIn());

  194.         out.write(buffer.array());
  195.     }

  196. }