pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View file

@ -0,0 +1,178 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpException;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
/**
* Class used to represent an asynchronous revalidation event, such as with
* "stale-while-revalidate"
*/
class AsynchronousValidationRequest implements Runnable {
private final AsynchronousValidator parent;
private final CachingExec cachingExec;
private final HttpRoute route;
private final HttpRequestWrapper request;
private final HttpClientContext context;
private final HttpExecutionAware execAware;
private final HttpCacheEntry cacheEntry;
private final String identifier;
private final int consecutiveFailedAttempts;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
/**
* Used internally by {@link AsynchronousValidator} to schedule a
* revalidation.
* @param request
* @param context
* @param cacheEntry
* @param identifier
* @param consecutiveFailedAttempts
*/
AsynchronousValidationRequest(
final AsynchronousValidator parent,
final CachingExec cachingExec,
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry cacheEntry,
final String identifier,
final int consecutiveFailedAttempts) {
this.parent = parent;
this.cachingExec = cachingExec;
this.route = route;
this.request = request;
this.context = context;
this.execAware = execAware;
this.cacheEntry = cacheEntry;
this.identifier = identifier;
this.consecutiveFailedAttempts = consecutiveFailedAttempts;
}
public void run() {
try {
if (revalidateCacheEntry()) {
parent.jobSuccessful(identifier);
} else {
parent.jobFailed(identifier);
}
} finally {
parent.markComplete(identifier);
}
}
/**
* Revalidate the cache entry and return if the operation was successful.
* Success means a connection to the server was established and replay did
* not indicate a server error.
* @return <code>true</code> if the cache entry was successfully validated;
* otherwise <code>false</code>
*/
protected boolean revalidateCacheEntry() {
try {
final CloseableHttpResponse httpResponse = cachingExec.revalidateCacheEntry(route, request, context, execAware, cacheEntry);
try {
final int statusCode = httpResponse.getStatusLine().getStatusCode();
return isNotServerError(statusCode) && isNotStale(httpResponse);
} finally {
httpResponse.close();
}
} catch (final IOException ioe) {
log.debug("Asynchronous revalidation failed due to I/O error", ioe);
return false;
} catch (final HttpException pe) {
log.error("HTTP protocol exception during asynchronous revalidation", pe);
return false;
} catch (final RuntimeException re) {
log.error("RuntimeException thrown during asynchronous revalidation: " + re);
return false;
}
}
/**
* Return whether the status code indicates a server error or not.
* @param statusCode the status code to be checked
* @return if the status code indicates a server error or not
*/
private boolean isNotServerError(final int statusCode) {
return statusCode < 500;
}
/**
* Try to detect if the returned response is generated from a stale cache entry.
* @param httpResponse the response to be checked
* @return whether the response is stale or not
*/
private boolean isNotStale(final HttpResponse httpResponse) {
final Header[] warnings = httpResponse.getHeaders(HeaderConstants.WARNING);
if (warnings != null)
{
for (final Header warning : warnings)
{
/**
* warn-codes
* 110 = Response is stale
* 111 = Revalidation failed
*/
final String warningValue = warning.getValue();
if (warningValue.startsWith("110") || warningValue.startsWith("111"))
{
return false;
}
}
}
return true;
}
String getIdentifier() {
return identifier;
}
/**
* The number of consecutively failed revalidation attempts.
* @return the number of consecutively failed revalidation attempts.
*/
public int getConsecutiveFailedAttempts() {
return consecutiveFailedAttempts;
}
}

View file

@ -0,0 +1,150 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
/**
* Class used for asynchronous revalidations to be used when the "stale-
* while-revalidate" directive is present
*/
class AsynchronousValidator implements Closeable {
private final SchedulingStrategy schedulingStrategy;
private final Set<String> queued;
private final CacheKeyGenerator cacheKeyGenerator;
private final FailureCache failureCache;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
/**
* Create AsynchronousValidator which will make revalidation requests
* using an {@link ImmediateSchedulingStrategy}. Its thread
* pool will be configured according to the given {@link CacheConfig}.
* @param config specifies thread pool settings. See
* {@link CacheConfig#getAsynchronousWorkersMax()},
* {@link CacheConfig#getAsynchronousWorkersCore()},
* {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
* and {@link CacheConfig#getRevalidationQueueSize()}.
*/
public AsynchronousValidator(final CacheConfig config) {
this(new ImmediateSchedulingStrategy(config));
}
/**
* Create AsynchronousValidator which will make revalidation requests
* using the supplied {@link SchedulingStrategy}. Closing the validator
* will also close the given schedulingStrategy.
* @param schedulingStrategy used to maintain a pool of worker threads and
* schedules when requests are executed
*/
AsynchronousValidator(final SchedulingStrategy schedulingStrategy) {
this.schedulingStrategy = schedulingStrategy;
this.queued = new HashSet<String>();
this.cacheKeyGenerator = new CacheKeyGenerator();
this.failureCache = new DefaultFailureCache();
}
public void close() throws IOException {
schedulingStrategy.close();
}
/**
* Schedules an asynchronous revalidation
*/
public synchronized void revalidateCacheEntry(
final CachingExec cachingExec,
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry entry) {
// getVariantURI will fall back on getURI if no variants exist
final String uri = cacheKeyGenerator.getVariantURI(context.getTargetHost(), request, entry);
if (!queued.contains(uri)) {
final int consecutiveFailedAttempts = failureCache.getErrorCount(uri);
final AsynchronousValidationRequest revalidationRequest =
new AsynchronousValidationRequest(
this, cachingExec, route, request, context, execAware, entry, uri, consecutiveFailedAttempts);
try {
schedulingStrategy.schedule(revalidationRequest);
queued.add(uri);
} catch (final RejectedExecutionException ree) {
log.debug("Revalidation for [" + uri + "] not scheduled: " + ree);
}
}
}
/**
* Removes an identifier from the internal list of revalidation jobs in
* progress. This is meant to be called by
* {@link AsynchronousValidationRequest#run()} once the revalidation is
* complete, using the identifier passed in during constructions.
* @param identifier
*/
synchronized void markComplete(final String identifier) {
queued.remove(identifier);
}
/**
* The revalidation job was successful thus the number of consecutive
* failed attempts will be reset to zero. Should be called by
* {@link AsynchronousValidationRequest#run()}.
* @param identifier the revalidation job's unique identifier
*/
void jobSuccessful(final String identifier) {
failureCache.resetErrorCount(identifier);
}
/**
* The revalidation job did fail and thus the number of consecutive failed
* attempts will be increased. Should be called by
* {@link AsynchronousValidationRequest#run()}.
* @param identifier the revalidation job's unique identifier
*/
void jobFailed(final String identifier) {
failureCache.increaseErrorCount(identifier);
}
Set<String> getScheduledIdentifiers() {
return Collections.unmodifiableSet(queued);
}
}

View file

@ -0,0 +1,376 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateException;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
import ch.boye.httpclientandroidlib.protocol.HTTP;
class BasicHttpCache implements HttpCache {
private static final Set<String> safeRequestMethods = new HashSet<String>(
Arrays.asList(HeaderConstants.HEAD_METHOD,
HeaderConstants.GET_METHOD, HeaderConstants.OPTIONS_METHOD,
HeaderConstants.TRACE_METHOD));
private final CacheKeyGenerator uriExtractor;
private final ResourceFactory resourceFactory;
private final long maxObjectSizeBytes;
private final CacheEntryUpdater cacheEntryUpdater;
private final CachedHttpResponseGenerator responseGenerator;
private final HttpCacheInvalidator cacheInvalidator;
private final HttpCacheStorage storage;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
public BasicHttpCache(
final ResourceFactory resourceFactory,
final HttpCacheStorage storage,
final CacheConfig config,
final CacheKeyGenerator uriExtractor,
final HttpCacheInvalidator cacheInvalidator) {
this.resourceFactory = resourceFactory;
this.uriExtractor = uriExtractor;
this.cacheEntryUpdater = new CacheEntryUpdater(resourceFactory);
this.maxObjectSizeBytes = config.getMaxObjectSize();
this.responseGenerator = new CachedHttpResponseGenerator();
this.storage = storage;
this.cacheInvalidator = cacheInvalidator;
}
public BasicHttpCache(
final ResourceFactory resourceFactory,
final HttpCacheStorage storage,
final CacheConfig config,
final CacheKeyGenerator uriExtractor) {
this( resourceFactory, storage, config, uriExtractor,
new CacheInvalidator(uriExtractor, storage));
}
public BasicHttpCache(
final ResourceFactory resourceFactory,
final HttpCacheStorage storage,
final CacheConfig config) {
this( resourceFactory, storage, config, new CacheKeyGenerator());
}
public BasicHttpCache(final CacheConfig config) {
this(new HeapResourceFactory(), new BasicHttpCacheStorage(config), config);
}
public BasicHttpCache() {
this(CacheConfig.DEFAULT);
}
public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request)
throws IOException {
if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
final String uri = uriExtractor.getURI(host, request);
storage.removeEntry(uri);
}
}
public void flushInvalidatedCacheEntriesFor(final HttpHost host, final HttpRequest request, final HttpResponse response) {
if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
cacheInvalidator.flushInvalidatedCacheEntries(host, request, response);
}
}
void storeInCache(
final HttpHost target, final HttpRequest request, final HttpCacheEntry entry) throws IOException {
if (entry.hasVariants()) {
storeVariantEntry(target, request, entry);
} else {
storeNonVariantEntry(target, request, entry);
}
}
void storeNonVariantEntry(
final HttpHost target, final HttpRequest req, final HttpCacheEntry entry) throws IOException {
final String uri = uriExtractor.getURI(target, req);
storage.putEntry(uri, entry);
}
void storeVariantEntry(
final HttpHost target,
final HttpRequest req,
final HttpCacheEntry entry) throws IOException {
final String parentURI = uriExtractor.getURI(target, req);
final String variantURI = uriExtractor.getVariantURI(target, req, entry);
storage.putEntry(variantURI, entry);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
public HttpCacheEntry update(final HttpCacheEntry existing) throws IOException {
return doGetUpdatedParentEntry(
req.getRequestLine().getUri(), existing, entry,
uriExtractor.getVariantKey(req, entry),
variantURI);
}
};
try {
storage.updateEntry(parentURI, callback);
} catch (final HttpCacheUpdateException e) {
log.warn("Could not update key [" + parentURI + "]", e);
}
}
public void reuseVariantEntryFor(final HttpHost target, final HttpRequest req,
final Variant variant) throws IOException {
final String parentCacheKey = uriExtractor.getURI(target, req);
final HttpCacheEntry entry = variant.getEntry();
final String variantKey = uriExtractor.getVariantKey(req, entry);
final String variantCacheKey = variant.getCacheKey();
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
public HttpCacheEntry update(final HttpCacheEntry existing)
throws IOException {
return doGetUpdatedParentEntry(req.getRequestLine().getUri(),
existing, entry, variantKey, variantCacheKey);
}
};
try {
storage.updateEntry(parentCacheKey, callback);
} catch (final HttpCacheUpdateException e) {
log.warn("Could not update key [" + parentCacheKey + "]", e);
}
}
boolean isIncompleteResponse(final HttpResponse resp, final Resource resource) {
final int status = resp.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK
&& status != HttpStatus.SC_PARTIAL_CONTENT) {
return false;
}
final Header hdr = resp.getFirstHeader(HTTP.CONTENT_LEN);
if (hdr == null) {
return false;
}
final int contentLength;
try {
contentLength = Integer.parseInt(hdr.getValue());
} catch (final NumberFormatException nfe) {
return false;
}
return (resource.length() < contentLength);
}
CloseableHttpResponse generateIncompleteResponseError(
final HttpResponse response, final Resource resource) {
final int contentLength = Integer.parseInt(response.getFirstHeader(HTTP.CONTENT_LEN).getValue());
final HttpResponse error =
new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_GATEWAY, "Bad Gateway");
error.setHeader("Content-Type","text/plain;charset=UTF-8");
final String msg = String.format("Received incomplete response " +
"with Content-Length %d but actual body length %d",
contentLength, resource.length());
final byte[] msgBytes = msg.getBytes();
error.setHeader("Content-Length", Integer.toString(msgBytes.length));
error.setEntity(new ByteArrayEntity(msgBytes));
return Proxies.enhanceResponse(error);
}
HttpCacheEntry doGetUpdatedParentEntry(
final String requestId,
final HttpCacheEntry existing,
final HttpCacheEntry entry,
final String variantKey,
final String variantCacheKey) throws IOException {
HttpCacheEntry src = existing;
if (src == null) {
src = entry;
}
Resource resource = null;
if (src.getResource() != null) {
resource = resourceFactory.copy(requestId, src.getResource());
}
final Map<String,String> variantMap = new HashMap<String,String>(src.getVariantMap());
variantMap.put(variantKey, variantCacheKey);
return new HttpCacheEntry(
src.getRequestDate(),
src.getResponseDate(),
src.getStatusLine(),
src.getAllHeaders(),
resource,
variantMap);
}
public HttpCacheEntry updateCacheEntry(final HttpHost target, final HttpRequest request,
final HttpCacheEntry stale, final HttpResponse originResponse,
final Date requestSent, final Date responseReceived) throws IOException {
final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
request.getRequestLine().getUri(),
stale,
requestSent,
responseReceived,
originResponse);
storeInCache(target, request, updatedEntry);
return updatedEntry;
}
public HttpCacheEntry updateVariantCacheEntry(final HttpHost target, final HttpRequest request,
final HttpCacheEntry stale, final HttpResponse originResponse,
final Date requestSent, final Date responseReceived, final String cacheKey) throws IOException {
final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
request.getRequestLine().getUri(),
stale,
requestSent,
responseReceived,
originResponse);
storage.putEntry(cacheKey, updatedEntry);
return updatedEntry;
}
public HttpResponse cacheAndReturnResponse(final HttpHost host, final HttpRequest request,
final HttpResponse originResponse, final Date requestSent, final Date responseReceived)
throws IOException {
return cacheAndReturnResponse(host, request,
Proxies.enhanceResponse(originResponse), requestSent,
responseReceived);
}
public CloseableHttpResponse cacheAndReturnResponse(
final HttpHost host,
final HttpRequest request,
final CloseableHttpResponse 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.getStatusLine(),
originResponse.getAllHeaders(),
resource);
storeInCache(host, request, entry);
return responseGenerator.generateResponse(entry);
} finally {
if (closeOriginResponse) {
originResponse.close();
}
}
}
SizeLimitedResponseReader getResponseReader(final HttpRequest request,
final CloseableHttpResponse backEndResponse) {
return new SizeLimitedResponseReader(
resourceFactory, maxObjectSizeBytes, request, backEndResponse);
}
public HttpCacheEntry getCacheEntry(final HttpHost host, final HttpRequest request) throws IOException {
final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
if (root == null) {
return null;
}
if (!root.hasVariants()) {
return root;
}
final String variantCacheKey = root.getVariantMap().get(uriExtractor.getVariantKey(request, root));
if (variantCacheKey == null) {
return null;
}
return storage.getEntry(variantCacheKey);
}
public void flushInvalidatedCacheEntriesFor(final HttpHost host,
final HttpRequest request) throws IOException {
cacheInvalidator.flushInvalidatedCacheEntries(host, request);
}
public Map<String, Variant> getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request)
throws IOException {
final Map<String,Variant> variants = new HashMap<String,Variant>();
final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
if (root == null || !root.hasVariants()) {
return variants;
}
for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
final String variantKey = variant.getKey();
final String variantCacheKey = variant.getValue();
addVariantWithEtag(variantKey, variantCacheKey, variants);
}
return variants;
}
private void addVariantWithEtag(final String variantKey,
final String variantCacheKey, final Map<String, Variant> variants)
throws IOException {
final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
if (entry == null) {
return;
}
final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
if (etagHeader == null) {
return;
}
variants.put(etagHeader.getValue(), new Variant(variantKey, variantCacheKey, entry));
}
}

View file

@ -0,0 +1,96 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
/**
* Basic {@link HttpCacheStorage} implementation backed by an instance of
* {@link java.util.LinkedHashMap}. In other words, cache entries and
* the cached response bodies are held in-memory. This cache does NOT
* deallocate resources associated with the cache entries; it is intended
* for use with {@link HeapResource} and similar. This is the default cache
* storage backend used by {@link CachingHttpClients}.
*
* @since 4.1
*/
@ThreadSafe
public class BasicHttpCacheStorage implements HttpCacheStorage {
private final CacheMap entries;
public BasicHttpCacheStorage(final CacheConfig config) {
super();
this.entries = new CacheMap(config.getMaxCacheEntries());
}
/**
* Places a HttpCacheEntry in the cache
*
* @param url
* Url to use as the cache key
* @param entry
* HttpCacheEntry to place in the cache
*/
public synchronized void putEntry(final String url, final HttpCacheEntry entry) throws IOException {
entries.put(url, entry);
}
/**
* Gets an entry from the cache, if it exists
*
* @param url
* Url that is the cache key
* @return HttpCacheEntry if one exists, or null for cache miss
*/
public synchronized HttpCacheEntry getEntry(final String url) throws IOException {
return entries.get(url);
}
/**
* Removes a HttpCacheEntry from the cache
*
* @param url
* Url that is the cache key
*/
public synchronized void removeEntry(final String url) throws IOException {
entries.remove(url);
}
public synchronized void updateEntry(
final String url,
final HttpCacheUpdateCallback callback) throws IOException {
final HttpCacheEntry existingEntry = entries.get(url);
entries.put(url, callback.update(existingEntry));
}
}

View file

@ -0,0 +1,86 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Formatter;
import java.util.Locale;
import ch.boye.httpclientandroidlib.annotation.GuardedBy;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
/**
* Should produce reasonably unique tokens.
*/
@ThreadSafe
class BasicIdGenerator {
private final String hostname;
private final SecureRandom rnd;
@GuardedBy("this")
private long count;
public BasicIdGenerator() {
super();
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (final UnknownHostException ex) {
hostname = "localhost";
}
this.hostname = hostname;
try {
this.rnd = SecureRandom.getInstance("SHA1PRNG");
} catch (final NoSuchAlgorithmException ex) {
throw new Error(ex);
}
this.rnd.setSeed(System.currentTimeMillis());
}
public synchronized void generate(final StringBuilder buffer) {
this.count++;
final int rndnum = this.rnd.nextInt();
buffer.append(System.currentTimeMillis());
buffer.append('.');
final Formatter formatter = new Formatter(buffer, Locale.US);
formatter.format("%1$016x-%2$08x", this.count, rndnum);
formatter.close();
buffer.append('.');
buffer.append(this.hostname);
}
public String generate() {
final StringBuilder buffer = new StringBuilder();
generate(buffer);
return buffer.toString();
}
}

View file

