Author: ebourg
Date: Mon Dec 16 13:18:24 2013
New Revision: 1551202

URL: http://svn.apache.org/r1551202
Log:
GzipCompressorOutputStream revamp to support custom compression level and 
header metadata (COMPRESS-250)

Added:
    
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
   (with props)
Modified:
    commons/proper/compress/trunk/src/changes/changes.xml
    
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStream.java
    
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/compressors/GZipTestCase.java

Modified: commons/proper/compress/trunk/src/changes/changes.xml
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/changes/changes.xml?rev=1551202&r1=1551201&r2=1551202&view=diff
==============================================================================
--- commons/proper/compress/trunk/src/changes/changes.xml (original)
+++ commons/proper/compress/trunk/src/changes/changes.xml Mon Dec 16 13:18:24 
2013
@@ -44,6 +44,10 @@ The <action> type attribute can be add,u
   <body>
     <release version="1.7" date="not released, yet"
              description="Release 1.7">
+      <action issue="COMPRESS-250" type="add" date="2012-12-16" 
due-to="Emmanuel Bourg">
+        GzipCompressorOutputStream now supports setting the compression level 
and the header metadata
+        (filename, comment, modification time, operating system and extra 
flags)
+      </action>
       <action issue="COMPRESS-241" type="fix" date="2013-10-27">
         SevenZOutputFile#closeArchiveEntry throws an exception when
         using LZMA2 compression on Java8.

Modified: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStream.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStream.java?rev=1551202&r1=1551201&r2=1551202&view=diff
==============================================================================
--- 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStream.java
 (original)
+++ 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStream.java
 Mon Dec 16 13:18:24 2013
@@ -20,21 +20,113 @@ package org.apache.commons.compress.comp
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 
 import org.apache.commons.compress.compressors.CompressorOutputStream;
 
