Redesign of HTTP cache resource APIs

This commit is contained in:
Oleg Kalnichevski 2017-09-23 18:15:40 +02:00
parent e2a464084c
commit 73c67f221d
14 changed files with 257 additions and 611 deletions

View File

@ -26,7 +26,7 @@
*/
package org.apache.hc.client5.http.cache;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Serializable;
@ -36,25 +36,40 @@ import java.io.Serializable;
*
* @since 4.1
*/
public interface Resource extends Serializable {
public abstract class Resource implements Serializable {
/**
* Returns an {@link InputStream} from which the response
* body can be read.
* @throws IOException
* Returns resource content as a {@link InputStream}.
*
* @throws ResourceIOException
*/
InputStream getInputStream() throws IOException;
public InputStream getInputStream() throws ResourceIOException {
return new ByteArrayInputStream(get());
}
/**
* Returns resource content as a byte array.
* <p>
* Please note for memory efficiency some resource implementations
* may return a reference to the underlying byte array. The returned
* value should be treated as immutable.
*
* @throws ResourceIOException
*
* @since 5.0
*/
public abstract byte[] get() throws ResourceIOException;
/**
* Returns the length in bytes of the response body.
*/
long length();
public abstract long length();
/**
* Indicates the system no longer needs to keep this
* response body and any system resources associated with
* it may be reclaimed.
*/
void dispose();
public abstract void dispose();
}

View File

@ -26,9 +26,6 @@
*/
package org.apache.hc.client5.http.cache;
import java.io.IOException;
import java.io.InputStream;
/**
* Generates {@link Resource} instances for handling cached
* HTTP response bodies.
@ -39,20 +36,25 @@ public interface ResourceFactory {
/**
* Creates a {@link Resource} from a given response body.
* @param requestId a unique identifier for this particular
* response body
* @param instream the original {@link InputStream}
* containing the response body of the origin HTTP response.
* @param limit maximum number of bytes to consume of the
* response body; if this limit is reached before the
* response body is fully consumed, mark the limit has
* having been reached and return a {@code Resource}
* containing the data read to that point.
* @param requestId a unique identifier for this particular response body.
* @param content byte array that represents the origin HTTP response body.
* @return a {@code Resource} containing however much of
* the response body was successfully read.
* @throws IOException
* @throws ResourceIOException
*/
Resource generate(String requestId, InputStream instream, InputLimit limit) throws IOException;
Resource generate(String requestId, byte[] content) throws ResourceIOException;
/**
* Creates a {@link Resource} from a given response body.
* @param requestId a unique identifier for this particular response body.
* @param content byte array that represents the origin HTTP response body.
* @param off the start offset in the array.
* @param len the number of bytes to read from the array.
* @return a {@code Resource} containing however much of
* the response body was successfully read.
* @throws ResourceIOException
*/
Resource generate(String requestId, byte[] content, int off, int len) throws ResourceIOException;
/**
* Clones an existing {@link Resource}.
@ -60,8 +62,8 @@ public interface ResourceFactory {
* with the cloned response body.
* @param resource the original response body to clone.
* @return the {@code Resource} copy
* @throws IOException
* @throws ResourceIOException
*/
Resource copy(String requestId, Resource resource) throws IOException;
Resource copy(String requestId, Resource resource) throws ResourceIOException;
}

View File