@ -0,0 +1,764 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.util.Args;
/**
* <p>Java Beans-style configuration for a {@link CachingHttpClient}. Any class
* in the caching module that has configuration options should take a
* {@link CacheConfig} argument in one of its constructors. A
* {@code CacheConfig} instance has sane and conservative defaults, so the
* easiest way to specify options is to get an instance and then set just
* the options you want to modify from their defaults.</p>
*
* <p><b>N.B.</b> This class is only for caching-specific configuration; to
* configure the behavior of the rest of the client, configure the
* {@link ch.boye.httpclientandroidlib.client.HttpClient} used as the &quot;backend&quot;
* for the {@code CachingHttpClient}.</p>
*
* <p>Cache configuration can be grouped into the following categories:</p>
*
* <p><b>Cache size.</b> If the backend storage supports these limits, you
* can specify the {@link CacheConfig#getMaxCacheEntries maximum number of
* cache entries} as well as the {@link CacheConfig#getMaxObjectSizeBytes
* maximum cacheable response body size}.</p>
*
* <p><b>Public/private caching.</b> By default, the caching module considers
* itself to be a shared (public) cache, and will not, for example, cache
* responses to requests with {@code Authorization} headers or responses
* marked with {@code Cache-Control: private}. If, however, the cache
* is only going to be used by one logical "user" (behaving similarly to a
* browser cache), then you will want to {@link
* CacheConfig#setSharedCache(boolean) turn off the shared cache setting}.</p>
*
* <p><b>303 caching</b>. RFC2616 explicitly disallows caching 303 responses;
* however, the HTTPbis working group says they can be cached
* if explicitly indicated in the response headers and permitted by the request method.
* (They also indicate that disallowing 303 caching is actually an unintended
* spec error in RFC2616).
* This behavior is off by default, to err on the side of a conservative
* adherence to the existing standard, but you may want to
* {@link Builder#setAllow303Caching(boolean) enable it}.
*
* <p><b>Weak ETags on PUT/DELETE If-Match requests</b>. RFC2616 explicitly
* prohibits the use of weak validators in non-GET requests, however, the
* HTTPbis working group says while the limitation for weak validators on ranged
* requests makes sense, weak ETag validation is useful on full non-GET
* requests; e.g., PUT with If-Match. This behavior is off by default, to err on
* the side of a conservative adherence to the existing standard, but you may
* want to {@link Builder#setWeakETagOnPutDeleteAllowed(boolean) enable it}.
*
* <p><b>Heuristic caching</b>. Per RFC2616, a cache may cache certain cache
* entries even if no explicit cache control headers are set by the origin.
* This behavior is off by default, but you may want to turn this on if you
* are working with an origin that doesn't set proper headers but where you
* still want to cache the responses. You will want to {@link
* CacheConfig#setHeuristicCachingEnabled(boolean) enable heuristic caching},
* then specify either a {@link CacheConfig#getHeuristicDefaultLifetime()
* default freshness lifetime} and/or a {@link
* CacheConfig#setHeuristicCoefficient(float) fraction of the time since
* the resource was last modified}. See Sections
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.2">
* 13.2.2</a> and <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4">
* 13.2.4</a> of the HTTP/1.1 RFC for more details on heuristic caching.</p>
*
* <p><b>Background validation</b>. The cache module supports the
* {@code stale-while-revalidate} directive of
* <a href="http://tools.ietf.org/html/rfc5861">RFC5861</a>, which allows
* certain cache entry revalidations to happen in the background. You may
* want to tweak the settings for the {@link
* CacheConfig#getAsynchronousWorkersCore() minimum} and {@link
* CacheConfig#getAsynchronousWorkersMax() maximum} number of background
* worker threads, as well as the {@link
* CacheConfig#getAsynchronousWorkerIdleLifetimeSecs() maximum time they
* can be idle before being reclaimed}. You can also control the {@link
* CacheConfig#getRevalidationQueueSize() size of the queue} used for
* revalidations when there aren't enough workers to keep up with demand.</b>
*/
public class CacheConfig implements Cloneable {
/** Default setting for the maximum object size that will be
* cached, in bytes.
*/
public final static int DEFAULT_MAX_OBJECT_SIZE_BYTES = 8192;
/** Default setting for the maximum number of cache entries
* that will be retained.
*/
public final static int DEFAULT_MAX_CACHE_ENTRIES = 1000;
/** Default setting for the number of retries on a failed
* cache update
*/
public final static int DEFAULT_MAX_UPDATE_RETRIES = 1;
/** Default setting for 303 caching
*/
public final static boolean DEFAULT_303_CACHING_ENABLED = false;
/** Default setting to allow weak tags on PUT/DELETE methods
*/
public final static boolean DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED = false;
/** Default setting for heuristic caching
*/
public final static boolean DEFAULT_HEURISTIC_CACHING_ENABLED = false;
/** Default coefficient used to heuristically determine freshness
* lifetime from the Last-Modified time of a cache entry.
*/
public final static float DEFAULT_HEURISTIC_COEFFICIENT = 0.1f;
/** Default lifetime in seconds to be assumed when we cannot calculate
* freshness heuristically.
*/
public final static long DEFAULT_HEURISTIC_LIFETIME = 0;
/** Default number of worker threads to allow for background revalidations
* resulting from the stale-while-revalidate directive.
*/
public static final int DEFAULT_ASYNCHRONOUS_WORKERS_MAX = 1;
/** Default minimum number of worker threads to allow for background
* revalidations resulting from the stale-while-revalidate directive.
*/
public static final int DEFAULT_ASYNCHRONOUS_WORKERS_CORE = 1;
/** Default maximum idle lifetime for a background revalidation thread
* before it gets reclaimed.
*/
public static final int DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS = 60;
/** Default maximum queue length for background revalidation requests.
*/
public static final int DEFAULT_REVALIDATION_QUEUE_SIZE = 100;
public static final CacheConfig DEFAULT = new Builder().build();
// TODO: make final
private long maxObjectSize;
private int maxCacheEntries;
private int maxUpdateRetries;
private boolean allow303Caching;
private boolean weakETagOnPutDeleteAllowed;
private boolean heuristicCachingEnabled;
private float heuristicCoefficient;
private long heuristicDefaultLifetime;
private boolean isSharedCache;
private int asynchronousWorkersMax;
private int asynchronousWorkersCore;
private int asynchronousWorkerIdleLifetimeSecs;
private int revalidationQueueSize;
private boolean neverCacheHTTP10ResponsesWithQuery;
/**
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public CacheConfig() {
super();
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
this.weakETagOnPutDeleteAllowed = DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED;
this.heuristicCachingEnabled = DEFAULT_HEURISTIC_CACHING_ENABLED;
this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
this.isSharedCache = true;
this.asynchronousWorkersMax = DEFAULT_ASYNCHRONOUS_WORKERS_MAX;
this.asynchronousWorkersCore = DEFAULT_ASYNCHRONOUS_WORKERS_CORE;
this.asynchronousWorkerIdleLifetimeSecs = DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS;
this.revalidationQueueSize = DEFAULT_REVALIDATION_QUEUE_SIZE;
}
CacheConfig(
final long maxObjectSize,
final int maxCacheEntries,
final int maxUpdateRetries,
final boolean allow303Caching,
final boolean weakETagOnPutDeleteAllowed,
final boolean heuristicCachingEnabled,
final float heuristicCoefficient,
final long heuristicDefaultLifetime,
final boolean isSharedCache,
final int asynchronousWorkersMax,
final int asynchronousWorkersCore,
final int asynchronousWorkerIdleLifetimeSecs,
final int revalidationQueueSize,
final boolean neverCacheHTTP10ResponsesWithQuery) {
super();
this.maxObjectSize = maxObjectSize;
this.maxCacheEntries = maxCacheEntries;
this.maxUpdateRetries = maxUpdateRetries;
this.allow303Caching = allow303Caching;
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
this.heuristicCachingEnabled = heuristicCachingEnabled;
this.heuristicCoefficient = heuristicCoefficient;
this.heuristicDefaultLifetime = heuristicDefaultLifetime;
this.isSharedCache = isSharedCache;
this.asynchronousWorkersMax = asynchronousWorkersMax;
this.asynchronousWorkersCore = asynchronousWorkersCore;
this.asynchronousWorkerIdleLifetimeSecs = asynchronousWorkerIdleLifetimeSecs;
this.revalidationQueueSize = revalidationQueueSize;
}
/**
* Returns the current maximum response body size that will be cached.
* @return size in bytes
*
* @deprecated (4.2) use {@link #getMaxObjectSize()}
*/
@Deprecated
public int getMaxObjectSizeBytes() {
return maxObjectSize > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) maxObjectSize;
}
/**
* Specifies the maximum response body size that will be eligible for caching.
* @param maxObjectSizeBytes size in bytes
*
* @deprecated (4.2) use {@link Builder}.
*/
@Deprecated
public void setMaxObjectSizeBytes(final int maxObjectSizeBytes) {
if (maxObjectSizeBytes > Integer.MAX_VALUE) {
this.maxObjectSize = Integer.MAX_VALUE;
} else {
this.maxObjectSize = maxObjectSizeBytes;
}
}
/**
* Returns the current maximum response body size that will be cached.
* @return size in bytes
*
* @since 4.2
*/
public long getMaxObjectSize() {
return maxObjectSize;
}
/**
* Specifies the maximum response body size that will be eligible for caching.
* @param maxObjectSize size in bytes
*
* @since 4.2
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setMaxObjectSize(final long maxObjectSize) {
this.maxObjectSize = maxObjectSize;
}
/**
* Returns whether the cache will never cache HTTP 1.0 responses with a query string or not.
* @return {@code true} to not cache query string responses, {@code false} to cache if explicit cache headers are
* found
*/
public boolean isNeverCacheHTTP10ResponsesWithQuery() {
return neverCacheHTTP10ResponsesWithQuery;
}
/**
* Returns the maximum number of cache entries the cache will retain.
*/
public int getMaxCacheEntries() {
return maxCacheEntries;
}
/**
* Sets the maximum number of cache entries the cache will retain.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setMaxCacheEntries(final int maxCacheEntries) {
this.maxCacheEntries = maxCacheEntries;
}
/**
* Returns the number of times to retry a cache update on failure
*/
public int getMaxUpdateRetries(){
return maxUpdateRetries;
}
/**
* Sets the number of times to retry a cache update on failure
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setMaxUpdateRetries(final int maxUpdateRetries){
this.maxUpdateRetries = maxUpdateRetries;
}
/**
* Returns whether 303 caching is enabled.
* @return {@code true} if it is enabled.
*/
public boolean is303CachingEnabled() {
return allow303Caching;
}
/**
* Returns whether weak etags is allowed with PUT/DELETE methods.
* @return {@code true} if it is allowed.
*/
public boolean isWeakETagOnPutDeleteAllowed() {
return weakETagOnPutDeleteAllowed;
}
/**
* Returns whether heuristic caching is enabled.
* @return {@code true} if it is enabled.
*/
public boolean isHeuristicCachingEnabled() {
return heuristicCachingEnabled;
}
/**
* Enables or disables heuristic caching.
* @param heuristicCachingEnabled should be {@code true} to
* permit heuristic caching, {@code false} to disable it.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setHeuristicCachingEnabled(final boolean heuristicCachingEnabled) {
this.heuristicCachingEnabled = heuristicCachingEnabled;
}
/**
* Returns lifetime coefficient used in heuristic freshness caching.
*/
public float getHeuristicCoefficient() {
return heuristicCoefficient;
}
/**
* Sets coefficient to be used in heuristic freshness caching. This is
* interpreted as the fraction of the time between the {@code Last-Modified}
* and {@code Date} headers of a cached response during which the cached
* response will be considered heuristically fresh.
* @param heuristicCoefficient should be between {@code 0.0} and
* {@code 1.0}.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setHeuristicCoefficient(final float heuristicCoefficient) {
this.heuristicCoefficient = heuristicCoefficient;
}
/**
* Get the default lifetime to be used if heuristic freshness calculation is
* not possible.
*/
public long getHeuristicDefaultLifetime() {
return heuristicDefaultLifetime;
}
/**
* Sets default lifetime in seconds to be used if heuristic freshness
* calculation is not possible. Explicit cache control directives on
* either the request or origin response will override this, as will
* the heuristic {@code Last-Modified} freshness calculation if it is
* available.
* @param heuristicDefaultLifetimeSecs is the number of seconds to
* consider a cache-eligible response fresh in the absence of other
* information. Set this to {@code 0} to disable this style of
* heuristic caching.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setHeuristicDefaultLifetime(final long heuristicDefaultLifetimeSecs) {
this.heuristicDefaultLifetime = heuristicDefaultLifetimeSecs;
}
/**
* Returns whether the cache will behave as a shared cache or not.
* @return {@code true} for a shared cache, {@code false} for a non-
* shared (private) cache
*/
public boolean isSharedCache() {
return isSharedCache;
}
/**
* Sets whether the cache should behave as a shared cache or not.
* @param isSharedCache true to behave as a shared cache, false to
* behave as a non-shared (private) cache. To have the cache
* behave like a browser cache, you want to set this to {@code false}.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setSharedCache(final boolean isSharedCache) {
this.isSharedCache = isSharedCache;
}
/**
* Returns the maximum number of threads to allow for background
* revalidations due to the {@code stale-while-revalidate} directive. A
* value of 0 means background revalidations are disabled.
*/
public int getAsynchronousWorkersMax() {
return asynchronousWorkersMax;
}
/**
* Sets the maximum number of threads to allow for background
* revalidations due to the {@code stale-while-revalidate} directive.
* @param max number of threads; a value of 0 disables background
* revalidations.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setAsynchronousWorkersMax(final int max) {
this.asynchronousWorkersMax = max;
}
/**
* Returns the minimum number of threads to keep alive for background
* revalidations due to the {@code stale-while-revalidate} directive.
*/
public int getAsynchronousWorkersCore() {
return asynchronousWorkersCore;
}
/**
* Sets the minimum number of threads to keep alive for background
* revalidations due to the {@code stale-while-revalidate} directive.
* @param min should be greater than zero and less than or equal
* to <code>getAsynchronousWorkersMax()</code>
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setAsynchronousWorkersCore(final int min) {
this.asynchronousWorkersCore = min;
}
/**
* Returns the current maximum idle lifetime in seconds for a
* background revalidation worker thread. If a worker thread is idle
* for this long, and there are more than the core number of worker
* threads alive, the worker will be reclaimed.
*/
public int getAsynchronousWorkerIdleLifetimeSecs() {
return asynchronousWorkerIdleLifetimeSecs;
}
/**
* Sets the current maximum idle lifetime in seconds for a
* background revalidation worker thread. If a worker thread is idle
* for this long, and there are more than the core number of worker
* threads alive, the worker will be reclaimed.
* @param secs idle lifetime in seconds
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setAsynchronousWorkerIdleLifetimeSecs(final int secs) {
this.asynchronousWorkerIdleLifetimeSecs = secs;
}
/**
* Returns the current maximum queue size for background revalidations.
*/
public int getRevalidationQueueSize() {
return revalidationQueueSize;
}
/**
* Sets the current maximum queue size for background revalidations.
*
* @deprecated (4.3) use {@link Builder}.
*/
@Deprecated
public void setRevalidationQueueSize(final int size) {
this.revalidationQueueSize = size;
}
@Override
protected CacheConfig clone() throws CloneNotSupportedException {
return (CacheConfig) super.clone();
}
public static Builder custom() {
return new Builder();
}
public static Builder copy(final CacheConfig config) {
Args.notNull(config, "Cache config");
return new Builder()
.setMaxObjectSize(config.getMaxObjectSize())
.setMaxCacheEntries(config.getMaxCacheEntries())
.setMaxUpdateRetries(config.getMaxUpdateRetries())
.setHeuristicCachingEnabled(config.isHeuristicCachingEnabled())
.setHeuristicCoefficient(config.getHeuristicCoefficient())
.setHeuristicDefaultLifetime(config.getHeuristicDefaultLifetime())
.setSharedCache(config.isSharedCache())
.setAsynchronousWorkersMax(config.getAsynchronousWorkersMax())
.setAsynchronousWorkersCore(config.getAsynchronousWorkersCore())
.setAsynchronousWorkerIdleLifetimeSecs(config.getAsynchronousWorkerIdleLifetimeSecs())
.setRevalidationQueueSize(config.getRevalidationQueueSize())
.setNeverCacheHTTP10ResponsesWithQueryString(config.isNeverCacheHTTP10ResponsesWithQuery());
}
public static class Builder {
private long maxObjectSize;
private int maxCacheEntries;
private int maxUpdateRetries;
private boolean allow303Caching;
private boolean weakETagOnPutDeleteAllowed;
private boolean heuristicCachingEnabled;
private float heuristicCoefficient;
private long heuristicDefaultLifetime;
private boolean isSharedCache;
private int asynchronousWorkersMax;
private int asynchronousWorkersCore;
private int asynchronousWorkerIdleLifetimeSecs;
private int revalidationQueueSize;
private boolean neverCacheHTTP10ResponsesWithQuery;
Builder() {
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
this.weakETagOnPutDeleteAllowed = DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED;
this.heuristicCachingEnabled = false;
this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
this.isSharedCache = true;
this.asynchronousWorkersMax = DEFAULT_ASYNCHRONOUS_WORKERS_MAX;
this.asynchronousWorkersCore = DEFAULT_ASYNCHRONOUS_WORKERS_CORE;
this.asynchronousWorkerIdleLifetimeSecs = DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS;
this.revalidationQueueSize = DEFAULT_REVALIDATION_QUEUE_SIZE;
}
/**
* Specifies the maximum response body size that will be eligible for caching.
* @param maxObjectSize size in bytes
*/
public Builder setMaxObjectSize(final long maxObjectSize) {
this.maxObjectSize = maxObjectSize;
return this;
}
/**
* Sets the maximum number of cache entries the cache will retain.
*/
public Builder setMaxCacheEntries(final int maxCacheEntries) {
this.maxCacheEntries = maxCacheEntries;
return this;
}
/**
* Sets the number of times to retry a cache update on failure
*/
public Builder setMaxUpdateRetries(final int maxUpdateRetries) {
this.maxUpdateRetries = maxUpdateRetries;
return this;
}
/**
* Enables or disables 303 caching.
* @param allow303Caching should be {@code true} to
* permit 303 caching, {@code false} to disable it.
*/
public Builder setAllow303Caching(final boolean allow303Caching) {
this.allow303Caching = allow303Caching;
return this;
}
/**
* Allows or disallows weak etags to be used with PUT/DELETE If-Match requests.
* @param weakETagOnPutDeleteAllowed should be {@code true} to
* permit weak etags, {@code false} to reject them.
*/
public Builder setWeakETagOnPutDeleteAllowed(final boolean weakETagOnPutDeleteAllowed) {
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
return this;
}
/**
* Enables or disables heuristic caching.
* @param heuristicCachingEnabled should be {@code true} to
* permit heuristic caching, {@code false} to enable it.
*/
public Builder setHeuristicCachingEnabled(final boolean heuristicCachingEnabled) {
this.heuristicCachingEnabled = heuristicCachingEnabled;
return this;
}
/**
* Sets coefficient to be used in heuristic freshness caching. This is
* interpreted as the fraction of the time between the {@code Last-Modified}
* and {@code Date} headers of a cached response during which the cached
* response will be considered heuristically fresh.
* @param heuristicCoefficient should be between {@code 0.0} and
* {@code 1.0}.
*/
public Builder setHeuristicCoefficient(final float heuristicCoefficient) {
this.heuristicCoefficient = heuristicCoefficient;
return this;
}
/**
* Sets default lifetime in seconds to be used if heuristic freshness
* calculation is not possible. Explicit cache control directives on
* either the request or origin response will override this, as will
* the heuristic {@code Last-Modified} freshness calculation if it is
* available.
* @param heuristicDefaultLifetime is the number of seconds to
* consider a cache-eligible response fresh in the absence of other
* information. Set this to {@code 0} to disable this style of
* heuristic caching.
*/
public Builder setHeuristicDefaultLifetime(final long heuristicDefaultLifetime) {
this.heuristicDefaultLifetime = heuristicDefaultLifetime;
return this;
}
/**
* Sets whether the cache should behave as a shared cache or not.
* @param isSharedCache true to behave as a shared cache, false to
* behave as a non-shared (private) cache. To have the cache
* behave like a browser cache, you want to set this to {@code false}.
*/
public Builder setSharedCache(final boolean isSharedCache) {
this.isSharedCache = isSharedCache;
return this;
}
/**
* Sets the maximum number of threads to allow for background
* revalidations due to the {@code stale-while-revalidate} directive.
* @param asynchronousWorkersMax number of threads; a value of 0 disables background
* revalidations.
*/
public Builder setAsynchronousWorkersMax(final int asynchronousWorkersMax) {
this.asynchronousWorkersMax = asynchronousWorkersMax;
return this;
}
/**
* Sets the minimum number of threads to keep alive for background
* revalidations due to the {@code stale-while-revalidate} directive.
* @param asynchronousWorkersCore should be greater than zero and less than or equal
* to <code>getAsynchronousWorkersMax()</code>
*/
public Builder setAsynchronousWorkersCore(final int asynchronousWorkersCore) {
this.asynchronousWorkersCore = asynchronousWorkersCore;
return this;
}
/**
* Sets the current maximum idle lifetime in seconds for a
* background revalidation worker thread. If a worker thread is idle
* for this long, and there are more than the core number of worker
* threads alive, the worker will be reclaimed.
* @param asynchronousWorkerIdleLifetimeSecs idle lifetime in seconds
*/
public Builder setAsynchronousWorkerIdleLifetimeSecs(final int asynchronousWorkerIdleLifetimeSecs) {
this.asynchronousWorkerIdleLifetimeSecs = asynchronousWorkerIdleLifetimeSecs;
return this;
}
/**
* Sets the current maximum queue size for background revalidations.
*/
public Builder setRevalidationQueueSize(final int revalidationQueueSize) {
this.revalidationQueueSize = revalidationQueueSize;
return this;
}
/**
* Sets whether the cache should never cache HTTP 1.0 responses with a query string or not.
* @param neverCacheHTTP10ResponsesWithQuery true to never cache responses with a query
* string, false to cache if explicit cache headers are found. Set this to {@code true}
* to better emulate IE, which also never caches responses, regardless of what caching
* headers may be present.
*/
public Builder setNeverCacheHTTP10ResponsesWithQueryString(
final boolean neverCacheHTTP10ResponsesWithQuery) {
this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
return this;
}
public CacheConfig build() {
return new CacheConfig(
maxObjectSize,
maxCacheEntries,
maxUpdateRetries,
allow303Caching,
weakETagOnPutDeleteAllowed,
heuristicCachingEnabled,
heuristicCoefficient,
heuristicDefaultLifetime,
isSharedCache,
asynchronousWorkersMax,
asynchronousWorkersCore,
asynchronousWorkerIdleLifetimeSecs,
revalidationQueueSize,
neverCacheHTTP10ResponsesWithQuery);
}
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("[maxObjectSize=").append(this.maxObjectSize)
.append(", maxCacheEntries=").append(this.maxCacheEntries)
.append(", maxUpdateRetries=").append(this.maxUpdateRetries)
.append(", 303CachingEnabled=").append(this.allow303Caching)
.append(", weakETagOnPutDeleteAllowed=").append(this.weakETagOnPutDeleteAllowed)
.append(", heuristicCachingEnabled=").append(this.heuristicCachingEnabled)
.append(", heuristicCoefficient=").append(this.heuristicCoefficient)
.append(", heuristicDefaultLifetime=").append(this.heuristicDefaultLifetime)
.append(", isSharedCache=").append(this.isSharedCache)
.append(", asynchronousWorkersMax=").append(this.asynchronousWorkersMax)
.append(", asynchronousWorkersCore=").append(this.asynchronousWorkersCore)
.append(", asynchronousWorkerIdleLifetimeSecs=").append(this.asynchronousWorkerIdleLifetimeSecs)
.append(", revalidationQueueSize=").append(this.revalidationQueueSize)
.append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
.append("]");
return builder.toString();
}
}

View file