+/**
+ * Compressed output stream using the gzip format. This implementation improves
+ * over the standard {@link GZIPOutputStream} class by allowing
+ * the configuration of the compression level and the header metadata 
(filename,
+ * comment, modification time, operating system and extra flags).
+ * 
+ * @see <a href="http://tools.ietf.org/html/rfc1952";>GZIP File Format 
Specification</a>
+ */
 public class GzipCompressorOutputStream extends CompressorOutputStream {
 
-    private final GZIPOutputStream out;
+    /** Header flag indicating a file name follows the header */
+    private static final int FNAME = 1 << 3;
 
-    public GzipCompressorOutputStream( final OutputStream outputStream ) 
throws IOException {
-        out = new GZIPOutputStream(outputStream);
+    /** Header flag indicating a comment follows the header */
+    private static final int FCOMMENT = 1 << 4;
+
+    /** The underlying stream */
+    private final OutputStream out;
+
+    /** Deflater used to compress the data */
+    private final Deflater deflater;
+
+    /** The buffer receiving the compressed data from the deflater */
+    private final byte[] buffer = new byte[512];
+
+    /** Indicates if the stream has been closed */
+    private boolean closed;
+
+    /** The checksum of the uncompressed data */
+    private final CRC32 crc = new CRC32();
+
+    /**
+     * Creates a gzip compressed output stream with the default parameters.
+     */
+    public GzipCompressorOutputStream(OutputStream out) throws IOException {
+        this(out, new GzipParameters());
+    }
+
+    /**
+     * Creates a gzip compressed output stream with the specified parameters.
+     * 
+     * @since 1.7
+     */
+    public GzipCompressorOutputStream(OutputStream out, GzipParameters 
parameters) throws IOException {
+        this.out = out;
+        this.deflater = new Deflater(parameters.getCompressionLevel(), true);
+        
+        writeHeader(parameters);
+    }
+
+    private void writeHeader(GzipParameters parameters) throws IOException {
+        String filename = parameters.getFilename();
+        String comment = parameters.getComment();
+        
+        ByteBuffer buffer = ByteBuffer.allocate(10);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putShort((short) GZIPInputStream.GZIP_MAGIC);
+        buffer.put((byte) 8); // compression method (8: deflate)
+        buffer.put((byte) ((filename != null ? FNAME : 0) | (comment != null ? 
FCOMMENT : 0))); // flags
+        buffer.putInt((int) (parameters.getModificationTime() / 1000));
+        
+        // extra flags
+        int compressionLevel = parameters.getCompressionLevel();
+        if (compressionLevel == Deflater.BEST_COMPRESSION) {
+            buffer.put((byte) 2);
+        } else if (compressionLevel == Deflater.BEST_SPEED) {
+            buffer.put((byte) 4);
+        } else {
+            buffer.put((byte) 0);
+        }
+        
+        buffer.put((byte) parameters.getOperatingSystem());
+        
+        out.write(buffer.array());
+        
+        if (filename != null) {
+            out.write(filename.getBytes("ISO-8859-1"));
+            out.write(0);
+        }
+        
+        if (comment != null) {
+            out.write(comment.getBytes("ISO-8859-1"));
+            out.write(0);
+        }
+    }
+
+    private void writeTrailer() throws IOException {
+        ByteBuffer buffer = ByteBuffer.allocate(8);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putInt((int) crc.getValue());
+        buffer.putInt(deflater.getTotalIn());
+
+        out.write(buffer.array());
     }
 
     @Override
     public void write(int b) throws IOException {
-        out.write(b);
+        write(new byte[]{(byte) (b & 0xff)}, 0, 1);
     }
 
     /**
@@ -43,8 +135,8 @@ public class GzipCompressorOutputStream 
      * @since 1.1
      */
     @Override
-    public void write(byte[] b) throws IOException {
-        out.write(b);
+    public void write(byte[] buffer) throws IOException {
+        write(buffer, 0, buffer.length);
     }
 
     /**
@@ -53,8 +145,26 @@ public class GzipCompressorOutputStream 
      * @since 1.1
      */
     @Override
-    public void write(byte[] b, int from, int length) throws IOException {
-        out.write(b, from, length);
+    public void write(byte[] buffer, int offset, int length) throws 
IOException {
+        if (deflater.finished()) {
+            throw new IOException("Cannot write more data, the end of the 
compressed data stream has been reached");
+
+        } else if (length > 0) {
+            deflater.setInput(buffer, offset, length);
+            
+            while (!deflater.needsInput()) {
+                deflate();
+            }
+            
+            crc.update(buffer, offset, length);
+        }
+    }
+
+    private void deflate() throws IOException {
+        int length = deflater.deflate(buffer, 0, buffer.length);
+        if (length > 0) {
+            out.write(buffer, 0, length);
+        }
     }
 
     /**
@@ -63,7 +173,15 @@ public class GzipCompressorOutputStream 
      * @since 1.7
      */
     public void finish() throws IOException {
-        out.finish();
+        if (!deflater.finished()) {
+            deflater.finish();
+
+            while (!deflater.finished()) {
+                deflate();
+            }
+            
+            writeTrailer();
+        }
     }
 
     /**
@@ -78,7 +196,12 @@ public class GzipCompressorOutputStream 
 
     @Override
     public void close() throws IOException {
-        out.close();
+        if (!closed) {
+            finish();
+            deflater.end();
+            out.close();
+            closed = true;
+        }
     }
 
 }

Added: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java?rev=1551202&view=auto
==============================================================================
--- 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
 (added)
+++ 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
 Mon Dec 16 13:18:24 2013
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.commons.compress.compressors.gzip;
+
+import java.util.zip.Deflater;
+
+/**
+ * Parameters for the GZIP compressor.
+ * 
+ * @since 1.7
+ */
+public class GzipParameters {
+
+    private int compressionLevel = Deflater.DEFAULT_COMPRESSION;
+    private long modificationTime;
+    private String filename;
+    private String comment;
+    private int operatingSystem = 255; // Unknown OS by default
+
+    public int getCompressionLevel() {
+        return compressionLevel;
+    }
+
+    /**
+     * Sets the compression level.
+     * 
+     * @param compressionLevel the compression level (between 0 and 9) 
+     * @see Deflater#NO_COMPRESSION
+     * @see Deflater#BEST_SPEED
+     * @see Deflater#DEFAULT_COMPRESSION
+     * @see Deflater#BEST_COMPRESSION
+     */
+    public void setCompressionLevel(int compressionLevel) {
+        if (compressionLevel < -1 || compressionLevel > 9) {
+            throw new IllegalArgumentException("Invalid gzip compression 
level: " + compressionLevel);
+        }
+        this.compressionLevel = compressionLevel;
+    }
+
+    public long getModificationTime() {
+        return modificationTime;
+    }
+
+    /**
+     * Sets the modification time of the compressed file.
+     * 
+     * @param modificationTime the modification time, in milliseconds
+     */
+    public void setModificationTime(long modificationTime) {
+        this.modificationTime = modificationTime;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    /**
+     * Sets the name of the compressed file.
+     * 
+     * @param filename the name of the file without the directory path
+     */
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+
+    public int getOperatingSystem() {
+        return operatingSystem;
+    }
+
+    /**
+     * Sets the operating system on which the compression took place.
+     * The defined values are:
+     * <ul>
+     *   <li>0: FAT filesystem (MS-DOS, OS/2, NT/Win32)</li>
+     *   <li>1: Amiga</li>
+     *   <li>2: VMS (or OpenVMS)</li>
+     *   <li>3: Unix</li>
+     *   <li>4: VM/CMS</li>
+     *   <li>5: Atari TOS</li>
+     *   <li>6: HPFS filesystem (OS/2, NT)</li>
+     *   <li>7: Macintosh</li>
+     *   <li>8: Z-System</li>
+     *   <li>9: CP/M</li>
+     *   <li>10: TOPS-20</li>
+     *   <li>11: NTFS filesystem (NT)</li>
+     *   <li>12: QDOS</li>
+     *   <li>13: Acorn RISCOS</li>
+     *   <li>255: Unknown</li>
+     * </ul>
+     * 
+     * @param operatingSystem the code of the operating system
+     */
+    public void setOperatingSystem(int operatingSystem) {
+        this.operatingSystem = operatingSystem;
+    }
+}

Propchange: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/compressors/gzip/GzipParameters.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Modified: 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/compressors/GZipTestCase.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/compressors/GZipTestCase.java?rev=1551202&r1=1551201&r2=1551202&view=diff
==============================================================================
--- 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/compressors/GZipTestCase.java
 (original)
+++ 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/compressors/GZipTestCase.java
 Mon Dec 16 13:18:24 2013
@@ -26,10 +26,17 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
 
 import org.apache.commons.compress.AbstractTestCase;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipParameters;
 import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.output.NullOutputStream;
+import org.junit.Assert;
 
 public final class GZipTestCase extends AbstractTestCase {
 
@@ -147,4 +154,103 @@ public final class GZipTestCase extends 
             }
         }
     }
+
+    public void testInteroperabilityWithGzipCompressorInputStream() throws 
Exception {
+        byte[] content = FileUtils.readFileToByteArray(getFile("test3.xml"));
+        
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+
+        GzipParameters parameters = new GzipParameters();
+        parameters.setCompressionLevel(Deflater.BEST_COMPRESSION);
+        parameters.setOperatingSystem(3);
+        parameters.setFilename("test3.xml");
+        parameters.setComment("Test file");
+        parameters.setModificationTime(System.currentTimeMillis());
+        GzipCompressorOutputStream out = new GzipCompressorOutputStream(bout, 
parameters);
+        out.write(content);
+        out.flush();
+        out.close();
+
+        GzipCompressorInputStream in = new GzipCompressorInputStream(new 
ByteArrayInputStream(bout.toByteArray()));
+        byte[] content2 = IOUtils.toByteArray(in);
+
+        Assert.assertArrayEquals("uncompressed content", content, content2);
+    }
+
+    public void testInteroperabilityWithGZIPInputStream() throws Exception {
+        byte[] content = FileUtils.readFileToByteArray(getFile("test3.xml"));
+        
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+
+        GzipParameters parameters = new GzipParameters();
+        parameters.setCompressionLevel(Deflater.BEST_COMPRESSION);
+        parameters.setOperatingSystem(3);
+        parameters.setFilename("test3.xml");
+        parameters.setComment("Test file");
+        parameters.setModificationTime(System.currentTimeMillis());
+        GzipCompressorOutputStream out = new GzipCompressorOutputStream(bout, 
parameters);
+        out.write(content);
+        out.flush();
+        out.close();
+
+        GZIPInputStream in = new GZIPInputStream(new 
ByteArrayInputStream(bout.toByteArray()));
+        byte[] content2 = IOUtils.toByteArray(in);
+
+        Assert.assertArrayEquals("uncompressed content", content, content2);
+    }
+
+    public void testInvalidCompressionLevel() {
+        GzipParameters parameters = new GzipParameters();
+        try {
+            parameters.setCompressionLevel(10);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        
+        try {
+            parameters.setCompressionLevel(-5);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    private void testExtraFlags(int compressionLevel, int flag) throws 
Exception {
+        byte[] content = FileUtils.readFileToByteArray(getFile("test3.xml"));
+        
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        
+        GzipParameters parameters = new GzipParameters();
+        parameters.setCompressionLevel(compressionLevel);
+        GzipCompressorOutputStream out = new GzipCompressorOutputStream(bout, 
parameters);
+        IOUtils.copy(new ByteArrayInputStream(content), out);
+        out.flush();
+        out.close();
+        
+        assertEquals("extra flags (XFL)", flag, bout.toByteArray()[8]);
+    }
+
+    public void testExtraFlagsFastestCompression() throws Exception {
+        testExtraFlags(Deflater.BEST_SPEED, 4);
+    }
+
+    public void testExtraFlagsBestCompression() throws Exception {
+        testExtraFlags(Deflater.BEST_COMPRESSION, 2);
+    }
+
+    public void testExtraFlagsDefaultCompression() throws Exception {
+        testExtraFlags(Deflater.DEFAULT_COMPRESSION, 0);
+    }
+    
+    public void testOverWrite() throws Exception {
+        GzipCompressorOutputStream out = new GzipCompressorOutputStream(new 
NullOutputStream());
+        out.close();
+        try {
+            out.write(0);
+            fail("IOException expected");
+        } catch (IOException e) {
+            // expected
+        }
+    }
 }


Reply via email to