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 + } + } }