@ -0,0 +1,99 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.protocol.HTTP;
import ch.boye.httpclientandroidlib.util.Args;
@Immutable
class CacheEntity implements HttpEntity, Serializable {
private static final long serialVersionUID = -3467082284120936233L;
private final HttpCacheEntry cacheEntry;
public CacheEntity(final HttpCacheEntry cacheEntry) {
super();
this.cacheEntry = cacheEntry;
}
public Header getContentType() {
return this.cacheEntry.getFirstHeader(HTTP.CONTENT_TYPE);
}
public Header getContentEncoding() {
return this.cacheEntry.getFirstHeader(HTTP.CONTENT_ENCODING);
}
public boolean isChunked() {
return false;
}
public boolean isRepeatable() {
return true;
}
public long getContentLength() {
return this.cacheEntry.getResource().length();
}
public InputStream getContent() throws IOException {
return this.cacheEntry.getResource().getInputStream();
}
public void writeTo(final OutputStream outstream) throws IOException {
Args.notNull(outstream, "Output stream");
final InputStream instream = this.cacheEntry.getResource().getInputStream();
try {
IOUtils.copy(instream, outstream);
} finally {
instream.close();
}
}
public boolean isStreaming() {
return false;
}
public void consumeContent() throws IOException {
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

View file

@ -0,0 +1,173 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.protocol.HTTP;
import ch.boye.httpclientandroidlib.util.Args;
/**
* Update a {@link HttpCacheEntry} with new or updated information based on the latest
* 304 status response from the Server. Use the {@link HttpResponse} to perform
* the update.
*
* @since 4.1
*/
@Immutable
class CacheEntryUpdater {
private final ResourceFactory resourceFactory;
CacheEntryUpdater() {
this(new HeapResourceFactory());
}
CacheEntryUpdater(final ResourceFactory resourceFactory) {
super();
this.resourceFactory = resourceFactory;
}
/**
* Update the entry with the new information from the response. Should only be used for
* 304 responses.
*
* @param requestId
* @param entry The cache Entry to be updated
* @param requestDate When the request was performed
* @param responseDate When the response was gotten
* @param response The HttpResponse from the backend server call
* @return HttpCacheEntry an updated version of the cache entry
* @throws java.io.IOException if something bad happens while trying to read the body from the original entry
*/
public HttpCacheEntry updateCacheEntry(
final String requestId,
final HttpCacheEntry entry,
final Date requestDate,
final Date responseDate,
final HttpResponse response) throws IOException {
Args.check(response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED,
"Response must have 304 status code");
final Header[] mergedHeaders = mergeHeaders(entry, response);
Resource resource = null;
if (entry.getResource() != null) {
resource = resourceFactory.copy(requestId, entry.getResource());
}
return new HttpCacheEntry(
requestDate,
responseDate,
entry.getStatusLine(),
mergedHeaders,
resource);
}
protected Header[] mergeHeaders(final HttpCacheEntry entry, final HttpResponse response) {
if (entryAndResponseHaveDateHeader(entry, response)
&& entryDateHeaderNewerThenResponse(entry, response)) {
// Don't merge headers, keep the entry's headers as they are newer.
return entry.getAllHeaders();
}
final List<Header> cacheEntryHeaderList = new ArrayList<Header>(Arrays.asList(entry
.getAllHeaders()));
removeCacheHeadersThatMatchResponse(cacheEntryHeaderList, response);
removeCacheEntry1xxWarnings(cacheEntryHeaderList, entry);
cacheEntryHeaderList.addAll(Arrays.asList(response.getAllHeaders()));
return cacheEntryHeaderList.toArray(new Header[cacheEntryHeaderList.size()]);
}
private void removeCacheHeadersThatMatchResponse(final List<Header> cacheEntryHeaderList,
final HttpResponse response) {
for (final Header responseHeader : response.getAllHeaders()) {
final ListIterator<Header> cacheEntryHeaderListIter = cacheEntryHeaderList.listIterator();
while (cacheEntryHeaderListIter.hasNext()) {
final String cacheEntryHeaderName = cacheEntryHeaderListIter.next().getName();
if (cacheEntryHeaderName.equals(responseHeader.getName())) {
cacheEntryHeaderListIter.remove();
}
}
}
}
private void removeCacheEntry1xxWarnings(final List<Header> cacheEntryHeaderList, final HttpCacheEntry entry) {
final ListIterator<Header> cacheEntryHeaderListIter = cacheEntryHeaderList.listIterator();
while (cacheEntryHeaderListIter.hasNext()) {
final String cacheEntryHeaderName = cacheEntryHeaderListIter.next().getName();
if (HeaderConstants.WARNING.equals(cacheEntryHeaderName)) {
for (final Header cacheEntryWarning : entry.getHeaders(HeaderConstants.WARNING)) {
if (cacheEntryWarning.getValue().startsWith("1")) {
cacheEntryHeaderListIter.remove();
}
}
}
}
}
private boolean entryDateHeaderNewerThenResponse(final HttpCacheEntry entry, final HttpResponse response) {
final Date entryDate = DateUtils.parseDate(entry.getFirstHeader(HTTP.DATE_HEADER)
.getValue());
final Date responseDate = DateUtils.parseDate(response.getFirstHeader(HTTP.DATE_HEADER)
.getValue());
if (entryDate == null || responseDate == null) {
return false;
}
if (!entryDate.after(responseDate)) {
return false;
}
return true;
}
private boolean entryAndResponseHaveDateHeader(final HttpCacheEntry entry, final HttpResponse response) {
if (entry.getFirstHeader(HTTP.DATE_HEADER) != null
&& response.getFirstHeader(HTTP.DATE_HEADER) != null) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,288 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* Given a particular HttpRequest, flush any cache entries that this request
* would invalidate.
*
* @since 4.1
*/
@Immutable
class CacheInvalidator implements HttpCacheInvalidator {
private final HttpCacheStorage storage;
private final CacheKeyGenerator cacheKeyGenerator;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
/**
* Create a new {@link CacheInvalidator} for a given {@link HttpCache} and
* {@link CacheKeyGenerator}.
*
* @param uriExtractor Provides identifiers for the keys to store cache entries
* @param storage the cache to store items away in
*/
public CacheInvalidator(
final CacheKeyGenerator uriExtractor,
final HttpCacheStorage storage) {
this.cacheKeyGenerator = uriExtractor;
this.storage = storage;
}
/**
* Remove cache entries from the cache that are no longer fresh or
* have been invalidated in some way.
*
* @param host The backend host we are talking to
* @param req The HttpRequest to that host
*/
public void flushInvalidatedCacheEntries(final HttpHost host, final HttpRequest req) {
if (requestShouldNotBeCached(req)) {
log.debug("Request should not be cached");
final String theUri = cacheKeyGenerator.getURI(host, req);
final HttpCacheEntry parent = getEntry(theUri);
log.debug("parent entry: " + parent);
if (parent != null) {
for (final String variantURI : parent.getVariantMap().values()) {
flushEntry(variantURI);
}
flushEntry(theUri);
}
final URL reqURL = getAbsoluteURL(theUri);
if (reqURL == null) {
log.error("Couldn't transform request into valid URL");
return;
}
final Header clHdr = req.getFirstHeader("Content-Location");
if (clHdr != null) {
final String contentLocation = clHdr.getValue();
if (!flushAbsoluteUriFromSameHost(reqURL, contentLocation)) {
flushRelativeUriFromSameHost(reqURL, contentLocation);
}
}
final Header lHdr = req.getFirstHeader("Location");
if (lHdr != null) {
flushAbsoluteUriFromSameHost(reqURL, lHdr.getValue());
}
}
}
private void flushEntry(final String uri) {
try {
storage.removeEntry(uri);
} catch (final IOException ioe) {
log.warn("unable to flush cache entry", ioe);
}
}
private HttpCacheEntry getEntry(final String theUri) {
try {
return storage.getEntry(theUri);
} catch (final IOException ioe) {
log.warn("could not retrieve entry from storage", ioe);
}
return null;
}
protected void flushUriIfSameHost(final URL requestURL, final URL targetURL) {
final URL canonicalTarget = getAbsoluteURL(cacheKeyGenerator.canonicalizeUri(targetURL.toString()));
if (canonicalTarget == null) {
return;
}
if (canonicalTarget.getAuthority().equalsIgnoreCase(requestURL.getAuthority())) {
flushEntry(canonicalTarget.toString());
}
}
protected void flushRelativeUriFromSameHost(final URL reqURL, final String relUri) {
final URL relURL = getRelativeURL(reqURL, relUri);
if (relURL == null) {
return;
}
flushUriIfSameHost(reqURL, relURL);
}
protected boolean flushAbsoluteUriFromSameHost(final URL reqURL, final String uri) {
final URL absURL = getAbsoluteURL(uri);
if (absURL == null) {
return false;
}
flushUriIfSameHost(reqURL,absURL);
return true;
}
private URL getAbsoluteURL(final String uri) {
URL absURL = null;
try {
absURL = new URL(uri);
} catch (final MalformedURLException mue) {
// nop
}
return absURL;
}
private URL getRelativeURL(final URL reqURL, final String relUri) {
URL relURL = null;
try {
relURL = new URL(reqURL,relUri);
} catch (final MalformedURLException e) {
// nop
}
return relURL;
}
protected boolean requestShouldNotBeCached(final HttpRequest req) {
final String method = req.getRequestLine().getMethod();
return notGetOrHeadRequest(method);
}
private boolean notGetOrHeadRequest(final String method) {
return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD
.equals(method));
}
/** Flushes entries that were invalidated by the given response
* received for the given host/request pair.
*/
public void flushInvalidatedCacheEntries(final HttpHost host,
final HttpRequest request, final HttpResponse response) {
final int status = response.getStatusLine().getStatusCode();
if (status < 200 || status > 299) {
return;
}
final URL reqURL = getAbsoluteURL(cacheKeyGenerator.getURI(host, request));
if (reqURL == null) {
return;
}
final URL contentLocation = getContentLocationURL(reqURL, response);
if (contentLocation != null) {
flushLocationCacheEntry(reqURL, response, contentLocation);
}
final URL location = getLocationURL(reqURL, response);
if (location != null) {
flushLocationCacheEntry(reqURL, response, location);
}
}
private void flushLocationCacheEntry(final URL reqURL,
final HttpResponse response, final URL location) {
final String cacheKey = cacheKeyGenerator.canonicalizeUri(location.toString());
final HttpCacheEntry entry = getEntry(cacheKey);
if (entry == null) {
return;
}
// do not invalidate if response is strictly older than entry
// or if the etags match
if (responseDateOlderThanEntryDate(response, entry)) {
return;
}
if (!responseAndEntryEtagsDiffer(response, entry)) {
return;
}
flushUriIfSameHost(reqURL, location);
}
private URL getContentLocationURL(final URL reqURL, final HttpResponse response) {
final Header clHeader = response.getFirstHeader("Content-Location");
if (clHeader == null) {
return null;
}
final String contentLocation = clHeader.getValue();
final URL canonURL = getAbsoluteURL(contentLocation);
if (canonURL != null) {
return canonURL;
}
return getRelativeURL(reqURL, contentLocation);
}
private URL getLocationURL(final URL reqURL, final HttpResponse response) {
final Header clHeader = response.getFirstHeader("Location");
if (clHeader == null) {
return null;
}
final String location = clHeader.getValue();
final URL canonURL = getAbsoluteURL(location);
if (canonURL != null) {
return canonURL;
}
return getRelativeURL(reqURL, location);
}
private boolean responseAndEntryEtagsDiffer(final HttpResponse response,
final HttpCacheEntry entry) {
final Header entryEtag = entry.getFirstHeader(HeaderConstants.ETAG);
final Header responseEtag = response.getFirstHeader(HeaderConstants.ETAG);
if (entryEtag == null || responseEtag == null) {
return false;
}
return (!entryEtag.getValue().equals(responseEtag.getValue()));
}
private boolean responseDateOlderThanEntryDate(final HttpResponse response,
final HttpCacheEntry entry) {
final Header entryDateHeader = entry.getFirstHeader(HTTP.DATE_HEADER);
final Header responseDateHeader = response.getFirstHeader(HTTP.DATE_HEADER);
if (entryDateHeader == null || responseDateHeader == null) {
/* be conservative; should probably flush */
return false;
}
final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue());
if (entryDate == null || responseDate == null) {
return false;
}
return responseDate.before(entryDate);
}
}

View file

@ -0,0 +1,178 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import ch.boye.httpclientandroidlib.Consts;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.utils.URIUtils;
/**
* @since 4.1
*/
@Immutable
class CacheKeyGenerator {
private static final URI BASE_URI = URI.create("http://example.com/");
/**
* For a given {@link HttpHost} and {@link HttpRequest} get a URI from the
* pair that I can use as an identifier KEY into my HttpCache
*
* @param host The host for this request
* @param req the {@link HttpRequest}
* @return String the extracted URI
*/
public String getURI(final HttpHost host, final HttpRequest req) {
if (isRelativeRequest(req)) {
return canonicalizeUri(String.format("%s%s", host.toString(), req.getRequestLine().getUri()));
}
return canonicalizeUri(req.getRequestLine().getUri());
}
public String canonicalizeUri(final String uri) {
try {
final URI normalized = URIUtils.resolve(BASE_URI, uri);
final URL u = new URL(normalized.toASCIIString());
final String protocol = u.getProtocol();
final String hostname = u.getHost();
final int port = canonicalizePort(u.getPort(), protocol);
final String path = u.getPath();
final String query = u.getQuery();
final String file = (query != null) ? (path + "?" + query) : path;
final URL out = new URL(protocol, hostname, port, file);
return out.toString();
} catch (final IllegalArgumentException e) {
return uri;
} catch (final MalformedURLException e) {
return uri;
}
}
private int canonicalizePort(final int port, final String protocol) {
if (port == -1 && "http".equalsIgnoreCase(protocol)) {
return 80;
} else if (port == -1 && "https".equalsIgnoreCase(protocol)) {
return 443;
}
return port;
}
private boolean isRelativeRequest(final HttpRequest req) {
final String requestUri = req.getRequestLine().getUri();
return ("*".equals(requestUri) || requestUri.startsWith("/"));
}
protected String getFullHeaderValue(final Header[] headers) {
if (headers == null) {
return "";
}
final StringBuilder buf = new StringBuilder("");
boolean first = true;
for (final Header hdr : headers) {
if (!first) {
buf.append(", ");
}
buf.append(hdr.getValue().trim());
first = false;
}
return buf.toString();
}
/**
* For a given {@link HttpHost} and {@link HttpRequest} if the request has a
* VARY header - I need to get an additional URI from the pair of host and
* request so that I can also store the variant into my HttpCache.
*
* @param host The host for this request
* @param req the {@link HttpRequest}
* @param entry the parent entry used to track the variants
* @return String the extracted variant URI
*/
public String getVariantURI(final HttpHost host, final HttpRequest req, final HttpCacheEntry entry) {
if (!entry.hasVariants()) {
return getURI(host, req);
}
return getVariantKey(req, entry) + getURI(host, req);
}
/**
* Compute a "variant key" from the headers of a given request that are
* covered by the Vary header of a given cache entry. Any request whose
* varying headers match those of this request should have the same
* variant key.
* @param req originating request
* @param entry cache entry in question that has variants
* @return a <code>String</code> variant key
*/
public String getVariantKey(final HttpRequest req, final HttpCacheEntry entry) {
final List<String> variantHeaderNames = new ArrayList<String>();
for (final Header varyHdr : entry.getHeaders(HeaderConstants.VARY)) {
for (final HeaderElement elt : varyHdr.getElements()) {
variantHeaderNames.add(elt.getName());
}
}
Collections.sort(variantHeaderNames);
StringBuilder buf;
try {
buf = new StringBuilder("{");
boolean first = true;
for (final String headerName : variantHeaderNames) {
if (!first) {
buf.append("&");
}
buf.append(URLEncoder.encode(headerName, Consts.UTF_8.name()));
buf.append("=");
buf.append(URLEncoder.encode(getFullHeaderValue(req.getHeaders(headerName)),
Consts.UTF_8.name()));
first = false;
}
buf.append("}");
} catch (final UnsupportedEncodingException uee) {
throw new RuntimeException("couldn't encode to UTF-8", uee);
}
return buf.toString();
}
}

View file

@ -0,0 +1,50 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.LinkedHashMap;
import java.util.Map;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
final class CacheMap extends LinkedHashMap<String, HttpCacheEntry> {
private static final long serialVersionUID = -7750025207539768511L;
private final int maxEntries;
CacheMap(final int maxEntries) {
super(20, 0.75f, true);
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(final Map.Entry<String, HttpCacheEntry> eldest) {
return size() > this.maxEntries;
}
}

View file

@ -0,0 +1,320 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Date;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* @since 4.1
*/
@Immutable
class CacheValidityPolicy {
public static final long MAX_AGE = 2147483648L;
CacheValidityPolicy() {
super();
}
public long getCurrentAgeSecs(final HttpCacheEntry entry, final Date now) {
return getCorrectedInitialAgeSecs(entry) + getResidentTimeSecs(entry, now);
}
public long getFreshnessLifetimeSecs(final HttpCacheEntry entry) {
final long maxage = getMaxAge(entry);
if (maxage > -1) {
return maxage;
}
final Date dateValue = entry.getDate();
if (dateValue == null) {
return 0L;
}
final Date expiry = getExpirationDate(entry);
if (expiry == null) {
return 0;
}
final long diff = expiry.getTime() - dateValue.getTime();
return (diff / 1000);
}
public boolean isResponseFresh(final HttpCacheEntry entry, final Date now) {
return (getCurrentAgeSecs(entry, now) < getFreshnessLifetimeSecs(entry));
}
/**
* Decides if this response is fresh enough based Last-Modified and Date, if available.
* This entry is meant to be used when isResponseFresh returns false. The algorithm is as follows:
*
* if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified),
* else freshness lifetime is defaultLifetime
*
* @param entry the cache entry
* @param now what time is it currently (When is right NOW)
* @param coefficient Part of the heuristic for cache entry freshness
* @param defaultLifetime How long can I assume a cache entry is default TTL
* @return {@code true} if the response is fresh
*/
public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry,
final Date now, final float coefficient, final long defaultLifetime) {
return (getCurrentAgeSecs(entry, now) < getHeuristicFreshnessLifetimeSecs(entry, coefficient, defaultLifetime));
}
public long getHeuristicFreshnessLifetimeSecs(final HttpCacheEntry entry,
final float coefficient, final long defaultLifetime) {
final Date dateValue = entry.getDate();
final Date lastModifiedValue = getLastModifiedValue(entry);
if (dateValue != null && lastModifiedValue != null) {
final long diff = dateValue.getTime() - lastModifiedValue.getTime();
if (diff < 0) {
return 0;
}
return (long)(coefficient * (diff / 1000));
}
return defaultLifetime;
}
public boolean isRevalidatable(final HttpCacheEntry entry) {
return entry.getFirstHeader(HeaderConstants.ETAG) != null
|| entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null;
}
public boolean mustRevalidate(final HttpCacheEntry entry) {
return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE);
}
public boolean proxyRevalidate(final HttpCacheEntry entry) {
return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE);
}
public boolean mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, final Date now) {
for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.STALE_WHILE_REVALIDATE.equalsIgnoreCase(elt.getName())) {
try {
final int allowedStalenessLifetime = Integer.parseInt(elt.getValue());
if (getStalenessSecs(entry, now) <= allowedStalenessLifetime) {
return true;
}
} catch (final NumberFormatException nfe) {
// skip malformed directive
}
}
}
}
return false;
}
public boolean mayReturnStaleIfError(final HttpRequest request,
final HttpCacheEntry entry, final Date now) {
final long stalenessSecs = getStalenessSecs(entry, now);
return mayReturnStaleIfError(request.getHeaders(HeaderConstants.CACHE_CONTROL),
stalenessSecs)
|| mayReturnStaleIfError(entry.getHeaders(HeaderConstants.CACHE_CONTROL),
stalenessSecs);
}
private boolean mayReturnStaleIfError(final Header[] headers, final long stalenessSecs) {
boolean result = false;
for(final Header h : headers) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.STALE_IF_ERROR.equals(elt.getName())) {
try {
final int staleIfErrorSecs = Integer.parseInt(elt.getValue());
if (stalenessSecs <= staleIfErrorSecs) {
result = true;
break;
}
} catch (final NumberFormatException nfe) {
// skip malformed directive
}
}
}
}
return result;
}
/**
* @deprecated (4.3) use {@link HttpCacheEntry#getDate()}.
* @param entry
* @return the Date of the entry
*/
@Deprecated
protected Date getDateValue(final HttpCacheEntry entry) {
return entry.getDate();
}
protected Date getLastModifiedValue(final HttpCacheEntry entry) {
final Header dateHdr = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
if (dateHdr == null) {
return null;
}
return DateUtils.parseDate(dateHdr.getValue());
}
protected long getContentLengthValue(final HttpCacheEntry entry) {
final Header cl = entry.getFirstHeader(HTTP.CONTENT_LEN);
if (cl == null) {
return -1;
}
try {
return Long.parseLong(cl.getValue());
} catch (final NumberFormatException ex) {
return -1;
}
}
protected boolean hasContentLengthHeader(final HttpCacheEntry entry) {
return null != entry.getFirstHeader(HTTP.CONTENT_LEN);
}
/**
* This matters for deciding whether the cache entry is valid to serve as a
* response. If these values do not match, we might have a partial response
*
* @param entry The cache entry we are currently working with
* @return boolean indicating whether actual length matches Content-Length
*/
protected boolean contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry) {
return !hasContentLengthHeader(entry) || getContentLengthValue(entry) == entry.getResource().length();
}
protected long getApparentAgeSecs(final HttpCacheEntry entry) {
final Date dateValue = entry.getDate();
if (dateValue == null) {
return MAX_AGE;
}
final long diff = entry.getResponseDate().getTime() - dateValue.getTime();
if (diff < 0L) {
return 0;
}
return (diff / 1000);
}
protected long getAgeValue(final HttpCacheEntry entry) {
long ageValue = 0;
for (final Header hdr : entry.getHeaders(HeaderConstants.AGE)) {
long hdrAge;
try {
hdrAge = Long.parseLong(hdr.getValue());
if (hdrAge < 0) {
hdrAge = MAX_AGE;
}
} catch (final NumberFormatException nfe) {
hdrAge = MAX_AGE;
}
ageValue = (hdrAge > ageValue) ? hdrAge : ageValue;
}
return ageValue;
}
protected long getCorrectedReceivedAgeSecs(final HttpCacheEntry entry) {
final long apparentAge = getApparentAgeSecs(entry);
final long ageValue = getAgeValue(entry);
return (apparentAge > ageValue) ? apparentAge : ageValue;
}
protected long getResponseDelaySecs(final HttpCacheEntry entry) {
final long diff = entry.getResponseDate().getTime() - entry.getRequestDate().getTime();
return (diff / 1000L);
}
protected long getCorrectedInitialAgeSecs(final HttpCacheEntry entry) {
return getCorrectedReceivedAgeSecs(entry) + getResponseDelaySecs(entry);
}
protected long getResidentTimeSecs(final HttpCacheEntry entry, final Date now) {
final long diff = now.getTime() - entry.getResponseDate().getTime();
return (diff / 1000L);
}
protected long getMaxAge(final HttpCacheEntry entry) {
long maxage = -1;
for (final Header hdr : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for (final HeaderElement elt : hdr.getElements()) {
if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())
|| "s-maxage".equals(elt.getName())) {
try {
final long currMaxAge = Long.parseLong(elt.getValue());
if (maxage == -1 || currMaxAge < maxage) {
maxage = currMaxAge;
}
} catch (final NumberFormatException nfe) {
// be conservative if can't parse
maxage = 0;
}
}
}
}
return maxage;
}
protected Date getExpirationDate(final HttpCacheEntry entry) {
final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
if (expiresHeader == null) {
return null;
}
return DateUtils.parseDate(expiresHeader.getValue());
}
public boolean hasCacheControlDirective(final HttpCacheEntry entry,
final String directive) {
for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (directive.equalsIgnoreCase(elt.getName())) {
return true;
}
}
}
return false;
}
public long getStalenessSecs(final HttpCacheEntry entry, final Date now) {
final long age = getCurrentAgeSecs(entry, now);
final long freshness = getFreshnessLifetimeSecs(entry);
if (age <= freshness) {
return 0L;
}
return (age - freshness);
}
}