@ -26,48 +26,20 @@
*/
package org.apache.hc.client5.http.cache;
import java.io.IOException;
/**
* Used to limiting the size of an incoming response body of
* unknown size that is optimistically being read in anticipation
* of caching it.
* @since 4.1
* Signals a generic resource I/O error.
*/
public class InputLimit {
public class ResourceIOException extends IOException {
private final long value;
private boolean reached;
/**
* Create a limit for how many bytes of a response body to
* read.
* @param value maximum length in bytes
*/
public InputLimit(final long value) {
public ResourceIOException(final String message) {
super();
this.value = value;
this.reached = false;
}
/**
* Returns the current maximum limit that was set on
* creation.
*/
public long getValue() {
return this.value;
}
/**
* Used to report that the limit has been reached.
*/
public void reached() {
this.reached = true;
}
/**
* Returns {@code true} if the input limit has been reached.
*/
public boolean isReached() {
return this.reached;
public ResourceIOException(final String message, final Throwable cause) {
super(message);
initCause(cause);
}
}

View File

@ -27,6 +27,7 @@
package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
@ -44,6 +45,7 @@ import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
@ -51,6 +53,7 @@ import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.util.ByteArrayBuffer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -288,41 +291,38 @@ class BasicHttpCache implements HttpCache {
final ClassicHttpResponse originResponse,
final Date requestSent,
final Date responseReceived) throws IOException {
boolean closeOriginResponse = true;
final SizeLimitedResponseReader responseReader = getResponseReader(request, originResponse);
try {
responseReader.readResponse();
if (responseReader.isLimitReached()) {
closeOriginResponse = false;
return responseReader.getReconstructedResponse();
}
final Resource resource = responseReader.getResource();
if (isIncompleteResponse(originResponse, resource)) {
return generateIncompleteResponseError(originResponse, resource);
}
final HttpCacheEntry entry = new HttpCacheEntry(
requestSent,
responseReceived,
originResponse.getCode(),
originResponse.getAllHeaders(),
resource);
storeInCache(host, request, entry);
return responseGenerator.generateResponse(request, entry);
} finally {
if (closeOriginResponse) {
originResponse.close();
final Resource resource;
final HttpEntity entity = originResponse.getEntity();
if (entity != null) {
final ByteArrayBuffer buf = new ByteArrayBuffer(1024);
final InputStream instream = entity.getContent();
final byte[] tmp = new byte[2048];
long total = 0;
int l;
while ((l = instream.read(tmp)) != -1) {
buf.append(tmp, 0, l);
total += l;
if (total > maxObjectSizeBytes) {
originResponse.setEntity(new CombinedEntity(entity, buf));
return originResponse;
}
}
resource = resourceFactory.generate(request.getRequestUri(), buf.array(), 0, buf.length());
} else {
resource = null;
}
}
SizeLimitedResponseReader getResponseReader(final HttpRequest request,
final ClassicHttpResponse backEndResponse) {
return new SizeLimitedResponseReader(
resourceFactory, maxObjectSizeBytes, request, backEndResponse);
originResponse.close();
if (isIncompleteResponse(originResponse, resource)) {
return generateIncompleteResponseError(originResponse, resource);
}
final HttpCacheEntry entry = new HttpCacheEntry(
requestSent,
responseReceived,
originResponse.getCode(),
originResponse.getAllHeaders(),
resource);
storeInCache(host, request, entry);
return responseGenerator.generateResponse(request, entry);
}
@Override

View File

@ -298,7 +298,7 @@ public class CachingExec implements ExecChainHandler {
final ExecChain.Scope scope,
final ExecChain chain,
final HttpCacheEntry entry,
final Date now) throws HttpException {
final Date now) throws HttpException, IOException {
final HttpClientContext context = scope.clientContext;
try {

View File

@ -26,26 +26,31 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.FilterInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.List;
import java.util.Set;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
import org.apache.hc.core5.function.Supplier;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.ByteArrayBuffer;
class CombinedEntity extends AbstractHttpEntity {
class CombinedEntity implements HttpEntity {
private final Resource resource;
private final HttpEntity entity;
private final InputStream combinedStream;
CombinedEntity(final Resource resource, final InputStream instream) throws IOException {
CombinedEntity(final HttpEntity entity, final ByteArrayBuffer buf) throws IOException {
super();
this.resource = resource;
this.entity = entity;
this.combinedStream = new SequenceInputStream(
new ResourceStream(resource.getInputStream()), instream);
new ByteArrayInputStream(buf.array(), 0, buf.length()),
entity.getContent());
}
@Override
@ -53,6 +58,21 @@ class CombinedEntity extends AbstractHttpEntity {
return -1;
}
@Override
public String getContentType() {
return entity.getContentType();
}
@Override
public String getContentEncoding() {
return entity.getContentEncoding();
}
@Override
public boolean isChunked() {
return true;
}
@Override
public boolean isRepeatable() {
return false;
@ -68,6 +88,16 @@ class CombinedEntity extends AbstractHttpEntity {
return this.combinedStream;
}
@Override
public Set<String> getTrailerNames() {
return entity.getTrailerNames();
}
@Override
public Supplier<List<? extends Header>> getTrailers() {
return entity.getTrailers();
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
Args.notNull(outstream, "Output stream");
@ -82,28 +112,11 @@ class CombinedEntity extends AbstractHttpEntity {
@Override
public void close() throws IOException {
dispose();
}
private void dispose() {
this.resource.dispose();
}
class ResourceStream extends FilterInputStream {
protected ResourceStream(final InputStream in) {
super(in);
try {
combinedStream.close();
} finally {
entity.close();
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
dispose();
}
}
}
}

View File

@ -28,12 +28,17 @@ package org.apache.hc.client5.http.impl.cache;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.ByteArrayBuffer;
/**
* Cache resource backed by a file.
@ -41,41 +46,70 @@ import org.apache.hc.core5.annotation.ThreadingBehavior;
* @since 4.1
*/
@Contract(threading = ThreadingBehavior.SAFE)
public class FileResource implements Resource {
public class FileResource extends Resource {
private static final long serialVersionUID = 4132244415919043397L;
private final File file;
private volatile boolean disposed;
private final AtomicReference<File> fileRef;
public FileResource(final File file) {
super();
this.file = file;
this.disposed = false;
this.fileRef = new AtomicReference<>(Args.notNull(file, "File"));
}
synchronized File getFile() {
return this.file;
File getFile() {
return this.fileRef.get();
}
@Override
public synchronized InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
@Override
public synchronized long length() {
return this.file.length();
}
@Override
public synchronized void dispose() {
if (this.disposed) {
return;
public byte[] get() throws ResourceIOException {
final File file = this.fileRef.get();
if (file == null) {
throw new ResourceIOException("Resouce already dispoased");
}
try (final InputStream in = new FileInputStream(file)) {
final ByteArrayBuffer buf = new ByteArrayBuffer(1024);
final byte[] tmp = new byte[2048];
int len;
while ((len = in.read(tmp)) != -1) {
buf.append(tmp, 0, len);
}
return buf.toByteArray();
} catch (final IOException ex) {
throw new ResourceIOException(ex.getMessage(), ex);
}
}
@Override
public InputStream getInputStream() throws ResourceIOException {
final File file = this.fileRef.get();
if (file != null) {
try {
return new FileInputStream(file);
} catch (final FileNotFoundException ex) {
throw new ResourceIOException(ex.getMessage(), ex);
}
} else {
throw new ResourceIOException("Resouce already dispoased");
}
}
@Override
public long length() {
final File file = this.fileRef.get();
if (file != null) {
return file.length();
} else {
return -1;
}
}
@Override
public void dispose() {
final File file = this.fileRef.getAndSet(null);
if (file != null) {
file.delete();
}
this.disposed = true;
this.file.delete();
}
}

View File

@ -29,13 +29,13 @@ package org.apache.hc.client5.http.impl.cache;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.hc.client5.http.cache.InputLimit;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.util.Args;
/**
* Generates {@link Resource} instances whose body is stored in a temporary file.
@ -73,37 +73,41 @@ public class FileResourceFactory implements ResourceFactory {
@Override
public Resource generate(
final String requestId,
final InputStream instream,
final InputLimit limit) throws IOException {
final byte[] content, final int off, final int len) throws ResourceIOException {
Args.notNull(requestId, "Request id");
final File file = generateUniqueCacheFile(requestId);
try (FileOutputStream outstream = new FileOutputStream(file)) {
final byte[] buf = new byte[2048];
long total = 0;
int l;
while ((l = instream.read(buf)) != -1) {
outstream.write(buf, 0, l);
total += l;
if (limit != null && total > limit.getValue()) {
limit.reached();
break;
}
if (content != null) {
outstream.write(content, off, len);
}
} catch (final IOException ex) {
throw new ResourceIOException(ex.getMessage(), ex);
}
return new FileResource(file);
}
@Override
public Resource generate(final String requestId, final byte[] content) throws ResourceIOException {
Args.notNull(content, "Content");
return generate(requestId, content, 0, content.length);
}
@Override
public Resource copy(
final String requestId,
final Resource resource) throws IOException {
final Resource resource) throws ResourceIOException {
final File file = generateUniqueCacheFile(requestId);
if (resource instanceof FileResource) {
final File src = ((FileResource) resource).getFile();
IOUtils.copyFile(src, file);
} else {
final FileOutputStream out = new FileOutputStream(file);
IOUtils.copyAndClose(resource.getInputStream(), out);
try {
if (resource instanceof FileResource) {
final File src = ((FileResource) resource).getFile();
IOUtils.copyFile(src, file);
} else {
final FileOutputStream out = new FileOutputStream(file);
IOUtils.copyAndClose(resource.getInputStream(), out);
}
} catch (final IOException ex) {
throw new ResourceIOException(ex.getMessage(), ex);
}
return new FileResource(file);
}

View File

@ -26,10 +26,10 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
@ -39,33 +39,40 @@ import org.apache.hc.core5.annotation.ThreadingBehavior;
* @since 4.1
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class HeapResource implements Resource {
public class HeapResource extends Resource {
private static final long serialVersionUID = -2078599905620463394L;
private final byte[] b;
private final AtomicReference<byte[]> arrayRef;
public HeapResource(final byte[] b) {
super();
this.b = b;
}
byte[] getByteArray() {
return this.b;
this.arrayRef = new AtomicReference<>(b);
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(this.b);
public byte[] get() throws ResourceIOException {
final byte[] byteArray = this.arrayRef.get();
if (byteArray != null) {
return byteArray;
} else {
throw new ResourceIOException("Resouce already dispoased");
}
}
@Override
public long length() {
return this.b.length;
final byte[] byteArray = this.arrayRef.get();
if (byteArray != null) {
return byteArray.length;
} else {
return -1;
}
}
@Override
public void dispose() {
this.arrayRef.set(null);
}
}

View File

@ -26,15 +26,12 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.hc.client5.http.cache.InputLimit;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.util.Args;
/**
* Generates {@link Resource} instances stored entirely in heap.
@ -47,40 +44,23 @@ public class HeapResourceFactory implements ResourceFactory {
@Override
public Resource generate(
final String requestId,
final InputStream instream,
final InputLimit limit) throws IOException {
final ByteArrayOutputStream outstream = new ByteArrayOutputStream();
final byte[] buf = new byte[2048];
long total = 0;
int l;
while ((l = instream.read(buf)) != -1) {
outstream.write(buf, 0, l);
total += l;
if (limit != null && total > limit.getValue()) {
limit.reached();
break;
}
}
return createResource(outstream.toByteArray());
final byte[] content, final int off, final int len) throws ResourceIOException {
final byte[] copy = new byte[len];
System.arraycopy(content, off, copy, 0, len);
return new HeapResource(copy);
}
@Override
public Resource generate(final String requestId, final byte[] content) {
return new HeapResource(content != null ? content.clone() : null);
}
@Override
public Resource copy(
final String requestId,
final Resource resource) throws IOException {
final byte[] body;
if (resource instanceof HeapResource) {
body = ((HeapResource) resource).getByteArray();
} else {
final ByteArrayOutputStream outstream = new ByteArrayOutputStream();
IOUtils.copyAndClose(resource.getInputStream(), outstream);
body = outstream.toByteArray();
}
return createResource(body);
}
Resource createResource(final byte[] buf) {
return new HeapResource(buf);
final Resource resource) throws ResourceIOException {
Args.notNull(resource, "Resource");
return new HeapResource(resource.get());
}
}

View File

@ -1,146 +0,0 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.io.InputStream;
import org.apache.hc.client5.http.cache.InputLimit;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
/**
* @since 4.1
*/
class SizeLimitedResponseReader {
private final ResourceFactory resourceFactory;
private final long maxResponseSizeBytes;
private final HttpRequest request;
private final ClassicHttpResponse response;
private InputStream instream;
private InputLimit limit;
private Resource resource;
private boolean consumed;
/**
* Create an {@link ClassicHttpResponse} that is limited in size, this allows for checking
* the size of objects that will be stored in the cache.
*/
public SizeLimitedResponseReader(
final ResourceFactory resourceFactory,
final long maxResponseSizeBytes,
final HttpRequest request,
final ClassicHttpResponse response) {
super();
this.resourceFactory = resourceFactory;
this.maxResponseSizeBytes = maxResponseSizeBytes;
this.request = request;
this.response = response;
}
protected void readResponse() throws IOException {
if (!consumed) {
doConsume();
}
}
private void ensureNotConsumed() {
if (consumed) {
throw new IllegalStateException("Response has already been consumed");
}
}
private void ensureConsumed() {
if (!consumed) {
throw new IllegalStateException("Response has not been consumed");
}
}
private void doConsume() throws IOException {
ensureNotConsumed();
consumed = true;
limit = new InputLimit(maxResponseSizeBytes);
final HttpEntity entity = response.getEntity();
if (entity == null) {
return;
}
final String uri = request.getRequestUri();
instream = entity.getContent();
try {
resource = resourceFactory.generate(uri, instream, limit);
} finally {
if (!limit.isReached()) {
instream.close();
}
}
}
boolean isLimitReached() {
ensureConsumed();
return limit.isReached();
}
Resource getResource() {
ensureConsumed();
return resource;
}
ClassicHttpResponse getReconstructedResponse() throws IOException {
ensureConsumed();
final ClassicHttpResponse reconstructed = new BasicClassicHttpResponse(response.getCode()) {
@Override
public void close() throws IOException {
try {
super.close();
} finally {
response.close();
}
}
};
reconstructed.setHeaders(response.getAllHeaders());
final CombinedEntity combinedEntity = new CombinedEntity(resource, instream);
final HttpEntity entity = response.getEntity();
if (entity != null) {
combinedEntity.setContentType(entity.getContentType());
combinedEntity.setContentEncoding(entity.getContentEncoding());
combinedEntity.setChunked(entity.isChunked());
}
reconstructed.setEntity(combinedEntity);
return reconstructed;
}
}

View File

@ -35,7 +35,6 @@ import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
@ -359,7 +358,7 @@ public class TestBasicHttpCache {
final ClassicHttpResponse result = impl.cacheAndReturnResponse(host, request, originResponse, requestSent, responseReceived);
assertEquals(0, backing.map.size());
assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
assertSame(originResponse, result);
}
@ -521,53 +520,10 @@ public class TestBasicHttpCache {
assertTrue(inputStream.wasClosed());
}
static class DisposableResource implements Resource {
private static final long serialVersionUID = 1L;
private final byte[] b;
private boolean dispoased;
public DisposableResource(final byte[] b) {
super();
this.b = b;
}
byte[] getByteArray() {
return this.b;
}
@Override
public InputStream getInputStream() throws IOException {
if (dispoased) {
throw new IOException("Already dispoased");
}
return new ByteArrayInputStream(this.b);
}
@Override
public long length() {
return this.b.length;
}
@Override
public void dispose() {
this.dispoased = true;
}
}
@Test
public void testEntryUpdate() throws Exception {
final HeapResourceFactory rf = new HeapResourceFactory() {
@Override
Resource createResource(final byte[] buf) {
return new DisposableResource(buf);
}
};
final HeapResourceFactory rf = new HeapResourceFactory();
impl = new BasicHttpCache(rf, backing, CacheConfig.DEFAULT);

View File

@ -32,8 +32,9 @@ import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.ByteArrayBuffer;
import org.junit.Assert;
import org.junit.Test;
@ -41,21 +42,25 @@ public class TestCombinedEntity {
@Test
public void testCombinedEntityBasics() throws Exception {
final Resource resource = mock(Resource.class);
when(resource.getInputStream()).thenReturn(
new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }));
final HttpEntity httpEntity = mock(HttpEntity.class);
when(httpEntity.getContent()).thenReturn(
new ByteArrayInputStream(new byte[] { 6, 7, 8, 9, 10 }));
final ByteArrayInputStream instream = new ByteArrayInputStream(new byte[] { 6, 7, 8, 9, 10 });
final CombinedEntity entity = new CombinedEntity(resource, instream);
final ByteArrayBuffer buf = new ByteArrayBuffer(1024);
final byte[] tmp = new byte[] { 1, 2, 3, 4, 5 };
buf.append(tmp, 0, tmp.length);
final CombinedEntity entity = new CombinedEntity(httpEntity, buf);
Assert.assertEquals(-1, entity.getContentLength());
Assert.assertFalse(entity.isRepeatable());
Assert.assertTrue(entity.isStreaming());
final byte[] result = EntityUtils.toByteArray(entity);
Assert.assertArrayEquals(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, result);
Assert.assertArrayEquals(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, EntityUtils.toByteArray(entity));
verify(resource).getInputStream();
verify(resource).dispose();
verify(httpEntity).getContent();
entity.close();
verify(httpEntity).close();
}
}

View File

@ -1,196 +0,0 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class TestSizeLimitedResponseReader {
private static final long MAX_SIZE = 4;
private HttpRequest request;
private SizeLimitedResponseReader impl;
@Before
public void setUp() {
request = new HttpGet("http://foo.example.com/bar");
}
@Test
public void testLargeResponseIsTooLarge() throws Exception {
final byte[] buf = new byte[] { 1, 2, 3, 4, 5 };
final ClassicHttpResponse response = make200Response(buf);
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
final ClassicHttpResponse result = impl.getReconstructedResponse();
final byte[] body = EntityUtils.toByteArray(result.getEntity());
Assert.assertTrue(tooLarge);
Assert.assertArrayEquals(buf, body);
}
@Test
public void testExactSizeResponseIsNotTooLarge() throws Exception {
final byte[] buf = new byte[] { 1, 2, 3, 4 };
final ClassicHttpResponse response = make200Response(buf);
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
final ClassicHttpResponse reconstructed = impl.getReconstructedResponse();
final byte[] result = EntityUtils.toByteArray(reconstructed.getEntity());
Assert.assertFalse(tooLarge);
Assert.assertArrayEquals(buf, result);
}
@Test
public void testSmallResponseIsNotTooLarge() throws Exception {
final byte[] buf = new byte[] { 1, 2, 3 };
final ClassicHttpResponse response = make200Response(buf);
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
final ClassicHttpResponse reconstructed = impl.getReconstructedResponse();
final byte[] result = EntityUtils.toByteArray(reconstructed.getEntity());
Assert.assertFalse(tooLarge);
Assert.assertArrayEquals(buf, result);
}
@Test
public void testResponseWithNoEntityIsNotTooLarge() throws Exception {
final ClassicHttpResponse response = make200Response();
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
Assert.assertFalse(tooLarge);
}
@Test
public void testTooLargeEntityHasOriginalContentTypes() throws Exception {
final ClassicHttpResponse response = make200Response();
final StringEntity entity = new StringEntity("large entity content");
response.setEntity(entity);
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
final ClassicHttpResponse result = impl.getReconstructedResponse();
final HttpEntity reconstructedEntity = result.getEntity();
Assert.assertEquals(entity.getContentEncoding(), reconstructedEntity.getContentEncoding());
Assert.assertEquals(entity.getContentType(), reconstructedEntity.getContentType());
final String content = EntityUtils.toString(reconstructedEntity);
Assert.assertTrue(tooLarge);
Assert.assertEquals("large entity content", content);
}
@Test
public void testTooLargeResponseCombinedClosed() throws Exception {
final AtomicBoolean closed = new AtomicBoolean(false);
final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK") {
@Override
public void close() throws IOException {
closed.set(true);
}
};
final StringEntity entity = new StringEntity("large entity content");
response.setEntity(entity);
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
try (ClassicHttpResponse result = impl.getReconstructedResponse()) {
final HttpEntity reconstructedEntity = result.getEntity();
Assert.assertEquals(entity.getContentEncoding(), reconstructedEntity.getContentEncoding());
Assert.assertEquals(entity.getContentType(), reconstructedEntity.getContentType());
Assert.assertFalse(closed.get());
final String content = EntityUtils.toString(reconstructedEntity);
Assert.assertTrue(tooLarge);
Assert.assertEquals("large entity content", content);
}
Assert.assertTrue(closed.get());
}
@Test
public void testResponseCopiesAllOriginalHeaders() throws Exception {
final byte[] buf = new byte[] { 1, 2, 3 };
final ClassicHttpResponse response = make200Response(buf);
response.setHeader("Content-Encoding", "gzip");
impl = new SizeLimitedResponseReader(new HeapResourceFactory(), MAX_SIZE, request, response);
impl.readResponse();
final boolean tooLarge = impl.isLimitReached();
final ClassicHttpResponse reconstructed = impl.getReconstructedResponse();
final byte[] result = EntityUtils.toByteArray(reconstructed.getEntity());
Assert.assertFalse(tooLarge);
Assert.assertArrayEquals(buf, result);
Assert.assertEquals("gzip", reconstructed.getFirstHeader("Content-Encoding").getValue());
}
private ClassicHttpResponse make200Response() {
return new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
}
private ClassicHttpResponse make200Response(final byte[] buf) {
final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
response.setEntity(new ByteArrayEntity(buf));
return response;
}
}