Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wrap GZIPInputStream for connection reuse #840

Merged
merged 3 commits into from Oct 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,47 @@
/*
* Copyright 2019 Google LLC
*
* 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
*
* https://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.google.api.client.http;

import com.google.common.io.ByteStreams;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* This class in meant to wrap an {@link InputStream} so that all bytes in the steam are read and
* discarded on {@link InputStream#close()}. This ensures that the underlying connection has the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the stream never ends?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a very good question. I am not sure.

* option to be reused.
*/
final class ConsumingInputStream extends FilterInputStream {
private boolean closed = false;

ConsumingInputStream(InputStream inputStream) {
super(inputStream);
}

@Override
public void close() throws IOException {
if (!closed && in != null) {
try {
ByteStreams.exhaust(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could have an infinite loop here. See internal discussion.

super.in.close();
} finally {
this.closed = true;
}
}
}
}
Expand Up @@ -331,7 +331,8 @@ public InputStream getContent() throws IOException {
if (!returnRawInputStream
&& contentEncoding != null
&& contentEncoding.contains("gzip")) {
codyoss marked this conversation as resolved.
Show resolved Hide resolved
lowLevelResponseContent = new GZIPInputStream(lowLevelResponseContent);
lowLevelResponseContent =
new ConsumingInputStream(new GZIPInputStream(lowLevelResponseContent));
}
// logging (wrap content with LoggingInputStream)
Logger logger = HttpTransport.LOGGER;
Expand Down
@@ -0,0 +1,65 @@
/*
* Copyright 2019 Google LLC
*
* 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
*
* https://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.google.api.client.http;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.junit.Test;

public class ConsumingInputStreamTest {

@Test
public void testClose_drainsBytesOnClose() throws IOException {
MockInputStream mockInputStream = new MockInputStream("abc123".getBytes(StandardCharsets.UTF_8));
InputStream consumingInputStream = new ConsumingInputStream(mockInputStream);

assertEquals(6, mockInputStream.getBytesToRead());
codyoss marked this conversation as resolved.
Show resolved Hide resolved

// read one byte
consumingInputStream.read();
assertEquals(5, mockInputStream.getBytesToRead());

// closing the stream should read the remaining bytes
consumingInputStream.close();
assertEquals(0, mockInputStream.getBytesToRead());
}

private class MockInputStream extends InputStream {
private int bytesToRead;

MockInputStream(byte[] data) {
this.bytesToRead = data.length;
}

@Override
public int read() throws IOException {
if (bytesToRead == 0) {
return -1;
}
bytesToRead--;
return 1;
}

int getBytesToRead() {
return bytesToRead;
}
}
}
Expand Up @@ -26,10 +26,12 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import junit.framework.TestCase;

/**
Expand Down Expand Up @@ -457,4 +459,40 @@ public LowLevelHttpResponse execute() throws IOException {
"it should not decompress stream",
request.execute().getContent() instanceof GZIPInputStream);
}

public void testGetContent_gzipEncoding_finishReading() throws IOException {
byte[] dataToCompress = "abcd".getBytes(StandardCharsets.UTF_8);
byte[] mockBytes;
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToCompress.length)) {
GZIPOutputStream zipStream = new GZIPOutputStream((byteStream));
zipStream.write(dataToCompress);
zipStream.close();
mockBytes = byteStream.toByteArray();
}
final MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse();
mockResponse.setContent(mockBytes);
mockResponse.setContentEncoding("gzip");
mockResponse.setContentType("text/plain");

HttpTransport transport =
new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, final String url)
throws IOException {
return new MockLowLevelHttpRequest() {
@Override
public LowLevelHttpResponse execute() throws IOException {
return mockResponse;
}
};
}
};
HttpRequest request =
transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
HttpResponse response = request.execute();
TestableByteArrayInputStream output = (TestableByteArrayInputStream) mockResponse.getContent();
assertFalse(output.isClosed());
assertEquals("abcd", response.parseAsString());
assertTrue(output.isClosed());
}
}