View file

@ -0,0 +1,96 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
/**
* Determines if an HttpRequest is allowed to be served from the cache.
*
* @since 4.1
*/
@Immutable
class CacheableRequestPolicy {
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
/**
* Determines if an HttpRequest can be served from the cache.
*
* @param request
* an HttpRequest
* @return boolean Is it possible to serve this request from cache
*/
public boolean isServableFromCache(final HttpRequest request) {
final String method = request.getRequestLine().getMethod();
final ProtocolVersion pv = request.getRequestLine().getProtocolVersion();
if (HttpVersion.HTTP_1_1.compareToVersion(pv) != 0) {
log.trace("non-HTTP/1.1 request was not serveable from cache");
return false;
}
if (!method.equals(HeaderConstants.GET_METHOD)) {
log.trace("non-GET request was not serveable from cache");
return false;
}
if (request.getHeaders(HeaderConstants.PRAGMA).length > 0) {
log.trace("request with Pragma header was not serveable from cache");
return false;
}
final Header[] cacheControlHeaders = request.getHeaders(HeaderConstants.CACHE_CONTROL);
for (final Header cacheControl : cacheControlHeaders) {
for (final HeaderElement cacheControlElement : cacheControl.getElements()) {
if (HeaderConstants.CACHE_CONTROL_NO_STORE.equalsIgnoreCase(cacheControlElement
.getName())) {
log.trace("Request with no-store was not serveable from cache");
return false;
}
if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equalsIgnoreCase(cacheControlElement
.getName())) {
log.trace("Request with no-cache was not serveable from cache");
return false;
}
}
}
log.trace("Request was serveable from cache");
return true;
}
}

View file

@ -0,0 +1,166 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Date;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.message.BasicHeader;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* Rebuilds an {@link HttpResponse} from a {@link net.sf.ehcache.CacheEntry}
*
* @since 4.1
*/
@Immutable
class CachedHttpResponseGenerator {
private final CacheValidityPolicy validityStrategy;
CachedHttpResponseGenerator(final CacheValidityPolicy validityStrategy) {
super();
this.validityStrategy = validityStrategy;
}
CachedHttpResponseGenerator() {
this(new CacheValidityPolicy());
}
/**
* If I was able to use a {@link CacheEntity} to response to the {@link ch.boye.httpclientandroidlib.HttpRequest} then
* generate an {@link HttpResponse} based on the cache entry.
* @param entry
* {@link CacheEntity} to transform into an {@link HttpResponse}
* @return {@link HttpResponse} that was constructed
*/
CloseableHttpResponse generateResponse(final HttpCacheEntry entry) {
final Date now = new Date();
final HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, entry
.getStatusCode(), entry.getReasonPhrase());
response.setHeaders(entry.getAllHeaders());
if (entry.getResource() != null) {
final HttpEntity entity = new CacheEntity(entry);
addMissingContentLengthHeader(response, entity);
response.setEntity(entity);
}
final long age = this.validityStrategy.getCurrentAgeSecs(entry, now);
if (age > 0) {
if (age >= Integer.MAX_VALUE) {
response.setHeader(HeaderConstants.AGE, "2147483648");
} else {
response.setHeader(HeaderConstants.AGE, "" + ((int) age));
}
}
return Proxies.enhanceResponse(response);
}
/**
* Generate a 304 - Not Modified response from a {@link CacheEntity}. This should be
* used to respond to conditional requests, when the entry exists or has been re-validated.
*/
CloseableHttpResponse generateNotModifiedResponse(final HttpCacheEntry entry) {
final HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_NOT_MODIFIED, "Not Modified");
// The response MUST include the following headers
// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
// - Date, unless its omission is required by section 14.8.1
Header dateHeader = entry.getFirstHeader(HTTP.DATE_HEADER);
if (dateHeader == null) {
dateHeader = new BasicHeader(HTTP.DATE_HEADER, DateUtils.formatDate(new Date()));
}
response.addHeader(dateHeader);
// - ETag and/or Content-Location, if the header would have been sent
// in a 200 response to the same request
final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
if (etagHeader != null) {
response.addHeader(etagHeader);
}
final Header contentLocationHeader = entry.getFirstHeader("Content-Location");
if (contentLocationHeader != null) {
response.addHeader(contentLocationHeader);
}
// - Expires, Cache-Control, and/or Vary, if the field-value might
// differ from that sent in any previous response for the same
// variant
final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
if (expiresHeader != null) {
response.addHeader(expiresHeader);
}
final Header cacheControlHeader = entry.getFirstHeader(HeaderConstants.CACHE_CONTROL);
if (cacheControlHeader != null) {
response.addHeader(cacheControlHeader);
}
final Header varyHeader = entry.getFirstHeader(HeaderConstants.VARY);
if (varyHeader != null) {
response.addHeader(varyHeader);
}
return Proxies.enhanceResponse(response);
}
private void addMissingContentLengthHeader(final HttpResponse response, final HttpEntity entity) {
if (transferEncodingIsPresent(response)) {
return;
}
Header contentLength = response.getFirstHeader(HTTP.CONTENT_LEN);
if (contentLength == null) {
contentLength = new BasicHeader(HTTP.CONTENT_LEN, Long.toString(entity
.getContentLength()));
response.setHeader(contentLength);
}
}
private boolean transferEncodingIsPresent(final HttpResponse response) {
final Header hdr = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
return hdr != null;
}
}

View file

@ -0,0 +1,346 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Date;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
/**
* Determines whether a given {@link HttpCacheEntry} is suitable to be
* used as a response for a given {@link HttpRequest}.
*
* @since 4.1
*/
@Immutable
class CachedResponseSuitabilityChecker {
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
private final boolean sharedCache;
private final boolean useHeuristicCaching;
private final float heuristicCoefficient;
private final long heuristicDefaultLifetime;
private final CacheValidityPolicy validityStrategy;
CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
final CacheConfig config) {
super();
this.validityStrategy = validityStrategy;
this.sharedCache = config.isSharedCache();
this.useHeuristicCaching = config.isHeuristicCachingEnabled();
this.heuristicCoefficient = config.getHeuristicCoefficient();
this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime();
}
CachedResponseSuitabilityChecker(final CacheConfig config) {
this(new CacheValidityPolicy(), config);
}
private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Date now) {
if (validityStrategy.isResponseFresh(entry, now)) {
return true;
}
if (useHeuristicCaching &&
validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) {
return true;
}
if (originInsistsOnFreshness(entry)) {
return false;
}
final long maxstale = getMaxStale(request);
if (maxstale == -1) {
return false;
}
return (maxstale > validityStrategy.getStalenessSecs(entry, now));
}
private boolean originInsistsOnFreshness(final HttpCacheEntry entry) {
if (validityStrategy.mustRevalidate(entry)) {
return true;
}
if (!sharedCache) {
return false;
}
return validityStrategy.proxyRevalidate(entry) ||
validityStrategy.hasCacheControlDirective(entry, "s-maxage");
}
private long getMaxStale(final HttpRequest request) {
long maxstale = -1;
for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
if ((elt.getValue() == null || "".equals(elt.getValue().trim()))
&& maxstale == -1) {
maxstale = Long.MAX_VALUE;
} else {
try {
long val = Long.parseLong(elt.getValue());
if (val < 0) {
val = 0;
}
if (maxstale == -1 || val < maxstale) {
maxstale = val;
}
} catch (final NumberFormatException nfe) {
// err on the side of preserving semantic transparency
maxstale = 0;
}
}
}
}
}
return maxstale;
}
/**
* Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
* {@link HttpRequest}
*
* @param host
* {@link HttpHost}
* @param request
* {@link HttpRequest}
* @param entry
* {@link HttpCacheEntry}
* @param now
* Right now in time
* @return boolean yes/no answer
*/
public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Date now) {
if (!isFreshEnough(entry, request, now)) {
log.trace("Cache entry was not fresh enough");
return false;
}
if (!validityStrategy.contentLengthHeaderMatchesActualLength(entry)) {
log.debug("Cache entry Content-Length and header information do not match");
return false;
}
if (hasUnsupportedConditionalHeaders(request)) {
log.debug("Request contained conditional headers we don't handle");
return false;
}
if (!isConditional(request) && entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
return false;
}
if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
return false;
}
for (final Header ccHdr : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for (final HeaderElement elt : ccHdr.getElements()) {
if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
log.trace("Response contained NO CACHE directive, cache was not suitable");
return false;
}
if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) {
log.trace("Response contained NO STORE directive, cache was not suitable");
return false;
}
if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
try {
final int maxage = Integer.parseInt(elt.getValue());
if (validityStrategy.getCurrentAgeSecs(entry, now) > maxage) {
log.trace("Response from cache was NOT suitable due to max age");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed" + ex.getMessage());
return false;
}
}
if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
try {
final int maxstale = Integer.parseInt(elt.getValue());
if (validityStrategy.getFreshnessLifetimeSecs(entry) > maxstale) {
log.trace("Response from cache was not suitable due to Max stale freshness");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed: " + ex.getMessage());
return false;
}
}
if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) {
try {
final long minfresh = Long.parseLong(elt.getValue());
if (minfresh < 0L) {
return false;
}
final long age = validityStrategy.getCurrentAgeSecs(entry, now);
final long freshness = validityStrategy.getFreshnessLifetimeSecs(entry);
if (freshness - age < minfresh) {
log.trace("Response from cache was not suitable due to min fresh " +
"freshness requirement");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed: " + ex.getMessage());
return false;
}
}
}
}
log.trace("Response from cache was suitable");
return true;
}
/**
* Is this request the type of conditional request we support?
* @param request The current httpRequest being made
* @return {@code true} if the request is supported
*/
public boolean isConditional(final HttpRequest request) {
return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
}
/**
* Check that conditionals that are part of this request match
* @param request The current httpRequest being made
* @param entry the cache entry
* @param now right NOW in time
* @return {@code true} if the request matches all conditionals
*/
public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
final boolean hasEtagValidator = hasSupportedEtagValidator(request);
final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
if ((hasEtagValidator && hasLastModifiedValidator)
&& !(etagValidatorMatches && lastModifiedValidatorMatches)) {
return false;
} else if (hasEtagValidator && !etagValidatorMatches) {
return false;
}
if (hasLastModifiedValidator && !lastModifiedValidatorMatches) {
return false;
}
return true;
}
private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null
|| request.getFirstHeader(HeaderConstants.IF_MATCH) != null
|| hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE));
}
private boolean hasSupportedEtagValidator(final HttpRequest request) {
return request.containsHeader(HeaderConstants.IF_NONE_MATCH);
}
private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE);
}
/**
* Check entry against If-None-Match
* @param request The current httpRequest being made
* @param entry the cache entry
* @return boolean does the etag validator match
*/
private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
final Header[] ifNoneMatch = request.getHeaders(HeaderConstants.IF_NONE_MATCH);
if (ifNoneMatch != null) {
for (final Header h : ifNoneMatch) {
for (final HeaderElement elt : h.getElements()) {
final String reqEtag = elt.toString();
if (("*".equals(reqEtag) && etag != null)
|| reqEtag.equals(etag)) {
return true;
}
}
}
}
return false;
}
/**
* Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
* @param request The current httpRequest being made
* @param entry the cache entry
* @param now right NOW in time
* @return boolean Does the last modified header match
*/
private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
final Header lastModifiedHeader = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
Date lastModified = null;
if (lastModifiedHeader != null) {
lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
}
if (lastModified == null) {
return false;
}
for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) {
final Date ifModifiedSince = DateUtils.parseDate(h.getValue());
if (ifModifiedSince != null) {
if (ifModifiedSince.after(now) || lastModified.after(ifModifiedSince)) {
return false;
}
}
}
return true;
}
private boolean hasValidDateField(final HttpRequest request, final String headerName) {
for(final Header h : request.getHeaders(headerName)) {
final Date date = DateUtils.parseDate(h.getValue());
return date != null;
}
return false;
}
}

View file

