diff --git a/okhttp-protocols/pom.xml b/okhttp-protocols/pom.xml index 190af43cf..2dca09ebf 100644 --- a/okhttp-protocols/pom.xml +++ b/okhttp-protocols/pom.xml @@ -23,5 +23,11 @@ junit test + + org.codehaus.mojo + animal-sniffer-annotations + 1.10 + true + diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/BufferedSink.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/BufferedSink.java index da1f07048..cbd271650 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/BufferedSink.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/BufferedSink.java @@ -73,7 +73,7 @@ public final class BufferedSink implements Sink { emitCompleteSegments(deadline); } - private void emitCompleteSegments(Deadline deadline) throws IOException { + void emitCompleteSegments(Deadline deadline) throws IOException { long byteCount = buffer.byteCount; if (byteCount == 0) return; diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/DeflaterSink.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/DeflaterSink.java new file mode 100644 index 000000000..2413d4885 --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/DeflaterSink.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed 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 com.squareup.okhttp.internal.bytes; + +import java.io.IOException; +import java.util.zip.Deflater; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; + +/** + * A sink that uses DEFLATE to + * compress data written to another source. + * + *

Sync flush

+ * Aggressive flushing of this stream may result in reduced compression. Each + * call to {@link #flush} immediately compresses all currently-buffered data; + * this early compression may be less effective than compression performed + * without flushing. + * + *

This is equivalent to using {@link Deflater} with the sync flush option. + * This class does not offer any partial flush mechanism. For best performance, + * only call {@link #flush} when application behavior requires it. + */ +public final class DeflaterSink implements Sink { + private final BufferedSink sink; + private final Deflater deflater; + + public DeflaterSink(Sink sink, Deflater deflater) { + this.sink = new BufferedSink(sink); + this.deflater = deflater; + } + + @Override public void write(OkBuffer source, long byteCount, Deadline deadline) + throws IOException { + checkOffsetAndCount(source.byteCount, 0, byteCount); + while (byteCount > 0) { + // Share bytes from the head segment of 'source' with the deflater. + Segment head = source.head; + int toDeflate = (int) Math.min(byteCount, head.limit - head.pos); + deflater.setInput(head.data, head.pos, toDeflate); + + // Deflate those bytes into sink. + deflate(deadline, false); + + // Mark those bytes as read. + source.byteCount -= toDeflate; + head.pos += toDeflate; + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.INSTANCE.recycle(head); + } + + byteCount -= toDeflate; + } + } + + @IgnoreJRERequirement + private void deflate(Deadline deadline, boolean syncFlush) throws IOException { + while (true) { + Segment s = sink.buffer.writableSegment(1); + + // The 4-parameter overload of deflate() doesn't exist in the RI until + // Java 1.7, and is public (although with @hide) on Android since 2.3. + // The @hide tag means that this code won't compile against the Android + // 2.3 SDK, but it will run fine there. + int deflated = syncFlush + ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH) + : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit); + + if (deflated == 0) return; + s.limit += deflated; + sink.buffer.byteCount += deflated; + sink.emitCompleteSegments(deadline); + } + } + + @Override public void flush(Deadline deadline) throws IOException { + deflate(deadline, true); + sink.flush(deadline); + } + + @Override public void close(Deadline deadline) throws IOException { + deflater.finish(); + deflate(deadline, false); + sink.close(deadline); + } +} diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/InflaterSource.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/InflaterSource.java index 6681487da..790dcfd25 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/InflaterSource.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/bytes/InflaterSource.java @@ -20,7 +20,10 @@ import java.io.IOException; import java.util.zip.DataFormatException; import java.util.zip.Inflater; -/** A source that inflates another source. */ +/** + * A source that uses DEFLATE + * to decompress data read from another source. + */ public final class InflaterSource implements Source { private final BufferedSource source; private final Inflater inflater; diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/DeflaterSinkTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/DeflaterSinkTest.java new file mode 100644 index 000000000..e1d22b9a6 --- /dev/null +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/DeflaterSinkTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed 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 com.squareup.okhttp.internal.bytes; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Random; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public final class DeflaterSinkTest { + @Test public void deflateWithClose() throws Exception { + OkBuffer data = new OkBuffer(); + String original = "They're moving in herds. They do move in herds."; + data.writeUtf8(original); + OkBuffer sink = new OkBuffer(); + DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); + deflaterSink.write(data, data.byteCount(), Deadline.NONE); + deflaterSink.close(Deadline.NONE); + OkBuffer inflated = inflate(sink); + assertEquals(original, inflated.readUtf8((int) inflated.byteCount())); + } + + @Test public void deflateWithSyncFlush() throws Exception { + String original = "Yes, yes, yes. That's why we're taking extreme precautions."; + OkBuffer data = new OkBuffer(); + data.writeUtf8(original); + OkBuffer sink = new OkBuffer(); + DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); + deflaterSink.write(data, data.byteCount(), Deadline.NONE); + deflaterSink.flush(Deadline.NONE); + OkBuffer inflated = inflate(sink); + assertEquals(original, inflated.readUtf8((int) inflated.byteCount())); + } + + @Test public void deflateWellCompressed() throws IOException { + String original = repeat('a', 1024 * 1024); + OkBuffer data = new OkBuffer(); + data.writeUtf8(original); + OkBuffer sink = new OkBuffer(); + DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); + deflaterSink.write(data, data.byteCount(), Deadline.NONE); + deflaterSink.close(Deadline.NONE); + OkBuffer inflated = inflate(sink); + assertEquals(original, inflated.readUtf8((int) inflated.byteCount())); + } + + @Test public void deflatePoorlyCompressed() throws IOException { + ByteString original = randomBytes(1024 * 1024); + OkBuffer data = new OkBuffer(); + data.write(original); + OkBuffer sink = new OkBuffer(); + DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater()); + deflaterSink.write(data, data.byteCount(), Deadline.NONE); + deflaterSink.close(Deadline.NONE); + OkBuffer inflated = inflate(sink); + assertEquals(original, inflated.readByteString((int) inflated.byteCount())); + } + + /** + * Uses streaming decompression to inflate {@code deflated}. The input must + * either be finished or have a trailing sync flush. + */ + private OkBuffer inflate(OkBuffer deflated) throws IOException { + InputStream deflatedIn = new BufferedSource(deflated).inputStream(); + Inflater inflater = new Inflater(); + InputStream inflatedIn = new InflaterInputStream(deflatedIn, inflater); + OkBuffer result = new OkBuffer(); + byte[] buffer = new byte[8192]; + while (!inflater.needsInput() || deflated.byteCount() > 0 || deflatedIn.available() > 0) { + int count = inflatedIn.read(buffer, 0, buffer.length); + result.write(buffer, 0, count); + } + return result; + } + + private ByteString randomBytes(int length) { + Random random = new Random(0); + byte[] randomBytes = new byte[length]; + random.nextBytes(randomBytes); + return ByteString.of(randomBytes); + } + + private String repeat(char c, int count) { + char[] array = new char[count]; + Arrays.fill(array, c); + return new String(array); + } +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/InflaterSourceTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/InflaterSourceTest.java index a272e37a4..481f024fc 100644 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/InflaterSourceTest.java +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/bytes/InflaterSourceTest.java @@ -26,7 +26,7 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -public class InflaterSourceTest { +public final class InflaterSourceTest { @Test public void inflate() throws Exception { OkBuffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK" + "tYDAF6CD5s="); diff --git a/pom.xml b/pom.xml index 82861f385..4b7672d37 100644 --- a/pom.xml +++ b/pom.xml @@ -171,7 +171,7 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.9 + 1.10 test