@ -0,0 +1,870 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpException;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpMessage;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.RequestLine;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.CacheResponseStatus;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheContext;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
import ch.boye.httpclientandroidlib.protocol.HTTP;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
import ch.boye.httpclientandroidlib.util.Args;
import ch.boye.httpclientandroidlib.util.VersionInfo;
/**
* Request executor in the request execution chain that is responsible for
* transparent client-side caching. The current implementation is conditionally
* compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
* although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
* are obeyed too.
* <p/>
* Folks that would like to experiment with alternative storage backends
* should look at the {@link HttpCacheStorage} interface and the related
* package documentation there. You may also be interested in the provided
* {@link ch.boye.httpclientandroidlib.impl.client.cache.ehcache.EhcacheHttpCacheStorage
* EhCache} and {@link
* ch.boye.httpclientandroidlib.impl.client.cache.memcached.MemcachedHttpCacheStorage
* memcached} storage backends.
* <p/>
* Further responsibilities such as communication with the opposite
* endpoint is delegated to the next executor in the request execution
* chain.
*
* @since 4.3
*/
@ThreadSafe // So long as the responseCache implementation is threadsafe
public class CachingExec implements ClientExecChain {
private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
private final AtomicLong cacheHits = new AtomicLong();
private final AtomicLong cacheMisses = new AtomicLong();
private final AtomicLong cacheUpdates = new AtomicLong();
private final Map<ProtocolVersion, String> viaHeaders = new HashMap<ProtocolVersion, String>(4);
private final CacheConfig cacheConfig;
private final ClientExecChain backend;
private final HttpCache responseCache;
private final CacheValidityPolicy validityPolicy;
private final CachedHttpResponseGenerator responseGenerator;
private final CacheableRequestPolicy cacheableRequestPolicy;
private final CachedResponseSuitabilityChecker suitabilityChecker;
private final ConditionalRequestBuilder conditionalRequestBuilder;
private final ResponseProtocolCompliance responseCompliance;
private final RequestProtocolCompliance requestCompliance;
private final ResponseCachingPolicy responseCachingPolicy;
private final AsynchronousValidator asynchRevalidator;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
public CachingExec(
final ClientExecChain backend,
final HttpCache cache,
final CacheConfig config) {
this(backend, cache, config, null);
}
public CachingExec(
final ClientExecChain backend,
final HttpCache cache,
final CacheConfig config,
final AsynchronousValidator asynchRevalidator) {
super();
Args.notNull(backend, "HTTP backend");
Args.notNull(cache, "HttpCache");
this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
this.backend = backend;
this.responseCache = cache;
this.validityPolicy = new CacheValidityPolicy();
this.responseGenerator = new CachedHttpResponseGenerator(this.validityPolicy);
this.cacheableRequestPolicy = new CacheableRequestPolicy();
this.suitabilityChecker = new CachedResponseSuitabilityChecker(this.validityPolicy, this.cacheConfig);
this.conditionalRequestBuilder = new ConditionalRequestBuilder();
this.responseCompliance = new ResponseProtocolCompliance();
this.requestCompliance = new RequestProtocolCompliance(this.cacheConfig.isWeakETagOnPutDeleteAllowed());
this.responseCachingPolicy = new ResponseCachingPolicy(
this.cacheConfig.getMaxObjectSize(), this.cacheConfig.isSharedCache(),
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(), this.cacheConfig.is303CachingEnabled());
this.asynchRevalidator = asynchRevalidator;
}
public CachingExec(
final ClientExecChain backend,
final ResourceFactory resourceFactory,
final HttpCacheStorage storage,
final CacheConfig config) {
this(backend, new BasicHttpCache(resourceFactory, storage, config), config);
}
public CachingExec(final ClientExecChain backend) {
this(backend, new BasicHttpCache(), CacheConfig.DEFAULT);
}
CachingExec(
final ClientExecChain backend,
final HttpCache responseCache,
final CacheValidityPolicy validityPolicy,
final ResponseCachingPolicy responseCachingPolicy,
final CachedHttpResponseGenerator responseGenerator,
final CacheableRequestPolicy cacheableRequestPolicy,
final CachedResponseSuitabilityChecker suitabilityChecker,
final ConditionalRequestBuilder conditionalRequestBuilder,
final ResponseProtocolCompliance responseCompliance,
final RequestProtocolCompliance requestCompliance,
final CacheConfig config,
final AsynchronousValidator asynchRevalidator) {
this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
this.backend = backend;
this.responseCache = responseCache;
this.validityPolicy = validityPolicy;
this.responseCachingPolicy = responseCachingPolicy;
this.responseGenerator = responseGenerator;
this.cacheableRequestPolicy = cacheableRequestPolicy;
this.suitabilityChecker = suitabilityChecker;
this.conditionalRequestBuilder = conditionalRequestBuilder;
this.responseCompliance = responseCompliance;
this.requestCompliance = requestCompliance;
this.asynchRevalidator = asynchRevalidator;
}
/**
* Reports the number of times that the cache successfully responded
* to an {@link HttpRequest} without contacting the origin server.
* @return the number of cache hits
*/
public long getCacheHits() {
return cacheHits.get();
}
/**
* Reports the number of times that the cache contacted the origin
* server because it had no appropriate response cached.
* @return the number of cache misses
*/
public long getCacheMisses() {
return cacheMisses.get();
}
/**
* Reports the number of times that the cache was able to satisfy
* a response by revalidating an existing but stale cache entry.
* @return the number of cache revalidations
*/
public long getCacheUpdates() {
return cacheUpdates.get();
}
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request) throws IOException, HttpException {
return execute(route, request, HttpClientContext.create(), null);
}
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context) throws IOException, HttpException {
return execute(route, request, context, null);
}
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
final HttpHost target = context.getTargetHost();
final String via = generateViaHeader(request.getOriginal());
// default response context
setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
if (clientRequestsOurOptions(request)) {
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
return Proxies.enhanceResponse(new OptionsHttp11Response());
}
final HttpResponse fatalErrorResponse = getFatallyNoncompliantResponse(request, context);
if (fatalErrorResponse != null) {
return Proxies.enhanceResponse(fatalErrorResponse);
}
requestCompliance.makeRequestCompliant(request);
request.addHeader("Via",via);
flushEntriesInvalidatedByRequest(context.getTargetHost(), request);
if (!cacheableRequestPolicy.isServableFromCache(request)) {
log.debug("Request is not servable from cache");
return callBackend(route, request, context, execAware);
}
final HttpCacheEntry entry = satisfyFromCache(target, request);
if (entry == null) {
log.debug("Cache miss");
return handleCacheMiss(route, request, context, execAware);
} else {
return handleCacheHit(route, request, context, execAware, entry);
}
}
private CloseableHttpResponse handleCacheHit(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry entry) throws IOException, HttpException {
final HttpHost target = context.getTargetHost();
recordCacheHit(target, request);
CloseableHttpResponse out = null;
final Date now = getCurrentDate();
if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
log.debug("Cache hit");
out = generateCachedResponse(request, context, entry, now);
} else if (!mayCallBackend(request)) {
log.debug("Cache entry not suitable but only-if-cached requested");
out = generateGatewayTimeout(context);
} else if (!(entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED
&& !suitabilityChecker.isConditional(request))) {
log.debug("Revalidating cache entry");
return revalidateCacheEntry(route, request, context, execAware, entry, now);
} else {
log.debug("Cache entry not usable; calling backend");
return callBackend(route, request, context, execAware);
}
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
context.setAttribute(HttpClientContext.HTTP_TARGET_HOST, target);
context.setAttribute(HttpClientContext.HTTP_REQUEST, request);
context.setAttribute(HttpClientContext.HTTP_RESPONSE, out);
context.setAttribute(HttpClientContext.HTTP_REQ_SENT, Boolean.TRUE);
return out;
}
private CloseableHttpResponse revalidateCacheEntry(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry entry,
final Date now) throws HttpException {
try {
if (asynchRevalidator != null
&& !staleResponseNotAllowed(request, entry, now)
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
log.trace("Serving stale with asynchronous revalidation");
final CloseableHttpResponse resp = generateCachedResponse(request, context, entry, now);
asynchRevalidator.revalidateCacheEntry(this, route, request, context, execAware, entry);
return resp;
}
return revalidateCacheEntry(route, request, context, execAware, entry);
} catch (final IOException ioex) {
return handleRevalidationFailure(request, context, entry, now);
}
}
private CloseableHttpResponse handleCacheMiss(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
final HttpHost target = context.getTargetHost();
recordCacheMiss(target, request);
if (!mayCallBackend(request)) {
return Proxies.enhanceResponse(
new BasicHttpResponse(
HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"));
}
final Map<String, Variant> variants = getExistingCacheVariants(target, request);
if (variants != null && variants.size() > 0) {
return negotiateResponseFromVariants(route, request, context,
execAware, variants);
}
return callBackend(route, request, context, execAware);
}
private HttpCacheEntry satisfyFromCache(
final HttpHost target, final HttpRequestWrapper request) {
HttpCacheEntry entry = null;
try {
entry = responseCache.getCacheEntry(target, request);
} catch (final IOException ioe) {
log.warn("Unable to retrieve entries from cache", ioe);
}
return entry;
}
private HttpResponse getFatallyNoncompliantResponse(
final HttpRequestWrapper request,
final HttpContext context) {
HttpResponse fatalErrorResponse = null;
final List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
for (final RequestProtocolError error : fatalError) {
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
fatalErrorResponse = requestCompliance.getErrorForRequest(error);
}
return fatalErrorResponse;
}
private Map<String, Variant> getExistingCacheVariants(
final HttpHost target,
final HttpRequestWrapper request) {
Map<String,Variant> variants = null;
try {
variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
} catch (final IOException ioe) {
log.warn("Unable to retrieve variant entries from cache", ioe);
}
return variants;
}
private void recordCacheMiss(final HttpHost target, final HttpRequestWrapper request) {
cacheMisses.getAndIncrement();
if (log.isTraceEnabled()) {
final RequestLine rl = request.getRequestLine();
log.trace("Cache miss [host: " + target + "; uri: " + rl.getUri() + "]");
}
}
private void recordCacheHit(final HttpHost target, final HttpRequestWrapper request) {
cacheHits.getAndIncrement();
if (log.isTraceEnabled()) {
final RequestLine rl = request.getRequestLine();
log.trace("Cache hit [host: " + target + "; uri: " + rl.getUri() + "]");
}
}
private void recordCacheUpdate(final HttpContext context) {
cacheUpdates.getAndIncrement();
setResponseStatus(context, CacheResponseStatus.VALIDATED);
}
private void flushEntriesInvalidatedByRequest(
final HttpHost target,
final HttpRequestWrapper request) {
try {
responseCache.flushInvalidatedCacheEntriesFor(target, request);
} catch (final IOException ioe) {
log.warn("Unable to flush invalidated entries from cache", ioe);
}
}
private CloseableHttpResponse generateCachedResponse(final HttpRequestWrapper request,
final HttpContext context, final HttpCacheEntry entry, final Date now) {
final CloseableHttpResponse cachedResponse;
if (request.containsHeader(HeaderConstants.IF_NONE_MATCH)
|| request.containsHeader(HeaderConstants.IF_MODIFIED_SINCE)) {
cachedResponse = responseGenerator.generateNotModifiedResponse(entry);
} else {
cachedResponse = responseGenerator.generateResponse(entry);
}
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
if (validityPolicy.getStalenessSecs(entry, now) > 0L) {
cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\"");
}
return cachedResponse;
}
private CloseableHttpResponse handleRevalidationFailure(
final HttpRequestWrapper request,
final HttpContext context,
final HttpCacheEntry entry,
final Date now) {
if (staleResponseNotAllowed(request, entry, now)) {
return generateGatewayTimeout(context);
} else {
return unvalidatedCacheHit(context, entry);
}
}
private CloseableHttpResponse generateGatewayTimeout(
final HttpContext context) {
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
return Proxies.enhanceResponse(new BasicHttpResponse(
HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT,
"Gateway Timeout"));
}
private CloseableHttpResponse unvalidatedCacheHit(
final HttpContext context, final HttpCacheEntry entry) {
final CloseableHttpResponse cachedResponse = responseGenerator.generateResponse(entry);
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
cachedResponse.addHeader(HeaderConstants.WARNING, "111 localhost \"Revalidation failed\"");
return cachedResponse;
}
private boolean staleResponseNotAllowed(
final HttpRequestWrapper request,
final HttpCacheEntry entry,
final Date now) {
return validityPolicy.mustRevalidate(entry)
|| (cacheConfig.isSharedCache() && validityPolicy.proxyRevalidate(entry))
|| explicitFreshnessRequest(request, entry, now);
}
private boolean mayCallBackend(final HttpRequestWrapper request) {
for (final Header h: request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for (final HeaderElement elt : h.getElements()) {
if ("only-if-cached".equals(elt.getName())) {
log.trace("Request marked only-if-cached");
return false;
}
}
}
return true;
}
private boolean explicitFreshnessRequest(
final HttpRequestWrapper request,
final HttpCacheEntry entry,
final Date now) {
for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
try {
final int maxstale = Integer.parseInt(elt.getValue());
final long age = validityPolicy.getCurrentAgeSecs(entry, now);
final long lifetime = validityPolicy.getFreshnessLifetimeSecs(entry);
if (age - lifetime > maxstale) {
return true;
}
} catch (final NumberFormatException nfe) {
return true;
}
} else if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())
|| HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
return true;
}
}
}
return false;
}
private String generateViaHeader(final HttpMessage msg) {
final ProtocolVersion pv = msg.getProtocolVersion();
final String existingEntry = viaHeaders.get(pv);
if (existingEntry != null) {
return existingEntry;
}
final VersionInfo vi = VersionInfo.loadVersionInfo("ch.boye.httpclientandroidlib.client", getClass().getClassLoader());
final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
String value;
if ("http".equalsIgnoreCase(pv.getProtocol())) {
value = String.format("%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getMajor(), pv.getMinor(),
release);
} else {
value = String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getProtocol(), pv.getMajor(),
pv.getMinor(), release);
}
viaHeaders.put(pv, value);
return value;
}
private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
if (context != null) {
context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, value);
}
}
/**
* Reports whether this {@code CachingHttpClient} implementation
* supports byte-range requests as specified by the {@code Range}
* and {@code Content-Range} headers.
* @return {@code true} if byte-range requests are supported
*/
public boolean supportsRangeAndContentRangeHeaders() {
return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
}
Date getCurrentDate() {
return new Date();
}
boolean clientRequestsOurOptions(final HttpRequest request) {
final RequestLine line = request.getRequestLine();
if (!HeaderConstants.OPTIONS_METHOD.equals(line.getMethod())) {
return false;
}
if (!"*".equals(line.getUri())) {
return false;
}
if (!"0".equals(request.getFirstHeader(HeaderConstants.MAX_FORWARDS).getValue())) {
return false;
}
return true;
}
CloseableHttpResponse callBackend(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
final Date requestDate = getCurrentDate();
log.trace("Calling the backend");
final CloseableHttpResponse backendResponse = backend.execute(route, request, context, execAware);
try {
backendResponse.addHeader("Via", generateViaHeader(backendResponse));
return handleBackendResponse(route, request, context, execAware,
requestDate, getCurrentDate(), backendResponse);
} catch (final IOException ex) {
backendResponse.close();
throw ex;
} catch (final RuntimeException ex) {
backendResponse.close();
throw ex;
}
}
private boolean revalidationResponseIsTooOld(final HttpResponse backendResponse,
final HttpCacheEntry cacheEntry) {
final Header entryDateHeader = cacheEntry.getFirstHeader(HTTP.DATE_HEADER);
final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
if (entryDateHeader != null && responseDateHeader != null) {
final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
final Date respDate = DateUtils.parseDate(responseDateHeader.getValue());
if (entryDate == null || respDate == null) {
// either backend response or cached entry did not have a valid
// Date header, so we can't tell if they are out of order
// according to the origin clock; thus we can skip the
// unconditional retry recommended in 13.2.6 of RFC 2616.
return false;
}
if (respDate.before(entryDate)) {
return true;
}
}
return false;
}
CloseableHttpResponse negotiateResponseFromVariants(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final Map<String, Variant> variants) throws IOException, HttpException {
final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder
.buildConditionalRequestFromVariants(request, variants);
final Date requestDate = getCurrentDate();
final CloseableHttpResponse backendResponse = backend.execute(
route, conditionalRequest, context, execAware);
try {
final Date responseDate = getCurrentDate();
backendResponse.addHeader("Via", generateViaHeader(backendResponse));
if (backendResponse.getStatusLine().getStatusCode() != HttpStatus.SC_NOT_MODIFIED) {
return handleBackendResponse(
route, request, context, execAware,
requestDate, responseDate, backendResponse);
}
final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
if (resultEtagHeader == null) {
log.warn("304 response did not contain ETag");
IOUtils.consume(backendResponse.getEntity());
backendResponse.close();
return callBackend(route, request, context, execAware);
}
final String resultEtag = resultEtagHeader.getValue();
final Variant matchingVariant = variants.get(resultEtag);
if (matchingVariant == null) {
log.debug("304 response did not contain ETag matching one sent in If-None-Match");
IOUtils.consume(backendResponse.getEntity());
backendResponse.close();
return callBackend(route, request, context, execAware);
}
final HttpCacheEntry matchedEntry = matchingVariant.getEntry();
if (revalidationResponseIsTooOld(backendResponse, matchedEntry)) {
IOUtils.consume(backendResponse.getEntity());
backendResponse.close();
return retryRequestUnconditionally(route, request, context, execAware, matchedEntry);
}
recordCacheUpdate(context);
final HttpCacheEntry responseEntry = getUpdatedVariantEntry(
context.getTargetHost(), conditionalRequest, requestDate, responseDate,
backendResponse, matchingVariant, matchedEntry);
backendResponse.close();
final CloseableHttpResponse resp = responseGenerator.generateResponse(responseEntry);
tryToUpdateVariantMap(context.getTargetHost(), request, matchingVariant);
if (shouldSendNotModifiedResponse(request, responseEntry)) {
return responseGenerator.generateNotModifiedResponse(responseEntry);
}
return resp;
} catch (final IOException ex) {
backendResponse.close();
throw ex;
} catch (final RuntimeException ex) {
backendResponse.close();
throw ex;
}
}
private CloseableHttpResponse retryRequestUnconditionally(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry matchedEntry) throws IOException, HttpException {
final HttpRequestWrapper unconditional = conditionalRequestBuilder
.buildUnconditionalRequest(request, matchedEntry);
return callBackend(route, unconditional, context, execAware);
}
private HttpCacheEntry getUpdatedVariantEntry(
final HttpHost target,
final HttpRequestWrapper conditionalRequest,
final Date requestDate,
final Date responseDate,
final CloseableHttpResponse backendResponse,
final Variant matchingVariant,
final HttpCacheEntry matchedEntry) throws IOException {
HttpCacheEntry responseEntry = matchedEntry;
try {
responseEntry = responseCache.updateVariantCacheEntry(target, conditionalRequest,
matchedEntry, backendResponse, requestDate, responseDate, matchingVariant.getCacheKey());
} catch (final IOException ioe) {
log.warn("Could not update cache entry", ioe);
} finally {
backendResponse.close();
}
return responseEntry;
}
private void tryToUpdateVariantMap(
final HttpHost target,
final HttpRequestWrapper request,
final Variant matchingVariant) {
try {
responseCache.reuseVariantEntryFor(target, request, matchingVariant);
} catch (final IOException ioe) {
log.warn("Could not update cache entry to reuse variant", ioe);
}
}
private boolean shouldSendNotModifiedResponse(
final HttpRequestWrapper request,
final HttpCacheEntry responseEntry) {
return (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, responseEntry, new Date()));
}
CloseableHttpResponse revalidateCacheEntry(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final HttpCacheEntry cacheEntry) throws IOException, HttpException {
final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(request, cacheEntry);
Date requestDate = getCurrentDate();
CloseableHttpResponse backendResponse = backend.execute(
route, conditionalRequest, context, execAware);
Date responseDate = getCurrentDate();
if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
backendResponse.close();
final HttpRequestWrapper unconditional = conditionalRequestBuilder
.buildUnconditionalRequest(request, cacheEntry);
requestDate = getCurrentDate();
backendResponse = backend.execute(route, unconditional, context, execAware);
responseDate = getCurrentDate();
}
backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
final int statusCode = backendResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
recordCacheUpdate(context);
}
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
context.getTargetHost(), request, cacheEntry,
backendResponse, requestDate, responseDate);
if (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, updatedEntry, new Date())) {
return responseGenerator
.generateNotModifiedResponse(updatedEntry);
}
return responseGenerator.generateResponse(updatedEntry);
}
if (staleIfErrorAppliesTo(statusCode)
&& !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
try {
final CloseableHttpResponse cachedResponse = responseGenerator.generateResponse(cacheEntry);
cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
return cachedResponse;
} finally {
backendResponse.close();
}
}
return handleBackendResponse(
route, conditionalRequest, context, execAware,
requestDate, responseDate, backendResponse);
}
private boolean staleIfErrorAppliesTo(final int statusCode) {
return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
|| statusCode == HttpStatus.SC_BAD_GATEWAY
|| statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
|| statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
}
CloseableHttpResponse handleBackendResponse(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware,
final Date requestDate,
final Date responseDate,
final CloseableHttpResponse backendResponse) throws IOException {
log.trace("Handling Backend response");
responseCompliance.ensureProtocolCompliance(request, backendResponse);
final HttpHost target = context.getTargetHost();
final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
responseCache.flushInvalidatedCacheEntriesFor(target, request, backendResponse);
if (cacheable && !alreadyHaveNewerCacheEntry(target, request, backendResponse)) {
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
return responseCache.cacheAndReturnResponse(target, request,
backendResponse, requestDate, responseDate);
}
if (!cacheable) {
try {
responseCache.flushCacheEntriesFor(target, request);
} catch (final IOException ioe) {
log.warn("Unable to flush invalid cache entries", ioe);
}
}
return backendResponse;
}
/**
* For 304 Not modified responses, adds a "Last-Modified" header with the
* value of the "If-Modified-Since" header passed in the request. This
* header is required to be able to reuse match the cache entry for
* subsequent requests but as defined in http specifications it is not
* included in 304 responses by backend servers. This header will not be
* included in the resulting response.
*/
private void storeRequestIfModifiedSinceFor304Response(
final HttpRequest request, final HttpResponse backendResponse) {
if (backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
final Header h = request.getFirstHeader("If-Modified-Since");
if (h != null) {
backendResponse.addHeader("Last-Modified", h.getValue());
}
}
}
private boolean alreadyHaveNewerCacheEntry(final HttpHost target, final HttpRequestWrapper request,
final HttpResponse backendResponse) {
HttpCacheEntry existing = null;
try {
existing = responseCache.getCacheEntry(target, request);
} catch (final IOException ioe) {
// nop
}
if (existing == null) {
return false;
}
final Header entryDateHeader = existing.getFirstHeader(HTTP.DATE_HEADER);
if (entryDateHeader == null) {
return false;
}
final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
if (responseDateHeader == null) {
return false;
}
final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue());
if (entryDate == null || responseDate == null) {
return false;
}
return responseDate.before(entryDate);
}
}

View file

@ -0,0 +1,149 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.File;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
/**
* Builder for {@link ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient}
* instances capable of client-side caching.
*
* @since 4.3
*/
public class CachingHttpClientBuilder extends HttpClientBuilder {
private ResourceFactory resourceFactory;
private HttpCacheStorage storage;
private File cacheDir;
private CacheConfig cacheConfig;
private SchedulingStrategy schedulingStrategy;
private HttpCacheInvalidator httpCacheInvalidator;
public static CachingHttpClientBuilder create() {
return new CachingHttpClientBuilder();
}
protected CachingHttpClientBuilder() {
super();
}
public final CachingHttpClientBuilder setResourceFactory(
final ResourceFactory resourceFactory) {
this.resourceFactory = resourceFactory;
return this;
}
public final CachingHttpClientBuilder setHttpCacheStorage(
final HttpCacheStorage storage) {
this.storage = storage;
return this;
}
public final CachingHttpClientBuilder setCacheDir(
final File cacheDir) {
this.cacheDir = cacheDir;
return this;
}
public final CachingHttpClientBuilder setCacheConfig(
final CacheConfig cacheConfig) {
this.cacheConfig = cacheConfig;
return this;
}
public final CachingHttpClientBuilder setSchedulingStrategy(
final SchedulingStrategy schedulingStrategy) {
this.schedulingStrategy = schedulingStrategy;
return this;
}
public final CachingHttpClientBuilder setHttpCacheInvalidator(
final HttpCacheInvalidator cacheInvalidator) {
this.httpCacheInvalidator = cacheInvalidator;
return this;
}
@Override
protected ClientExecChain decorateMainExec(final ClientExecChain mainExec) {
final CacheConfig config = this.cacheConfig != null ? this.cacheConfig : CacheConfig.DEFAULT;
ResourceFactory resourceFactory = this.resourceFactory;
if (resourceFactory == null) {
if (this.cacheDir == null) {
resourceFactory = new HeapResourceFactory();
} else {
resourceFactory = new FileResourceFactory(cacheDir);
}
}
HttpCacheStorage storage = this.storage;
if (storage == null) {
if (this.cacheDir == null) {
storage = new BasicHttpCacheStorage(config);
} else {
final ManagedHttpCacheStorage managedStorage = new ManagedHttpCacheStorage(config);
addCloseable(managedStorage);
storage = managedStorage;
}
}
final AsynchronousValidator revalidator = createAsynchronousRevalidator(config);
final CacheKeyGenerator uriExtractor = new CacheKeyGenerator();
HttpCacheInvalidator cacheInvalidator = this.httpCacheInvalidator;
if (cacheInvalidator == null) {
cacheInvalidator = new CacheInvalidator(uriExtractor, storage);
}
return new CachingExec(mainExec,
new BasicHttpCache(
resourceFactory,
storage, config,
uriExtractor,
cacheInvalidator), config, revalidator);
}
private AsynchronousValidator createAsynchronousRevalidator(final CacheConfig config) {
if (config.getAsynchronousWorkersMax() > 0) {
final SchedulingStrategy configuredSchedulingStrategy = createSchedulingStrategy(config);
final AsynchronousValidator revalidator = new AsynchronousValidator(
configuredSchedulingStrategy);
addCloseable(revalidator);
return revalidator;
}
return null;
}
@SuppressWarnings("resource")
private SchedulingStrategy createSchedulingStrategy(final CacheConfig config) {
return schedulingStrategy != null ? schedulingStrategy : new ImmediateSchedulingStrategy(config);
}
}

View file

@ -0,0 +1,74 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.File;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
/**
* Factory methods for {@link CloseableHttpClient} instances
* capable of client-side caching.
*
* @since 4.3
*/
@Immutable
public class CachingHttpClients {
private CachingHttpClients() {
super();
}
/**
* Creates builder object for construction of custom
* {@link CloseableHttpClient} instances.
*/
public static CachingHttpClientBuilder custom() {
return CachingHttpClientBuilder.create();
}
/**
* Creates {@link CloseableHttpClient} instance that uses a memory bound
* response cache.
*/
public static CloseableHttpClient createMemoryBound() {
return CachingHttpClientBuilder.create().build();
}
/**
* Creates {@link CloseableHttpClient} instance that uses a file system
* bound response cache.
*
* @param cacheDir location of response cache.
*/
public static CloseableHttpClient createFileBound(final File cacheDir) {
return CachingHttpClientBuilder.create().setCacheDir(cacheDir).build();
}
}

View file

@ -0,0 +1,104 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
import ch.boye.httpclientandroidlib.util.Args;
@NotThreadSafe
class CombinedEntity extends AbstractHttpEntity {
private final Resource resource;
private final InputStream combinedStream;
CombinedEntity(final Resource resource, final InputStream instream) throws IOException {
super();
this.resource = resource;
this.combinedStream = new SequenceInputStream(
new ResourceStream(resource.getInputStream()), instream);
}
public long getContentLength() {
return -1;
}
public boolean isRepeatable() {
return false;
}
public boolean isStreaming() {
return true;
}
public InputStream getContent() throws IOException, IllegalStateException {
return this.combinedStream;
}
public void writeTo(final OutputStream outstream) throws IOException {
Args.notNull(outstream, "Output stream");
final InputStream instream = getContent();
try {
int l;
final byte[] tmp = new byte[2048];
while ((l = instream.read(tmp)) != -1) {
outstream.write(tmp, 0, l);
}
} finally {
instream.close();
}
}
private void dispose() {
this.resource.dispose();
}
class ResourceStream extends FilterInputStream {
protected ResourceStream(final InputStream in) {
super(in);
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
dispose();
}
}
}
}

View file

@ -0,0 +1,140 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Map;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.ProtocolException;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
/**
* @since 4.1
*/
@Immutable
class ConditionalRequestBuilder {
/**
* When a {@link HttpCacheEntry} is stale but 'might' be used as a response
* to an {@link ch.boye.httpclientandroidlib.HttpRequest} we will attempt to revalidate
* the entry with the origin. Build the origin {@link ch.boye.httpclientandroidlib.HttpRequest}
* here and return it.
*
* @param request the original request from the caller
* @param cacheEntry the entry that needs to be re-validated
* @return the wrapped request
* @throws ProtocolException when I am unable to build a new origin request.
*/
public HttpRequestWrapper buildConditionalRequest(final HttpRequestWrapper request, final HttpCacheEntry cacheEntry)
throws ProtocolException {
final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
newRequest.setHeaders(request.getAllHeaders());
final Header eTag = cacheEntry.getFirstHeader(HeaderConstants.ETAG);
if (eTag != null) {
newRequest.setHeader(HeaderConstants.IF_NONE_MATCH, eTag.getValue());
}
final Header lastModified = cacheEntry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
if (lastModified != null) {
newRequest.setHeader(HeaderConstants.IF_MODIFIED_SINCE, lastModified.getValue());
}
boolean mustRevalidate = false;
for(final Header h : cacheEntry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE.equalsIgnoreCase(elt.getName())
|| HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE.equalsIgnoreCase(elt.getName())) {
mustRevalidate = true;
break;
}
}
}
if (mustRevalidate) {
newRequest.addHeader(HeaderConstants.CACHE_CONTROL, HeaderConstants.CACHE_CONTROL_MAX_AGE + "=0");
}
return newRequest;
}
/**
* When a {@link HttpCacheEntry} does not exist for a specific
* {@link ch.boye.httpclientandroidlib.HttpRequest} we attempt to see if an existing
* {@link HttpCacheEntry} is appropriate by building a conditional
* {@link ch.boye.httpclientandroidlib.HttpRequest} using the variants' ETag values.
* If no such values exist, the request is unmodified
*
* @param request the original request from the caller
* @param variants
* @return the wrapped request
*/
public HttpRequestWrapper buildConditionalRequestFromVariants(final HttpRequestWrapper request,
final Map<String, Variant> variants) {
final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
newRequest.setHeaders(request.getAllHeaders());
// we do not support partial content so all etags are used
final StringBuilder etags = new StringBuilder();
boolean first = true;
for(final String etag : variants.keySet()) {
if (!first) {
etags.append(",");
}
first = false;
etags.append(etag);
}
newRequest.setHeader(HeaderConstants.IF_NONE_MATCH, etags.toString());
return newRequest;
}
/**
* Returns a request to unconditionally validate a cache entry with
* the origin. In certain cases (due to multiple intervening caches)
* our cache may actually receive a response to a normal conditional
* validation where the Date header is actually older than that of
* our current cache entry. In this case, the protocol recommendation
* is to retry the validation and force syncup with the origin.
* @param request client request we are trying to satisfy
* @param entry existing cache entry we are trying to validate
* @return an unconditional validation request
*/
public HttpRequestWrapper buildUnconditionalRequest(final HttpRequestWrapper request, final HttpCacheEntry entry) {
final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
newRequest.setHeaders(request.getAllHeaders());
newRequest.addHeader(HeaderConstants.CACHE_CONTROL,HeaderConstants.CACHE_CONTROL_NO_CACHE);
newRequest.addHeader(HeaderConstants.PRAGMA,HeaderConstants.CACHE_CONTROL_NO_CACHE);
newRequest.removeHeaders(HeaderConstants.IF_RANGE);
newRequest.removeHeaders(HeaderConstants.IF_MATCH);
newRequest.removeHeaders(HeaderConstants.IF_NONE_MATCH);
newRequest.removeHeaders(HeaderConstants.IF_UNMODIFIED_SINCE);
newRequest.removeHeaders(HeaderConstants.IF_MODIFIED_SINCE);
return newRequest;
}
}

View file

@ -0,0 +1,143 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Implements a bounded failure cache. The oldest entries are discarded when
* the maximum size is exceeded.
*
* @since 4.3
*/
@ThreadSafe
public class DefaultFailureCache implements FailureCache {
static final int DEFAULT_MAX_SIZE = 1000;
static final int MAX_UPDATE_TRIES = 10;
private final int maxSize;
private final ConcurrentMap<String, FailureCacheValue> storage;
/**
* Create a new failure cache with the maximum size of
* {@link #DEFAULT_MAX_SIZE}.
*/
public DefaultFailureCache() {
this(DEFAULT_MAX_SIZE);
}
/**
* Creates a new failure cache with the specified maximum size.
* @param maxSize the maximum number of entries the cache should store
*/
public DefaultFailureCache(final int maxSize) {
this.maxSize = maxSize;
this.storage = new ConcurrentHashMap<String, FailureCacheValue>();
}
public int getErrorCount(final String identifier) {
if (identifier == null) {
throw new IllegalArgumentException("identifier may not be null");
}
final FailureCacheValue storedErrorCode = storage.get(identifier);
return storedErrorCode != null ? storedErrorCode.getErrorCount() : 0;
}
public void resetErrorCount(final String identifier) {
if (identifier == null) {
throw new IllegalArgumentException("identifier may not be null");
}
storage.remove(identifier);
}
public void increaseErrorCount(final String identifier) {
if (identifier == null) {
throw new IllegalArgumentException("identifier may not be null");
}
updateValue(identifier);
removeOldestEntryIfMapSizeExceeded();
}
private void updateValue(final String identifier) {
/**
* Due to concurrency it is possible that someone else is modifying an
* entry before we could write back our updated value. So we keep
* trying until it is our turn.
*
* In case there is a lot of contention on that identifier, a thread
* might starve. Thus it gives up after a certain number of failed
* update tries.
*/
for (int i = 0; i < MAX_UPDATE_TRIES; i++) {
final FailureCacheValue oldValue = storage.get(identifier);
if (oldValue == null) {
final FailureCacheValue newValue = new FailureCacheValue(identifier, 1);
if (storage.putIfAbsent(identifier, newValue) == null) {
return;
}
}
else {
final int errorCount = oldValue.getErrorCount();
if (errorCount == Integer.MAX_VALUE) {
return;
}
final FailureCacheValue newValue = new FailureCacheValue(identifier, errorCount + 1);
if (storage.replace(identifier, oldValue, newValue)) {
return;
}
}
}
}
private void removeOldestEntryIfMapSizeExceeded() {
if (storage.size() > maxSize) {
final FailureCacheValue valueWithOldestTimestamp = findValueWithOldestTimestamp();
if (valueWithOldestTimestamp != null) {
storage.remove(valueWithOldestTimestamp.getKey(), valueWithOldestTimestamp);
}
}
}
private FailureCacheValue findValueWithOldestTimestamp() {
long oldestTimestamp = Long.MAX_VALUE;
FailureCacheValue oldestValue = null;
for (final Map.Entry<String, FailureCacheValue> storageEntry : storage.entrySet()) {
final FailureCacheValue value = storageEntry.getValue();
final long creationTimeInNanos = value.getCreationTimeInNanos();
if (creationTimeInNanos < oldestTimestamp) {
oldestTimestamp = creationTimeInNanos;
oldestValue = storageEntry.getValue();
}
}
return oldestValue;
}
}

View file

@ -0,0 +1,71 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntrySerializationException;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntrySerializer;
/**
* {@link HttpCacheEntrySerializer} implementation that uses the default (native)
* serialization.
*
* @see java.io.Serializable
*
* @since 4.1
*/
@Immutable
public class DefaultHttpCacheEntrySerializer implements HttpCacheEntrySerializer {
public void writeTo(final HttpCacheEntry cacheEntry, final OutputStream os) throws IOException {
final ObjectOutputStream oos = new ObjectOutputStream(os);
try {
oos.writeObject(cacheEntry);
} finally {
oos.close();
}
}
public HttpCacheEntry readFrom(final InputStream is) throws IOException {
final ObjectInputStream ois = new ObjectInputStream(is);
try {
return (HttpCacheEntry) ois.readObject();
} catch (final ClassNotFoundException ex) {
throw new HttpCacheEntrySerializationException("Class not found: " + ex.getMessage(), ex);
} finally {
ois.close();
}
}
}

View file

@ -0,0 +1,174 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* An implementation that backs off exponentially based on the number of
* consecutive failed attempts stored in the
* {@link AsynchronousValidationRequest}. It uses the following defaults:
* <pre>
* no delay in case it was never tried or didn't fail so far
* 6 secs delay for one failed attempt (= {@link #getInitialExpiryInMillis()})
* 60 secs delay for two failed attempts
* 10 mins delay for three failed attempts
* 100 mins delay for four failed attempts
* ~16 hours delay for five failed attempts
* 24 hours delay for six or more failed attempts (= {@link #getMaxExpiryInMillis()})
* </pre>
*
* The following equation is used to calculate the delay for a specific revalidation request:
* <pre>
* delay = {@link #getInitialExpiryInMillis()} * Math.pow({@link #getBackOffRate()}, {@link AsynchronousValidationRequest#getConsecutiveFailedAttempts()} - 1))
* </pre>
* The resulting delay won't exceed {@link #getMaxExpiryInMillis()}.
*
* @since 4.3
*/
@ThreadSafe
public class ExponentialBackOffSchedulingStrategy implements SchedulingStrategy {
public static final long DEFAULT_BACK_OFF_RATE = 10;
public static final long DEFAULT_INITIAL_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(6);
public static final long DEFAULT_MAX_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(86400);
private final long backOffRate;
private final long initialExpiryInMillis;
private final long maxExpiryInMillis;
private final ScheduledExecutorService executor;
/**
* Create a new scheduling strategy using a fixed pool of worker threads.
* @param cacheConfig the thread pool configuration to be used; not <code>null</code>
* @see ch.boye.httpclientandroidlib.impl.client.cache.CacheConfig#getAsynchronousWorkersMax()
* @see #DEFAULT_BACK_OFF_RATE
* @see #DEFAULT_INITIAL_EXPIRY_IN_MILLIS
* @see #DEFAULT_MAX_EXPIRY_IN_MILLIS
*/
public ExponentialBackOffSchedulingStrategy(final CacheConfig cacheConfig) {
this(cacheConfig,
DEFAULT_BACK_OFF_RATE,
DEFAULT_INITIAL_EXPIRY_IN_MILLIS,
DEFAULT_MAX_EXPIRY_IN_MILLIS);
}
/**
* Create a new scheduling strategy by using a fixed pool of worker threads and the
* given parameters to calculated the delay.
*
* @param cacheConfig the thread pool configuration to be used; not <code>null</code>
* @param backOffRate the back off rate to be used; not negative
* @param initialExpiryInMillis the initial expiry in milli seconds; not negative
* @param maxExpiryInMillis the upper limit of the delay in milli seconds; not negative
* @see ch.boye.httpclientandroidlib.impl.client.cache.CacheConfig#getAsynchronousWorkersMax()
* @see ExponentialBackOffSchedulingStrategy
*/
public ExponentialBackOffSchedulingStrategy(
final CacheConfig cacheConfig,
final long backOffRate,
final long initialExpiryInMillis,
final long maxExpiryInMillis) {
this(createThreadPoolFromCacheConfig(cacheConfig),
backOffRate,
initialExpiryInMillis,
maxExpiryInMillis);
}
private static ScheduledThreadPoolExecutor createThreadPoolFromCacheConfig(
final CacheConfig cacheConfig) {
final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(
cacheConfig.getAsynchronousWorkersMax());
scheduledThreadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
return scheduledThreadPoolExecutor;
}
ExponentialBackOffSchedulingStrategy(
final ScheduledExecutorService executor,
final long backOffRate,
final long initialExpiryInMillis,
final long maxExpiryInMillis) {
this.executor = checkNotNull("executor", executor);
this.backOffRate = checkNotNegative("backOffRate", backOffRate);
this.initialExpiryInMillis = checkNotNegative("initialExpiryInMillis", initialExpiryInMillis);
this.maxExpiryInMillis = checkNotNegative("maxExpiryInMillis", maxExpiryInMillis);
}
public void schedule(
final AsynchronousValidationRequest revalidationRequest) {
checkNotNull("revalidationRequest", revalidationRequest);
final int consecutiveFailedAttempts = revalidationRequest.getConsecutiveFailedAttempts();
final long delayInMillis = calculateDelayInMillis(consecutiveFailedAttempts);
executor.schedule(revalidationRequest, delayInMillis, TimeUnit.MILLISECONDS);
}
public void close() {
executor.shutdown();
}
public long getBackOffRate() {
return backOffRate;
}
public long getInitialExpiryInMillis() {
return initialExpiryInMillis;
}
public long getMaxExpiryInMillis() {
return maxExpiryInMillis;
}
protected long calculateDelayInMillis(final int consecutiveFailedAttempts) {
if (consecutiveFailedAttempts > 0) {
final long delayInSeconds = (long) (initialExpiryInMillis *
Math.pow(backOffRate, consecutiveFailedAttempts - 1));
return Math.min(delayInSeconds, maxExpiryInMillis);
}
else {
return 0;
}
}
protected static <T> T checkNotNull(final String parameterName, final T value) {
if (value == null) {
throw new IllegalArgumentException(parameterName + " may not be null");
}
return value;
}
protected static long checkNotNegative(final String parameterName, final long value) {
if (value < 0) {
throw new IllegalArgumentException(parameterName + " may not be negative");
}
return value;
}
}

View file

@ -0,0 +1,57 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
/**
* Increase and reset the number of errors associated with a specific
* identifier.
*
* @since 4.3
*/
public interface FailureCache {
/**
* Get the current error count.
* @param identifier the identifier for which the error count is requested
* @return the currently known error count or zero if there is no record
*/
int getErrorCount(String identifier);
/**
* Reset the error count back to zero.
* @param identifier the identifier for which the error count should be
* reset
*/
void resetErrorCount(String identifier);
/**
* Increases the error count by one.
* @param identifier the identifier for which the error count should be
* increased
*/
void increaseErrorCount(String identifier);
}

View file

@ -0,0 +1,67 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.annotation.Immutable;
/**
* The error count with a creation timestamp and its associated key.
*
* @since 4.3
*/
@Immutable
public class FailureCacheValue {
private final long creationTimeInNanos;
private final String key;
private final int errorCount;
public FailureCacheValue(final String key, final int errorCount) {
this.creationTimeInNanos = System.nanoTime();
this.key = key;
this.errorCount = errorCount;
}
public long getCreationTimeInNanos() {
return creationTimeInNanos;
}
public String getKey()
{
return key;
}
public int getErrorCount() {
return errorCount;
}
@Override
public String toString() {
return "[entry creationTimeInNanos=" + creationTimeInNanos + "; " +
"key=" + key + "; errorCount=" + errorCount + ']';
}
}

View file

@ -0,0 +1,77 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.Resource;
/**
* Cache resource backed by a file.
*
* @since 4.1
*/
@ThreadSafe
public class FileResource implements Resource {
private static final long serialVersionUID = 4132244415919043397L;
private final File file;
private volatile boolean disposed;
public FileResource(final File file) {
super();
this.file = file;
this.disposed = false;
}
synchronized File getFile() {
return this.file;
}
public synchronized InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
public synchronized long length() {
return this.file.length();
}
public synchronized void dispose() {
if (this.disposed) {
return;
}
this.disposed = true;
this.file.delete();
}
}

View file

@ -0,0 +1,111 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.InputLimit;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
/**
* Generates {@link Resource} instances whose body is stored in a temporary file.
*
* @since 4.1
*/
@Immutable
public class FileResourceFactory implements ResourceFactory {
private final File cacheDir;
private final BasicIdGenerator idgen;
public FileResourceFactory(final File cacheDir) {
super();
this.cacheDir = cacheDir;
this.idgen = new BasicIdGenerator();
}
private File generateUniqueCacheFile(final String requestId) {
final StringBuilder buffer = new StringBuilder();
this.idgen.generate(buffer);
buffer.append('.');
final int len = Math.min(requestId.length(), 100);
for (int i = 0; i < len; i++) {
final char ch = requestId.charAt(i);
if (Character.isLetterOrDigit(ch) || ch == '.') {
buffer.append(ch);
} else {
buffer.append('-');
}
}
return new File(this.cacheDir, buffer.toString());
}
public Resource generate(
final String requestId,
final InputStream instream,
final InputLimit limit) throws IOException {
final File file = generateUniqueCacheFile(requestId);
final FileOutputStream outstream = new FileOutputStream(file);
try {
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;
}
}
} finally {
outstream.close();
}
return new FileResource(file);
}
public Resource copy(
final String requestId,
final Resource resource) throws IOException {
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);
}
return new FileResource(file);
}
}

View file

@ -0,0 +1,67 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.Resource;
/**
* Cache resource backed by a byte array on the heap.
*
* @since 4.1
*/
@Immutable
public class HeapResource implements Resource {
private static final long serialVersionUID = -2078599905620463394L;
private final byte[] b;
public HeapResource(final byte[] b) {
super();
this.b = b;
}
byte[] getByteArray() {
return this.b;
}
public InputStream getInputStream() {
return new ByteArrayInputStream(this.b);
}
public long length() {
return this.b.length;
}
public void dispose() {
}
}

View file

@ -0,0 +1,83 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.InputLimit;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
/**
* Generates {@link Resource} instances stored entirely in heap.
*
* @since 4.1
*/
@Immutable
public class HeapResourceFactory implements ResourceFactory {
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());
}
public Resource copy(
final String requestId,
final Resource resource) throws IOException {
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);
}
}

View file

@ -0,0 +1,166 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
/**
* @since 4.1
*/
interface HttpCache {
/**
* Clear all matching {@link HttpCacheEntry}s.
* @param host
* @param request
* @throws IOException
*/
void flushCacheEntriesFor(HttpHost host, HttpRequest request)
throws IOException;
/**
* Clear invalidated matching {@link HttpCacheEntry}s
* @param host
* @param request
* @throws IOException
*/
void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request)
throws IOException;
/** Clear any entries that may be invalidated by the given response to
* a particular request.
* @param host
* @param request
* @param response
*/
void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request,
HttpResponse response);
/**
* Retrieve matching {@link HttpCacheEntry} from the cache if it exists
* @param host
* @param request
* @return the matching {@link HttpCacheEntry} or {@code null}
* @throws IOException
*/
HttpCacheEntry getCacheEntry(HttpHost host, HttpRequest request)
throws IOException;
/**
* Retrieve all variants from the cache, if there are no variants then an empty
* {@link Map} is returned
* @param host
* @param request
* @return a <code>Map</code> mapping Etags to variant cache entries
* @throws IOException
*/
Map<String,Variant> getVariantCacheEntriesWithEtags(HttpHost host, HttpRequest request)
throws IOException;
/**
* Store a {@link HttpResponse} in the cache if possible, and return
* @param host
* @param request
* @param originResponse
* @param requestSent
* @param responseReceived
* @return the {@link HttpResponse}
* @throws IOException
*/
HttpResponse cacheAndReturnResponse(
HttpHost host, HttpRequest request, HttpResponse originResponse,
Date requestSent, Date responseReceived)
throws IOException;
/**
* Store a {@link HttpResponse} in the cache if possible, and return
* @param host
* @param request
* @param originResponse
* @param requestSent
* @param responseReceived
* @return the {@link HttpResponse}
* @throws IOException
*/
CloseableHttpResponse cacheAndReturnResponse(HttpHost host,
HttpRequest request, CloseableHttpResponse originResponse,
Date requestSent, Date responseReceived)
throws IOException;
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
* @param target
* @param request
* @param stale
* @param originResponse
* @param requestSent
* @param responseReceived
* @return the updated {@link HttpCacheEntry}
* @throws IOException
*/
HttpCacheEntry updateCacheEntry(
HttpHost target, HttpRequest request, HttpCacheEntry stale, HttpResponse originResponse,
Date requestSent, Date responseReceived)
throws IOException;
/**
* Update a specific {@link HttpCacheEntry} representing a cached variant
* using a 304 {@link HttpResponse}.
* @param target host for client request
* @param request actual request from upstream client
* @param stale current variant cache entry
* @param originResponse 304 response received from origin
* @param requestSent when the validating request was sent
* @param responseReceived when the validating response was received
* @param cacheKey where in the cache this entry is currently stored
* @return the updated {@link HttpCacheEntry}
* @throws IOException
*/
HttpCacheEntry updateVariantCacheEntry(HttpHost target, HttpRequest request,
HttpCacheEntry stale, HttpResponse originResponse, Date requestSent,
Date responseReceived, String cacheKey)
throws IOException;
/**
* Specifies cache should reuse the given cached variant to satisfy
* requests whose varying headers match those of the given client request.
* @param target host of the upstream client request
* @param req request sent by upstream client
* @param variant variant cache entry to reuse
* @throws IOException may be thrown during cache update
*/
void reuseVariantEntryFor(HttpHost target, final HttpRequest req,
final Variant variant) throws IOException;
}

View file

@ -0,0 +1,109 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.annotation.Immutable;
@Immutable
class IOUtils {
static void consume(final HttpEntity entity) throws IOException {
if (entity == null) {
return;
}
if (entity.isStreaming()) {
final InputStream instream = entity.getContent();
if (instream != null) {
instream.close();
}
}
}
static void copy(final InputStream in, final OutputStream out) throws IOException {
final byte[] buf = new byte[2048];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
static void closeSilently(final Closeable closable) {
try {
closable.close();
} catch (final IOException ignore) {
}
}
static void copyAndClose(final InputStream in, final OutputStream out) throws IOException {
try {
copy(in, out);
in.close();
out.close();
} catch (final IOException ex) {
closeSilently(in);
closeSilently(out);
// Propagate the original exception
throw ex;
}
}
static void copyFile(final File in, final File out) throws IOException {
final RandomAccessFile f1 = new RandomAccessFile(in, "r");
final RandomAccessFile f2 = new RandomAccessFile(out, "rw");
try {
final FileChannel c1 = f1.getChannel();
final FileChannel c2 = f2.getChannel();
try {
c1.transferTo(0, f1.length(), c2);
c1.close();
c2.close();
} catch (final IOException ex) {
closeSilently(c1);
closeSilently(c2);
// Propagate the original exception
throw ex;
}
f1.close();
f2.close();
} catch (final IOException ex) {
closeSilently(f1);
closeSilently(f2);
// Propagate the original exception
throw ex;
}
}
}

View file

@ -0,0 +1,88 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Immediately schedules any incoming validation request. Relies on
* {@link CacheConfig} to configure the used {@link java.util.concurrent.ThreadPoolExecutor}.
*
* @since 4.3
*/
@ThreadSafe
public class ImmediateSchedulingStrategy implements SchedulingStrategy {
private final ExecutorService executor;
/**
* Uses a {@link java.util.concurrent.ThreadPoolExecutor} which is configured according to the
* given {@link CacheConfig}.
* @param cacheConfig specifies thread pool settings. See
* {@link CacheConfig#getAsynchronousWorkersMax()},
* {@link CacheConfig#getAsynchronousWorkersCore()},
* {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
* and {@link CacheConfig#getRevalidationQueueSize()}.
*/
public ImmediateSchedulingStrategy(final CacheConfig cacheConfig) {
this(new ThreadPoolExecutor(
cacheConfig.getAsynchronousWorkersCore(),
cacheConfig.getAsynchronousWorkersMax(),
cacheConfig.getAsynchronousWorkerIdleLifetimeSecs(),
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(cacheConfig.getRevalidationQueueSize()))
);
}
ImmediateSchedulingStrategy(final ExecutorService executor) {
this.executor = executor;
}
public void schedule(final AsynchronousValidationRequest revalidationRequest) {
if (revalidationRequest == null) {
throw new IllegalArgumentException("AsynchronousValidationRequest may not be null");
}
executor.execute(revalidationRequest);
}
public void close() {
executor.shutdown();
}
/**
* Visible for testing.
*/
void awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException {
executor.awaitTermination(timeout, unit);
}
}

View file

@ -0,0 +1,163 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.util.Args;
/**
* {@link HttpCacheStorage} implementation capable of deallocating resources associated with
* the cache entries. This cache keeps track of cache entries using
* {@link java.lang.ref.PhantomReference} and maintains a collection of all resources that
* are no longer in use. The cache, however, does not automatically deallocates associated
* resources by invoking {@link Resource#dispose()} method. The consumer MUST periodically
* call {@link #cleanResources()} method to trigger resource deallocation. The cache can be
* permanently shut down using {@link #shutdown()} method. All resources associated with
* the entries used by the cache will be deallocated.
*
* This {@link HttpCacheStorage} implementation is intended for use with {@link FileResource}
* and similar.
*
* @since 4.1
*/
@ThreadSafe
public class ManagedHttpCacheStorage implements HttpCacheStorage, Closeable {
private final CacheMap entries;
private final ReferenceQueue<HttpCacheEntry> morque;
private final Set<ResourceReference> resources;
private final AtomicBoolean active;
public ManagedHttpCacheStorage(final CacheConfig config) {
super();
this.entries = new CacheMap(config.getMaxCacheEntries());
this.morque = new ReferenceQueue<HttpCacheEntry>();
this.resources = new HashSet<ResourceReference>();
this.active = new AtomicBoolean(true);
}
private void ensureValidState() throws IllegalStateException {
if (!this.active.get()) {
throw new IllegalStateException("Cache has been shut down");
}
}
private void keepResourceReference(final HttpCacheEntry entry) {
final Resource resource = entry.getResource();
if (resource != null) {
// Must deallocate the resource when the entry is no longer in used
final ResourceReference ref = new ResourceReference(entry, this.morque);
this.resources.add(ref);
}
}
public void putEntry(final String url, final HttpCacheEntry entry) throws IOException {
Args.notNull(url, "URL");
Args.notNull(entry, "Cache entry");
ensureValidState();
synchronized (this) {
this.entries.put(url, entry);
keepResourceReference(entry);
}
}
public HttpCacheEntry getEntry(final String url) throws IOException {
Args.notNull(url, "URL");
ensureValidState();
synchronized (this) {
return this.entries.get(url);
}
}
public void removeEntry(final String url) throws IOException {
Args.notNull(url, "URL");
ensureValidState();
synchronized (this) {
// Cannot deallocate the associated resources immediately as the
// cache entry may still be in use
this.entries.remove(url);
}
}
public void updateEntry(
final String url,
final HttpCacheUpdateCallback callback) throws IOException {
Args.notNull(url, "URL");
Args.notNull(callback, "Callback");
ensureValidState();
synchronized (this) {
final HttpCacheEntry existing = this.entries.get(url);
final HttpCacheEntry updated = callback.update(existing);
this.entries.put(url, updated);
if (existing != updated) {
keepResourceReference(updated);
}
}
}
public void cleanResources() {
if (this.active.get()) {
ResourceReference ref;
while ((ref = (ResourceReference) this.morque.poll()) != null) {
synchronized (this) {
this.resources.remove(ref);
}
ref.getResource().dispose();
}
}
}
public void shutdown() {
if (this.active.compareAndSet(true, false)) {
synchronized (this) {
this.entries.clear();
for (final ResourceReference ref: this.resources) {
ref.getResource().dispose();
}
this.resources.clear();
while (this.morque.poll() != null) {
}
}
}
}
public void close() {
shutdown();
}
}

View file

@ -0,0 +1,182 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Locale;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderIterator;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.StatusLine;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.message.AbstractHttpMessage;
import ch.boye.httpclientandroidlib.message.BasicStatusLine;
import ch.boye.httpclientandroidlib.params.BasicHttpParams;
import ch.boye.httpclientandroidlib.params.HttpParams;
/**
* @since 4.1
*/
@SuppressWarnings("deprecation")
@Immutable
final class OptionsHttp11Response extends AbstractHttpMessage implements HttpResponse {
private final StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_NOT_IMPLEMENTED, "");
private final ProtocolVersion version = HttpVersion.HTTP_1_1;
public StatusLine getStatusLine() {
return statusLine;
}
public void setStatusLine(final StatusLine statusline) {
// No-op on purpose, this class is not going to be doing any work.
}
public void setStatusLine(final ProtocolVersion ver, final int code) {
// No-op on purpose, this class is not going to be doing any work.
}
public void setStatusLine(final ProtocolVersion ver, final int code, final String reason) {
// No-op on purpose, this class is not going to be doing any work.
}
public void setStatusCode(final int code) throws IllegalStateException {
// No-op on purpose, this class is not going to be doing any work.
}
public void setReasonPhrase(final String reason) throws IllegalStateException {
// No-op on purpose, this class is not going to be doing any work.
}
public HttpEntity getEntity() {
return null;
}
public void setEntity(final HttpEntity entity) {
// No-op on purpose, this class is not going to be doing any work.
}
public Locale getLocale() {
return null;
}
public void setLocale(final Locale loc) {
// No-op on purpose, this class is not going to be doing any work.
}
public ProtocolVersion getProtocolVersion() {
return version;
}
@Override
public boolean containsHeader(final String name) {
return this.headergroup.containsHeader(name);
}
@Override
public Header[] getHeaders(final String name) {
return this.headergroup.getHeaders(name);
}
@Override
public Header getFirstHeader(final String name) {
return this.headergroup.getFirstHeader(name);
}
@Override
public Header getLastHeader(final String name) {
return this.headergroup.getLastHeader(name);
}
@Override
public Header[] getAllHeaders() {
return this.headergroup.getAllHeaders();
}
@Override
public void addHeader(final Header header) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void addHeader(final String name, final String value) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void setHeader(final Header header) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void setHeader(final String name, final String value) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void setHeaders(final Header[] headers) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void removeHeader(final Header header) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public void removeHeaders(final String name) {
// No-op on purpose, this class is not going to be doing any work.
}
@Override
public HeaderIterator headerIterator() {
return this.headergroup.iterator();
}
@Override
public HeaderIterator headerIterator(final String name) {
return this.headergroup.iterator(name);
}
@Override
public HttpParams getParams() {
if (this.params == null) {
this.params = new BasicHttpParams();
}
return this.params;
}
@Override
public void setParams(final HttpParams params) {
// No-op on purpose, this class is not going to be doing any work.
}
}

View file

@ -0,0 +1,56 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.lang.reflect.Proxy;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.util.Args;
/**
* Proxies for HTTP message objects.
*
* @since 4.3
*/
@NotThreadSafe
class Proxies {
public static CloseableHttpResponse enhanceResponse(final HttpResponse original) {
Args.notNull(original, "HTTP response");
if (original instanceof CloseableHttpResponse) {
return (CloseableHttpResponse) original;
} else {
return (CloseableHttpResponse) Proxy.newProxyInstance(
ResponseProxyHandler.class.getClassLoader(),
new Class<?>[] { CloseableHttpResponse.class },
new ResponseProxyHandler(original));
}
}
}

View file

@ -0,0 +1,376 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
import ch.boye.httpclientandroidlib.entity.ContentType;
import ch.boye.httpclientandroidlib.message.BasicHeader;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
import ch.boye.httpclientandroidlib.message.BasicStatusLine;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* @since 4.1
*/
@Immutable
class RequestProtocolCompliance {
private final boolean weakETagOnPutDeleteAllowed;
public RequestProtocolCompliance() {
super();
this.weakETagOnPutDeleteAllowed = false;
}
public RequestProtocolCompliance(final boolean weakETagOnPutDeleteAllowed) {
super();
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
}
private static final List<String> disallowedWithNoCache =
Arrays.asList(HeaderConstants.CACHE_CONTROL_MIN_FRESH, HeaderConstants.CACHE_CONTROL_MAX_STALE, HeaderConstants.CACHE_CONTROL_MAX_AGE);
/**
* Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not
* and if not, we can not continue.
*
* @param request the HttpRequest Object
* @return list of {@link RequestProtocolError}
*/
public List<RequestProtocolError> requestIsFatallyNonCompliant(final HttpRequest request) {
final List<RequestProtocolError> theErrors = new ArrayList<RequestProtocolError>();
RequestProtocolError anError = requestHasWeakETagAndRange(request);
if (anError != null) {
theErrors.add(anError);
}
if (!weakETagOnPutDeleteAllowed) {
anError = requestHasWeekETagForPUTOrDELETEIfMatch(request);
if (anError != null) {
theErrors.add(anError);
}
}
anError = requestContainsNoCacheDirectiveWithFieldName(request);
if (anError != null) {
theErrors.add(anError);
}
return theErrors;
}
/**
* If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and
* fix the request here.
*
* @param request the request to check for compliance
* @throws ClientProtocolException when we have trouble making the request compliant
*/
public void makeRequestCompliant(final HttpRequestWrapper request)
throws ClientProtocolException {
if (requestMustNotHaveEntity(request)) {
((HttpEntityEnclosingRequest) request).setEntity(null);
}
verifyRequestWithExpectContinueFlagHas100continueHeader(request);
verifyOPTIONSRequestWithBodyHasContentType(request);
decrementOPTIONSMaxForwardsIfGreaterThen0(request);
stripOtherFreshnessDirectivesWithNoCache(request);
if (requestVersionIsTooLow(request)
|| requestMinorVersionIsTooHighMajorVersionsMatch(request)) {
request.setProtocolVersion(HttpVersion.HTTP_1_1);
}
}
private void stripOtherFreshnessDirectivesWithNoCache(final HttpRequest request) {
final List<HeaderElement> outElts = new ArrayList<HeaderElement>();
boolean shouldStrip = false;
for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (!disallowedWithNoCache.contains(elt.getName())) {
outElts.add(elt);
}
if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
shouldStrip = true;
}
}
}
if (!shouldStrip) {
return;
}
request.removeHeaders(HeaderConstants.CACHE_CONTROL);
request.setHeader(HeaderConstants.CACHE_CONTROL, buildHeaderFromElements(outElts));
}
private String buildHeaderFromElements(final List<HeaderElement> outElts) {
final StringBuilder newHdr = new StringBuilder("");
boolean first = true;
for(final HeaderElement elt : outElts) {
if (!first) {
newHdr.append(",");
} else {
first = false;
}
newHdr.append(elt.toString());
}
return newHdr.toString();
}
private boolean requestMustNotHaveEntity(final HttpRequest request) {
return HeaderConstants.TRACE_METHOD.equals(request.getRequestLine().getMethod())
&& request instanceof HttpEntityEnclosingRequest;
}
private void decrementOPTIONSMaxForwardsIfGreaterThen0(final HttpRequest request) {
if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) {
return;
}
final Header maxForwards = request.getFirstHeader(HeaderConstants.MAX_FORWARDS);
if (maxForwards == null) {
return;
}
request.removeHeaders(HeaderConstants.MAX_FORWARDS);
final int currentMaxForwards = Integer.parseInt(maxForwards.getValue());
request.setHeader(HeaderConstants.MAX_FORWARDS, Integer.toString(currentMaxForwards - 1));
}
private void verifyOPTIONSRequestWithBodyHasContentType(final HttpRequest request) {
if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) {
return;
}
if (!(request instanceof HttpEntityEnclosingRequest)) {
return;
}
addContentTypeHeaderIfMissing((HttpEntityEnclosingRequest) request);
}
private void addContentTypeHeaderIfMissing(final HttpEntityEnclosingRequest request) {
if (request.getEntity().getContentType() == null) {
((AbstractHttpEntity) request.getEntity()).setContentType(
ContentType.APPLICATION_OCTET_STREAM.getMimeType());
}
}
private void verifyRequestWithExpectContinueFlagHas100continueHeader(final HttpRequest request) {
if (request instanceof HttpEntityEnclosingRequest) {
if (((HttpEntityEnclosingRequest) request).expectContinue()
&& ((HttpEntityEnclosingRequest) request).getEntity() != null) {
add100ContinueHeaderIfMissing(request);
} else {
remove100ContinueHeaderIfExists(request);
}
} else {
remove100ContinueHeaderIfExists(request);
}
}
private void remove100ContinueHeaderIfExists(final HttpRequest request) {
boolean hasHeader = false;
final Header[] expectHeaders = request.getHeaders(HTTP.EXPECT_DIRECTIVE);
List<HeaderElement> expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();
for (final Header h : expectHeaders) {
for (final HeaderElement elt : h.getElements()) {
if (!(HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName()))) {
expectElementsThatAreNot100Continue.add(elt);
} else {
hasHeader = true;
}
}
if (hasHeader) {
request.removeHeader(h);
for (final HeaderElement elt : expectElementsThatAreNot100Continue) {
final BasicHeader newHeader = new BasicHeader(HTTP.EXPECT_DIRECTIVE, elt.getName());
request.addHeader(newHeader);
}
return;
} else {
expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();
}
}
}
private void add100ContinueHeaderIfMissing(final HttpRequest request) {
boolean hasHeader = false;
for (final Header h : request.getHeaders(HTTP.EXPECT_DIRECTIVE)) {
for (final HeaderElement elt : h.getElements()) {
if (HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName())) {
hasHeader = true;
}
}
}
if (!hasHeader) {
request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
}
}
protected boolean requestMinorVersionIsTooHighMajorVersionsMatch(final HttpRequest request) {
final ProtocolVersion requestProtocol = request.getProtocolVersion();
if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) {
return false;
}
if (requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor()) {
return true;
}
return false;
}
protected boolean requestVersionIsTooLow(final HttpRequest request) {
return request.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) < 0;
}
/**
* Extract error information about the {@link HttpRequest} telling the 'caller'
* that a problem occured.
*
* @param errorCheck What type of error should I get
* @return The {@link HttpResponse} that is the error generated
*/
public HttpResponse getErrorForRequest(final RequestProtocolError errorCheck) {
switch (errorCheck) {
case BODY_BUT_NO_LENGTH_ERROR:
return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_LENGTH_REQUIRED, ""));
case WEAK_ETAG_AND_RANGE_ERROR:
return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_BAD_REQUEST, "Weak eTag not compatible with byte range"));
case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR:
return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_BAD_REQUEST,
"Weak eTag not compatible with PUT or DELETE requests"));
case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME:
return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_BAD_REQUEST,
"No-Cache directive MUST NOT include a field name"));
default:
throw new IllegalStateException(
"The request was compliant, therefore no error can be generated for it.");
}
}
private RequestProtocolError requestHasWeakETagAndRange(final HttpRequest request) {
// TODO: Should these be looking at all the headers marked as Range?
final String method = request.getRequestLine().getMethod();
if (!(HeaderConstants.GET_METHOD.equals(method))) {
return null;
}
final Header range = request.getFirstHeader(HeaderConstants.RANGE);
if (range == null) {
return null;
}
final Header ifRange = request.getFirstHeader(HeaderConstants.IF_RANGE);
if (ifRange == null) {
return null;
}
final String val = ifRange.getValue();
if (val.startsWith("W/")) {
return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR;
}
return null;
}
private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch(final HttpRequest request) {
// TODO: Should these be looking at all the headers marked as If-Match/If-None-Match?
final String method = request.getRequestLine().getMethod();
if (!(HeaderConstants.PUT_METHOD.equals(method) || HeaderConstants.DELETE_METHOD
.equals(method))) {
return null;
}
final Header ifMatch = request.getFirstHeader(HeaderConstants.IF_MATCH);
if (ifMatch != null) {
final String val = ifMatch.getValue();
if (val.startsWith("W/")) {
return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
}
} else {
final Header ifNoneMatch = request.getFirstHeader(HeaderConstants.IF_NONE_MATCH);
if (ifNoneMatch == null) {
return null;
}
final String val2 = ifNoneMatch.getValue();
if (val2.startsWith("W/")) {
return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
}
}
return null;
}
private RequestProtocolError requestContainsNoCacheDirectiveWithFieldName(final HttpRequest request) {
for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
for(final HeaderElement elt : h.getElements()) {
if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equalsIgnoreCase(elt.getName())
&& elt.getValue() != null) {
return RequestProtocolError.NO_CACHE_DIRECTIVE_WITH_FIELD_NAME;
}
}
}
return null;
}
}

View file

@ -0,0 +1,40 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
/**
* @since 4.1
*/
enum RequestProtocolError {
UNKNOWN,
BODY_BUT_NO_LENGTH_ERROR,
WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR,
WEAK_ETAG_AND_RANGE_ERROR,
NO_CACHE_DIRECTIVE_WITH_FIELD_NAME
}

View file

@ -0,0 +1,62 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.util.Args;
@Immutable
class ResourceReference extends PhantomReference<HttpCacheEntry> {
private final Resource resource;
public ResourceReference(final HttpCacheEntry entry, final ReferenceQueue<HttpCacheEntry> q) {
super(entry, q);
Args.notNull(entry.getResource(), "Resource");
this.resource = entry.getResource();
}
public Resource getResource() {
return this.resource;
}
@Override
public int hashCode() {
return this.resource.hashCode();
}
@Override
public boolean equals(final Object obj) {
return this.resource.equals(obj);
}
}

View file

@ -0,0 +1,311 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
/* LogFactory removed by HttpClient for Android script. */
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpMessage;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* Determines if an HttpResponse can be cached.
*
* @since 4.1
*/
@Immutable
class ResponseCachingPolicy {
private static final String[] AUTH_CACHEABLE_PARAMS = {
"s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC
};
private final long maxObjectSizeBytes;
private final boolean sharedCache;
private final boolean neverCache1_0ResponsesWithQueryString;
public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
private static final Set<Integer> cacheableStatuses =
new HashSet<Integer>(Arrays.asList(HttpStatus.SC_OK,
HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
HttpStatus.SC_MULTIPLE_CHOICES,
HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_GONE));
private final Set<Integer> uncacheableStatuses;
/**
* Define a cache policy that limits the size of things that should be stored
* in the cache to a maximum of {@link HttpResponse} bytes in size.
*
* @param maxObjectSizeBytes the size to limit items into the cache
* @param sharedCache whether to behave as a shared cache (true) or a
* non-shared/private cache (false)
* @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
* to cache if explicit cache headers are found.
* @param allow303Caching if this policy is permitted to cache 303 response
*/
public ResponseCachingPolicy(final long maxObjectSizeBytes,
final boolean sharedCache,
final boolean neverCache1_0ResponsesWithQueryString,
final boolean allow303Caching) {
this.maxObjectSizeBytes = maxObjectSizeBytes;
this.sharedCache = sharedCache;
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
if (allow303Caching) {
uncacheableStatuses = new HashSet<Integer>(
Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT));
} else {
uncacheableStatuses = new HashSet<Integer>(Arrays.asList(
HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
}
}
/**
* Determines if an HttpResponse can be cached.
*
* @param httpMethod What type of request was this, a GET, PUT, other?
* @param response The origin response
* @return <code>true</code> if response is cacheable
*/
public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
boolean cacheable = false;
if (!HeaderConstants.GET_METHOD.equals(httpMethod)) {
log.debug("Response was not cacheable.");
return false;
}
final int status = response.getStatusLine().getStatusCode();
if (cacheableStatuses.contains(status)) {
// these response codes MAY be cached
cacheable = true;
} else if (uncacheableStatuses.contains(status)) {
return false;
} else if (unknownStatusCode(status)) {
// a response with an unknown status code MUST NOT be
// cached
return false;
}
final Header contentLength = response.getFirstHeader(HTTP.CONTENT_LEN);
if (contentLength != null) {
final int contentLengthValue = Integer.parseInt(contentLength.getValue());
if (contentLengthValue > this.maxObjectSizeBytes) {
return false;
}
}
final Header[] ageHeaders = response.getHeaders(HeaderConstants.AGE);
if (ageHeaders.length > 1) {
return false;
}
final Header[] expiresHeaders = response.getHeaders(HeaderConstants.EXPIRES);
if (expiresHeaders.length > 1) {
return false;
}
final Header[] dateHeaders = response.getHeaders(HTTP.DATE_HEADER);
if (dateHeaders.length != 1) {
return false;
}
final Date date = DateUtils.parseDate(dateHeaders[0].getValue());
if (date == null) {
return false;
}
for (final Header varyHdr : response.getHeaders(HeaderConstants.VARY)) {
for (final HeaderElement elem : varyHdr.getElements()) {
if ("*".equals(elem.getName())) {
return false;
}
}
}
if (isExplicitlyNonCacheable(response)) {
return false;
}
return (cacheable || isExplicitlyCacheable(response));
}
private boolean unknownStatusCode(final int status) {
if (status >= 100 && status <= 101) {
return false;
}
if (status >= 200 && status <= 206) {
return false;
}
if (status >= 300 && status <= 307) {
return false;
}
if (status >= 400 && status <= 417) {
return false;
}
if (status >= 500 && status <= 505) {
return false;
}
return true;
}
protected boolean isExplicitlyNonCacheable(final HttpResponse response) {
final Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL);
for (final Header header : cacheControlHeaders) {
for (final HeaderElement elem : header.getElements()) {
if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName())
|| HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName())
|| (sharedCache && HeaderConstants.PRIVATE.equals(elem.getName()))) {
return true;
}
}
}
return false;
}
protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
final Header[] cacheControlHeaders = msg.getHeaders(HeaderConstants.CACHE_CONTROL);
for (final Header header : cacheControlHeaders) {
for (final HeaderElement elem : header.getElements()) {
for (final String param : params) {
if (param.equalsIgnoreCase(elem.getName())) {
return true;
}
}
}
}
return false;
}
protected boolean isExplicitlyCacheable(final HttpResponse response) {
if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) {
return true;
}
final String[] cacheableParams = { HeaderConstants.CACHE_CONTROL_MAX_AGE, "s-maxage",
HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE,
HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE,
HeaderConstants.PUBLIC
};
return hasCacheControlParameterFrom(response, cacheableParams);
}
/**
* Determine if the {@link HttpResponse} gotten from the origin is a
* cacheable response.
*
* @param request the {@link HttpRequest} that generated an origin hit
* @param response the {@link HttpResponse} from the origin
* @return <code>true</code> if response is cacheable
*/
public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
if (requestProtocolGreaterThanAccepted(request)) {
log.debug("Response was not cacheable.");
return false;
}
final String[] uncacheableRequestDirectives = { HeaderConstants.CACHE_CONTROL_NO_STORE };
if (hasCacheControlParameterFrom(request,uncacheableRequestDirectives)) {
return false;
}
if (request.getRequestLine().getUri().contains("?")) {
if (neverCache1_0ResponsesWithQueryString && from1_0Origin(response)) {
log.debug("Response was not cacheable as it had a query string.");
return false;
} else if (!isExplicitlyCacheable(response)) {
log.debug("Response was not cacheable as it is missing explicit caching headers.");
return false;
}
}
if (expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(response)) {
return false;
}
if (sharedCache) {
final Header[] authNHeaders = request.getHeaders(HeaderConstants.AUTHORIZATION);
if (authNHeaders != null && authNHeaders.length > 0
&& !hasCacheControlParameterFrom(response, AUTH_CACHEABLE_PARAMS)) {
return false;
}
}
final String method = request.getRequestLine().getMethod();
return isResponseCacheable(method, response);
}
private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(
final HttpResponse response) {
if (response.getFirstHeader(HeaderConstants.CACHE_CONTROL) != null) {
return false;
}
final Header expiresHdr = response.getFirstHeader(HeaderConstants.EXPIRES);
final Header dateHdr = response.getFirstHeader(HTTP.DATE_HEADER);
if (expiresHdr == null || dateHdr == null) {
return false;
}
final Date expires = DateUtils.parseDate(expiresHdr.getValue());
final Date date = DateUtils.parseDate(dateHdr.getValue());
if (expires == null || date == null) {
return false;
}
return expires.equals(date) || expires.before(date);
}
private boolean from1_0Origin(final HttpResponse response) {
final Header via = response.getFirstHeader(HeaderConstants.VIA);
if (via != null) {
for(final HeaderElement elt : via.getElements()) {
final String proto = elt.toString().split("\\s")[0];
if (proto.contains("/")) {
return proto.equals("HTTP/1.0");
} else {
return proto.equals("1.0");
}
}
}
return HttpVersion.HTTP_1_0.equals(response.getProtocolVersion());
}
private boolean requestProtocolGreaterThanAccepted(final HttpRequest req) {
return req.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) > 0;
}
}

View file

@ -0,0 +1,251 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.HttpVersion;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
import ch.boye.httpclientandroidlib.message.BasicHeader;
import ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* @since 4.1
*/
@Immutable
class ResponseProtocolCompliance {
private static final String UNEXPECTED_100_CONTINUE = "The incoming request did not contain a "
+ "100-continue header, but the response was a Status 100, continue.";
private static final String UNEXPECTED_PARTIAL_CONTENT = "partial content was returned for a request that did not ask for it";
/**
* When we get a response from a down stream server (Origin Server)
* we attempt to see if it is HTTP 1.1 Compliant and if not, attempt to
* make it so.
*
* @param request The {@link HttpRequest} that generated an origin hit and response
* @param response The {@link HttpResponse} from the origin server
* @throws IOException Bad things happened
*/
public void ensureProtocolCompliance(final HttpRequestWrapper request, final HttpResponse response)
throws IOException {
if (backendResponseMustNotHaveBody(request, response)) {
consumeBody(response);
response.setEntity(null);
}
requestDidNotExpect100ContinueButResponseIsOne(request, response);
transferEncodingIsNotReturnedTo1_0Client(request, response);
ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(request, response);
ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(request, response);
ensure206ContainsDateHeader(response);
ensure304DoesNotContainExtraEntityHeaders(response);
identityIsNotUsedInContentEncoding(response);
warningsWithNonMatchingWarnDatesAreRemoved(response);
}
private void consumeBody(final HttpResponse response) throws IOException {
final HttpEntity body = response.getEntity();
if (body != null) {
IOUtils.consume(body);
}
}
private void warningsWithNonMatchingWarnDatesAreRemoved(
final HttpResponse response) {
final Date responseDate = DateUtils.parseDate(response.getFirstHeader(HTTP.DATE_HEADER).getValue());
if (responseDate == null) {
return;
}
final Header[] warningHeaders = response.getHeaders(HeaderConstants.WARNING);
if (warningHeaders == null || warningHeaders.length == 0) {
return;
}
final List<Header> newWarningHeaders = new ArrayList<Header>();
boolean modified = false;
for(final Header h : warningHeaders) {
for(final WarningValue wv : WarningValue.getWarningValues(h)) {
final Date warnDate = wv.getWarnDate();
if (warnDate == null || warnDate.equals(responseDate)) {
newWarningHeaders.add(new BasicHeader(HeaderConstants.WARNING,wv.toString()));
} else {
modified = true;
}
}
}
if (modified) {
response.removeHeaders(HeaderConstants.WARNING);
for(final Header h : newWarningHeaders) {
response.addHeader(h);
}
}
}
private void identityIsNotUsedInContentEncoding(final HttpResponse response) {
final Header[] hdrs = response.getHeaders(HTTP.CONTENT_ENCODING);
if (hdrs == null || hdrs.length == 0) {
return;
}
final List<Header> newHeaders = new ArrayList<Header>();
boolean modified = false;
for (final Header h : hdrs) {
final StringBuilder buf = new StringBuilder();
boolean first = true;
for (final HeaderElement elt : h.getElements()) {
if ("identity".equalsIgnoreCase(elt.getName())) {
modified = true;
} else {
if (!first) {
buf.append(",");
}
buf.append(elt.toString());
first = false;
}
}
final String newHeaderValue = buf.toString();
if (!"".equals(newHeaderValue)) {
newHeaders.add(new BasicHeader(HTTP.CONTENT_ENCODING, newHeaderValue));
}
}
if (!modified) {
return;
}
response.removeHeaders(HTTP.CONTENT_ENCODING);
for (final Header h : newHeaders) {
response.addHeader(h);
}
}
private void ensure206ContainsDateHeader(final HttpResponse response) {
if (response.getFirstHeader(HTTP.DATE_HEADER) == null) {
response.addHeader(HTTP.DATE_HEADER, DateUtils.formatDate(new Date()));
}
}
private void ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(final HttpRequest request,
final HttpResponse response) throws IOException {
if (request.getFirstHeader(HeaderConstants.RANGE) != null
|| response.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT) {
return;
}
consumeBody(response);
throw new ClientProtocolException(UNEXPECTED_PARTIAL_CONTENT);
}
private void ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(final HttpRequest request,
final HttpResponse response) {
if (!request.getRequestLine().getMethod().equalsIgnoreCase(HeaderConstants.OPTIONS_METHOD)) {
return;
}
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
return;
}
if (response.getFirstHeader(HTTP.CONTENT_LEN) == null) {
response.addHeader(HTTP.CONTENT_LEN, "0");
}
}
private void ensure304DoesNotContainExtraEntityHeaders(final HttpResponse response) {
final String[] disallowedEntityHeaders = { HeaderConstants.ALLOW, HTTP.CONTENT_ENCODING,
"Content-Language", HTTP.CONTENT_LEN, "Content-MD5",
"Content-Range", HTTP.CONTENT_TYPE, HeaderConstants.LAST_MODIFIED
};
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
for(final String hdr : disallowedEntityHeaders) {
response.removeHeaders(hdr);
}
}
}
private boolean backendResponseMustNotHaveBody(final HttpRequest request, final HttpResponse backendResponse) {
return HeaderConstants.HEAD_METHOD.equals(request.getRequestLine().getMethod())
|| backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT
|| backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_RESET_CONTENT
|| backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED;
}
private void requestDidNotExpect100ContinueButResponseIsOne(final HttpRequestWrapper request,
final HttpResponse response) throws IOException {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CONTINUE) {
return;
}
final HttpRequest originalRequest = request.getOriginal();
if (originalRequest instanceof HttpEntityEnclosingRequest) {
if (((HttpEntityEnclosingRequest)originalRequest).expectContinue()) {
return;
}
}
consumeBody(response);
throw new ClientProtocolException(UNEXPECTED_100_CONTINUE);
}
private void transferEncodingIsNotReturnedTo1_0Client(final HttpRequestWrapper request,
final HttpResponse response) {
final HttpRequest originalRequest = request.getOriginal();
if (originalRequest.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) >= 0) {
return;
}
removeResponseTransferEncoding(response);
}
private void removeResponseTransferEncoding(final HttpResponse response) {
response.removeHeaders("TE");
response.removeHeaders(HTTP.TRANSFER_ENCODING);
}
}

View file

@ -0,0 +1,88 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
/**
* A proxy class that can enhance an arbitrary {@link HttpResponse} with
* {@link Closeable#close()} method.
*
* @since 4.3
*/
@NotThreadSafe
class ResponseProxyHandler implements InvocationHandler {
private static final Method CLOSE_METHOD;
static {
try {
CLOSE_METHOD = Closeable.class.getMethod("close");
} catch (final NoSuchMethodException ex) {
throw new Error(ex);
}
}
private final HttpResponse original;
ResponseProxyHandler(final HttpResponse original) {
super();
this.original = original;
}
public void close() throws IOException {
IOUtils.consume(original.getEntity());
}
public Object invoke(
final Object proxy, final Method method, final Object[] args) throws Throwable {
if (method.equals(CLOSE_METHOD)) {
close();
return null;
} else {
try {
return method.invoke(this.original, args);
} catch (final InvocationTargetException ex) {
final Throwable cause = ex.getCause();
if (cause != null) {
throw cause;
} else {
throw ex;
}
}
}
}
}

View file

@ -0,0 +1,45 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.Closeable;
/**
* Specifies when revalidation requests are scheduled.
*
* @since 4.3
*/
public interface SchedulingStrategy extends Closeable
{
/**
* Schedule an {@link AsynchronousValidationRequest} to be executed.
*
* @param revalidationRequest the request to be executed; not <code>null</code>
* @throws java.util.concurrent.RejectedExecutionException if the request could not be scheduled for execution
*/
void schedule(AsynchronousValidationRequest revalidationRequest);
}

View file

@ -0,0 +1,150 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Proxy;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
import ch.boye.httpclientandroidlib.client.cache.InputLimit;
import ch.boye.httpclientandroidlib.client.cache.Resource;
import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
/**
* @since 4.1
*/
@NotThreadSafe
class SizeLimitedResponseReader {
private final ResourceFactory resourceFactory;
private final long maxResponseSizeBytes;
private final HttpRequest request;
private final CloseableHttpResponse response;
private InputStream instream;
private InputLimit limit;
private Resource resource;
private boolean consumed;
/**
* Create an {@link HttpResponse} 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 CloseableHttpResponse 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.getRequestLine().getUri();
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;
}
CloseableHttpResponse getReconstructedResponse() throws IOException {
ensureConsumed();
final HttpResponse reconstructed = new BasicHttpResponse(response.getStatusLine());
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 (CloseableHttpResponse) Proxy.newProxyInstance(
ResponseProxyHandler.class.getClassLoader(),
new Class<?>[] { CloseableHttpResponse.class },
new ResponseProxyHandler(reconstructed) {
@Override
public void close() throws IOException {
response.close();
}
});
}
}

View file

@ -0,0 +1,55 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
/** Records a set of information describing a cached variant. */
class Variant {
private final String variantKey;
private final String cacheKey;
private final HttpCacheEntry entry;
public Variant(final String variantKey, final String cacheKey, final HttpCacheEntry entry) {
this.variantKey = variantKey;
this.cacheKey = cacheKey;
this.entry = entry;
}
public String getVariantKey() {
return variantKey;
}
public String getCacheKey() {
return cacheKey;
}
public HttpCacheEntry getEntry() {
return entry;
}
}

View file

@ -0,0 +1,370 @@
/*
* ====================================================================
* 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 ch.boye.httpclientandroidlib.impl.client.cache;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.client.utils.DateUtils;
/** This class provides for parsing and understanding Warning headers. As
* the Warning header can be multi-valued, but the values can contain
* separators like commas inside quoted strings, we cannot use the regular
* {@link Header#getElements()} call to access the values.
*/
class WarningValue {
private int offs;
private int init_offs;
private final String src;
private int warnCode;
private String warnAgent;
private String warnText;
private Date warnDate;
WarningValue(final String s) {
this(s, 0);
}
WarningValue(final String s, final int offs) {
this.offs = this.init_offs = offs;
this.src = s;
consumeWarnValue();
}
/** Returns an array of the parseable warning values contained
* in the given header value, which is assumed to be a
* Warning header. Improperly formatted warning values will be
* skipped, in keeping with the philosophy of "ignore what you
* cannot understand."
* @param h Warning {@link Header} to parse
* @return array of <code>WarnValue</code> objects
*/
public static WarningValue[] getWarningValues(final Header h) {
final List<WarningValue> out = new ArrayList<WarningValue>();
final String src = h.getValue();
int offs = 0;
while(offs < src.length()) {
try {
final WarningValue wv = new WarningValue(src, offs);
out.add(wv);
offs = wv.offs;
} catch (final IllegalArgumentException e) {
final int nextComma = src.indexOf(',', offs);
if (nextComma == -1) {
break;
}
offs = nextComma + 1;
}
}
final WarningValue[] wvs = {};
return out.toArray(wvs);
}
/*
* LWS = [CRLF] 1*( SP | HT )
* CRLF = CR LF
*/
protected void consumeLinearWhitespace() {
while(offs < src.length()) {
switch(src.charAt(offs)) {
case '\r':
if (offs+2 >= src.length()
|| src.charAt(offs+1) != '\n'
|| (src.charAt(offs+2) != ' '
&& src.charAt(offs+2) != '\t')) {
return;
}
offs += 2;
break;
case ' ':
case '\t':
break;
default:
return;
}
offs++;
}
}
/*
* CHAR = <any US-ASCII character (octets 0 - 127)>
*/
private boolean isChar(final char c) {
final int i = c;
return (i >= 0 && i <= 127);
}
/*
* CTL = <any US-ASCII control character
(octets 0 - 31) and DEL (127)>
*/
private boolean isControl(final char c) {
final int i = c;
return (i == 127 || (i >=0 && i <= 31));
}
/*
* separators = "(" | ")" | "<" | ">" | "@"
* | "," | ";" | ":" | "\" | <">
* | "/" | "[" | "]" | "?" | "="
* | "{" | "}" | SP | HT
*/
private boolean isSeparator(final char c) {
return (c == '(' || c == ')' || c == '<' || c == '>'
|| c == '@' || c == ',' || c == ';' || c == ':'
|| c == '\\' || c == '\"' || c == '/'
|| c == '[' || c == ']' || c == '?' || c == '='
|| c == '{' || c == '}' || c == ' ' || c == '\t');
}
/*
* token = 1*<any CHAR except CTLs or separators>
*/
protected void consumeToken() {
if (!isTokenChar(src.charAt(offs))) {
parseError();
}
while(offs < src.length()) {
if (!isTokenChar(src.charAt(offs))) {
break;
}
offs++;
}
}
private boolean isTokenChar(final char c) {
return (isChar(c) && !isControl(c) && !isSeparator(c));
}
private static final String TOPLABEL = "\\p{Alpha}([\\p{Alnum}-]*\\p{Alnum})?";
private static final String DOMAINLABEL = "\\p{Alnum}([\\p{Alnum}-]*\\p{Alnum})?";
private static final String HOSTNAME = "(" + DOMAINLABEL + "\\.)*" + TOPLABEL + "\\.?";
private static final String IPV4ADDRESS = "\\d+\\.\\d+\\.\\d+\\.\\d+";
private static final String HOST = "(" + HOSTNAME + ")|(" + IPV4ADDRESS + ")";
private static final String PORT = "\\d*";
private static final String HOSTPORT = "(" + HOST + ")(\\:" + PORT + ")?";
private static final Pattern HOSTPORT_PATTERN = Pattern.compile(HOSTPORT);
protected void consumeHostPort() {
final Matcher m = HOSTPORT_PATTERN.matcher(src.substring(offs));
if (!m.find()) {
parseError();
}
if (m.start() != 0) {
parseError();
}
offs += m.end();
}
/*
* warn-agent = ( host [ ":" port ] ) | pseudonym
* pseudonym = token
*/
protected void consumeWarnAgent() {
final int curr_offs = offs;
try {
consumeHostPort();
warnAgent = src.substring(curr_offs, offs);
consumeCharacter(' ');
return;
} catch (final IllegalArgumentException e) {
offs = curr_offs;
}
consumeToken();
warnAgent = src.substring(curr_offs, offs);
consumeCharacter(' ');
}
/*
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
* qdtext = <any TEXT except <">>
*/
protected void consumeQuotedString() {
if (src.charAt(offs) != '\"') {
parseError();
}
offs++;
boolean foundEnd = false;
while(offs < src.length() && !foundEnd) {
final char c = src.charAt(offs);
if (offs + 1 < src.length() && c == '\\'
&& isChar(src.charAt(offs+1))) {
offs += 2; // consume quoted-pair
} else if (c == '\"') {
foundEnd = true;
offs++;
} else if (c != '\"' && !isControl(c)) {
offs++;
} else {
parseError();
}
}
if (!foundEnd) {
parseError();
}
}
/*
* warn-text = quoted-string
*/
protected void consumeWarnText() {
final int curr = offs;
consumeQuotedString();
warnText = src.substring(curr, offs);
}
private static final String MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
private static final String WEEKDAY = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday";
private static final String WKDAY = "Mon|Tue|Wed|Thu|Fri|Sat|Sun";
private static final String TIME = "\\d{2}:\\d{2}:\\d{2}";
private static final String DATE3 = "(" + MONTH + ") ( |\\d)\\d";
private static final String DATE2 = "\\d{2}-(" + MONTH + ")-\\d{2}";
private static final String DATE1 = "\\d{2} (" + MONTH + ") \\d{4}";
private static final String ASCTIME_DATE = "(" + WKDAY + ") (" + DATE3 + ") (" + TIME + ") \\d{4}";
private static final String RFC850_DATE = "(" + WEEKDAY + "), (" + DATE2 + ") (" + TIME + ") GMT";
private static final String RFC1123_DATE = "(" + WKDAY + "), (" + DATE1 + ") (" + TIME + ") GMT";
private static final String HTTP_DATE = "(" + RFC1123_DATE + ")|(" + RFC850_DATE + ")|(" + ASCTIME_DATE + ")";
private static final String WARN_DATE = "\"(" + HTTP_DATE + ")\"";
private static final Pattern WARN_DATE_PATTERN = Pattern.compile(WARN_DATE);
/*
* warn-date = <"> HTTP-date <">
*/
protected void consumeWarnDate() {
final int curr = offs;
final Matcher m = WARN_DATE_PATTERN.matcher(src.substring(offs));
if (!m.lookingAt()) {
parseError();
}
offs += m.end();
warnDate = DateUtils.parseDate(src.substring(curr+1,offs-1));
}
/*
* warning-value = warn-code SP warn-agent SP warn-text [SP warn-date]
*/
protected void consumeWarnValue() {
consumeLinearWhitespace();
consumeWarnCode();
consumeWarnAgent();
consumeWarnText();
if (offs + 1 < src.length() && src.charAt(offs) == ' ' && src.charAt(offs+1) == '\"') {
consumeCharacter(' ');
consumeWarnDate();
}
consumeLinearWhitespace();
if (offs != src.length()) {
consumeCharacter(',');
}
}
protected void consumeCharacter(final char c) {
if (offs + 1 > src.length()
|| c != src.charAt(offs)) {
parseError();
}
offs++;
}
/*
* warn-code = 3DIGIT
*/
protected void consumeWarnCode() {
if (offs + 4 > src.length()
|| !Character.isDigit(src.charAt(offs))
|| !Character.isDigit(src.charAt(offs + 1))
|| !Character.isDigit(src.charAt(offs + 2))
|| src.charAt(offs + 3) != ' ') {
parseError();
}
warnCode = Integer.parseInt(src.substring(offs,offs+3));
offs += 4;
}
private void parseError() {
final String s = src.substring(init_offs);
throw new IllegalArgumentException("Bad warn code \"" + s + "\"");
}
/** Returns the 3-digit code associated with this warning.
* @return <code>int</code>
*/
public int getWarnCode() { return warnCode; }
/** Returns the "warn-agent" string associated with this warning,
* which is either the name or pseudonym of the server that added
* this particular Warning header.
* @return {@link String}
*/
public String getWarnAgent() { return warnAgent; }
/** Returns the human-readable warning text for this warning. Note
* that the original quoted-string is returned here, including
* escaping for any contained characters. In other words, if the
* header was:
* <pre>
* Warning: 110 fred "Response is stale"
* </pre>
* then this method will return <code>"\"Response is stale\""</code>
* (surrounding quotes included).
* @return {@link String}
*/
public String getWarnText() { return warnText; }
/** Returns the date and time when this warning was added, or
* <code>null</code> if a warning date was not supplied in the
* header.
* @return {@link Date}
*/
public Date getWarnDate() { return warnDate; }
/** Formats a <code>WarningValue</code> as a {@link String}
* suitable for including in a header. For example, you can:
* <pre>
* WarningValue wv = ...;
* HttpResponse resp = ...;
* resp.addHeader("Warning", wv.toString());
* </pre>
* @return {@link String}
*/
@Override
public String toString() {
if (warnDate != null) {
return String.format("%d %s %s \"%s\"", warnCode,
warnAgent, warnText, DateUtils.formatDate(warnDate));
} else {
return String.format("%d %s %s", warnCode, warnAgent, warnText);
}
}
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
====================================================================
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/>.
-->
</head>
<body bgcolor="white">
<p>
This package contains a cache module that can be used for HTTP/1.1
client-side caching. The primary classes in this package are the
{@link org.apache.http.impl.client.cache.CachingHttpClient},
which is a drop-in replacement for
a {@link org.apache.http.impl.client.DefaultHttpClient} that adds
caching, and the {@link org.apache.http.impl.client.cache.CacheConfig}
class that can be used for configuring it.
</p>
</body>
</html>