com.koushikdutta.async.http.cache.ResponseHeaders Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of androidasync Show documentation
Show all versions of androidasync Show documentation
Asynchronous socket, http(s) (client+server) and websocket library for android. Based on nio, not threads.
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.koushikdutta.async.http.cache;
import android.net.Uri;
import com.koushikdutta.async.http.HttpDate;
import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
/**
* Parsed HTTP response headers.
*/
final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
/** HTTP header name for the local time when the response was received. */
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
private final Uri uri;
private final RawHeaders headers;
/** The server's time when this response was served, if known. */
private Date servedDate;
/** The last modified date of the response, if known. */
private Date lastModified;
/**
* The expiration date of the response, if known. If both this field and the
* max age are set, the max age is preferred.
*/
private Date expires;
/**
* Extension header set by HttpURLConnectionImpl specifying the timestamp
* when the HTTP request was first initiated.
*/
private long sentRequestMillis;
/**
* Extension header set by HttpURLConnectionImpl specifying the timestamp
* when the HTTP response was first received.
*/
private long receivedResponseMillis;
/**
* In the response, this field's name "no-cache" is misleading. It doesn't
* prevent us from caching the response; it only means we have to validate
* the response with the origin server before returning it. We can do this
* with a conditional get.
*/
private boolean noCache;
/** If true, this response should not be cached. */
private boolean noStore;
/**
* The duration past the response's served date that it can be served
* without validation.
*/
private int maxAgeSeconds = -1;
/**
* The "s-maxage" directive is the max age for shared caches. Not to be
* confused with "max-age" for non-shared caches, As in Firefox and Chrome,
* this directive is not honored by this cache.
*/
private int sMaxAgeSeconds = -1;
/**
* This request header field's name "only-if-cached" is misleading. It
* actually means "do not use the network". It is set by a client who only
* wants to make a request if it can be fully satisfied by the cache.
* Cached responses that would require validation (ie. conditional gets) are
* not permitted if this header is set.
*/
private boolean isPublic;
private boolean mustRevalidate;
private String etag;
private int ageSeconds = -1;
/** Case-insensitive set of field names. */
private Set varyFields = Collections.emptySet();
private String contentEncoding;
private String transferEncoding;
private long contentLength = -1;
private String connection;
private String proxyAuthenticate;
private String wwwAuthenticate;
public ResponseHeaders(Uri uri, RawHeaders headers) {
this.uri = uri;
this.headers = headers;
HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
@Override public void handle(String directive, String parameter) {
if (directive.equalsIgnoreCase("no-cache")) {
noCache = true;
} else if (directive.equalsIgnoreCase("no-store")) {
noStore = true;
} else if (directive.equalsIgnoreCase("max-age")) {
maxAgeSeconds = HeaderParser.parseSeconds(parameter);
} else if (directive.equalsIgnoreCase("s-maxage")) {
sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
} else if (directive.equalsIgnoreCase("public")) {
isPublic = true;
} else if (directive.equalsIgnoreCase("must-revalidate")) {
mustRevalidate = true;
}
}
};
for (int i = 0; i < headers.length(); i++) {
String fieldName = headers.getFieldName(i);
String value = headers.getValue(i);
if ("Cache-Control".equalsIgnoreCase(fieldName)) {
HeaderParser.parseCacheControl(value, handler);
} else if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Pragma".equalsIgnoreCase(fieldName)) {
if (value.equalsIgnoreCase("no-cache")) {
noCache = true;
}
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HeaderParser.parseSeconds(value);
} else if ("Vary".equalsIgnoreCase(fieldName)) {
// Replace the immutable empty set with something we can mutate.
if (varyFields.isEmpty()) {
varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER);
}
for (String varyField : value.split(",")) {
varyFields.add(varyField.trim().toLowerCase());
}
} else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
contentEncoding = value;
} else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
transferEncoding = value;
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
try {
contentLength = Long.parseLong(value);
} catch (NumberFormatException ignored) {
}
} else if ("Connection".equalsIgnoreCase(fieldName)) {
connection = value;
} else if ("Proxy-Authenticate".equalsIgnoreCase(fieldName)) {
proxyAuthenticate = value;
} else if ("WWW-Authenticate".equalsIgnoreCase(fieldName)) {
wwwAuthenticate = value;
} else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
receivedResponseMillis = Long.parseLong(value);
}
}
}
public boolean isContentEncodingGzip() {
return "gzip".equalsIgnoreCase(contentEncoding);
}
public void stripContentEncoding() {
contentEncoding = null;
headers.removeAll("Content-Encoding");
}
public boolean isChunked() {
return "chunked".equalsIgnoreCase(transferEncoding);
}
public boolean hasConnectionClose() {
return "close".equalsIgnoreCase(connection);
}
public Uri getUri() {
return uri;
}
public RawHeaders getHeaders() {
return headers;
}
public Date getServedDate() {
return servedDate;
}
public Date getLastModified() {
return lastModified;
}
public Date getExpires() {
return expires;
}
public boolean isNoCache() {
return noCache;
}
public boolean isNoStore() {
return noStore;
}
public int getMaxAgeSeconds() {
return maxAgeSeconds;
}
public int getSMaxAgeSeconds() {
return sMaxAgeSeconds;
}
public boolean isPublic() {
return isPublic;
}
public boolean isMustRevalidate() {
return mustRevalidate;
}
public String getEtag() {
return etag;
}
public Set getVaryFields() {
return varyFields;
}
public String getContentEncoding() {
return contentEncoding;
}
public long getContentLength() {
return contentLength;
}
public String getConnection() {
return connection;
}
public String getProxyAuthenticate() {
return proxyAuthenticate;
}
public String getWwwAuthenticate() {
return wwwAuthenticate;
}
public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
this.sentRequestMillis = sentRequestMillis;
headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
this.receivedResponseMillis = receivedResponseMillis;
headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
}
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.
*/
private long computeAge(long nowMillis) {
long apparentReceivedAge = servedDate != null
? Math.max(0, receivedResponseMillis - servedDate.getTime())
: 0;
long receivedAge = ageSeconds != -1
? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
long responseDuration = receivedResponseMillis - sentRequestMillis;
long residentDuration = nowMillis - receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
/**
* Returns the number of milliseconds that the response was fresh for,
* starting from the served date.
*/
private long computeFreshnessLifetime() {
if (maxAgeSeconds != -1) {
return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
} else if (expires != null) {
long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null && uri.getEncodedQuery() == null) {
/*
* As recommended by the HTTP RFC and implemented in Firefox, the
* max age of a document should be defaulted to 10% of the
* document's age at the time it was served. Default expiration
* dates aren't used for URIs containing a query.
*/
long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a
* heuristic to serve a cached response older than 24 hours, we are required
* to attach a warning.
*/
private boolean isFreshnessLifetimeHeuristic() {
return maxAgeSeconds == -1 && expires == null;
}
/**
* Returns true if this response can be stored to later serve another
* request.
*/
public boolean isCacheable(RequestHeaders request) {
/*
* Always go to network for uncacheable response codes (RFC 2616, 13.4),
* This implementation doesn't support caching partial content.
*/
int responseCode = headers.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK
&& responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
&& responseCode != HttpURLConnection.HTTP_MULT_CHOICE
&& responseCode != HttpURLConnection.HTTP_MOVED_PERM
&& responseCode != HttpURLConnection.HTTP_GONE) {
return false;
}
/*
* Responses to authorized requests aren't cacheable unless they include
* a 'public', 'must-revalidate' or 's-maxage' directive.
*/
if (request.hasAuthorization()
&& !isPublic
&& !mustRevalidate
&& sMaxAgeSeconds == -1) {
return false;
}
if (noStore) {
return false;
}
return true;
}
/**
* Returns true if a Vary header contains an asterisk. Such responses cannot
* be cached.
*/
public boolean hasVaryAll() {
return varyFields.contains("*");
}
/**
* Returns true if none of the Vary headers on this response have changed
* between {@code cachedRequest} and {@code newRequest}.
*/
public boolean varyMatches(Map> cachedRequest,
Map> newRequest) {
for (String field : varyFields) {
if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {
return false;
}
}
return true;
}
/**
* Returns the source to satisfy {@code request} given this cached response.
*/
public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
/*
* If this response shouldn't have been stored, it should never be used
* as a response source. This check should be redundant as long as the
* persistence store is well-behaved and the rules are constant.
*/
if (!isCacheable(request)) {
return ResponseSource.NETWORK;
}
if (request.isNoCache() || request.hasConditions()) {
return ResponseSource.NETWORK;
}
long ageMillis = computeAge(nowMillis);
long freshMillis = computeFreshnessLifetime();
if (request.getMaxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis,
TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
}
long minFreshMillis = 0;
if (request.getMinFreshSeconds() != -1) {
minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
}
long maxStaleMillis = 0;
if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
}
if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
if (ageMillis + minFreshMillis >= freshMillis) {
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
}
/*
* not available in API 8
if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
*/
if (ageMillis > 24L * 60L * 60L * 1000L && isFreshnessLifetimeHeuristic()) {
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return ResponseSource.CACHE;
}
if (etag != null) {
request.setIfNoneMatch(etag);
}
else if (lastModified != null) {
request.setIfModifiedSince(lastModified);
} else if (servedDate != null) {
request.setIfModifiedSince(servedDate);
}
return request.hasConditions()
? ResponseSource.CONDITIONAL_CACHE
: ResponseSource.NETWORK;
}
/**
* Returns true if this cached response should be used; false if the
* network response should be used.
*/
public boolean validate(ResponseHeaders networkResponse) {
if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
return true;
}
/*
* The HTTP spec says that if the network's response is older than our
* cached response, we may return the cache's response. Like Chrome (but
* unlike Firefox), this client prefers to return the newer response.
*/
if (lastModified != null
&& networkResponse.lastModified != null
&& networkResponse.lastModified.getTime() < lastModified.getTime()) {
return true;
}
return false;
}
/**
* Combines this cached header with a network header as defined by RFC 2616,
* 13.5.3.
*/
public ResponseHeaders combine(ResponseHeaders network) {
RawHeaders result = new RawHeaders();
for (int i = 0; i < headers.length(); i++) {
String fieldName = headers.getFieldName(i);
String value = headers.getValue(i);
if (fieldName.equals("Warning") && value.startsWith("1")) {
continue; // drop 100-level freshness warnings
}
if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
result.add(fieldName, value);
}
}
for (int i = 0; i < network.headers.length(); i++) {
String fieldName = network.headers.getFieldName(i);
if (isEndToEnd(fieldName)) {
result.add(fieldName, network.headers.getValue(i));
}
}
return new ResponseHeaders(uri, result);
}
/**
* Returns true if {@code fieldName} is an end-to-end HTTP header, as
* defined by RFC 2616, 13.5.1.
*/
private static boolean isEndToEnd(String fieldName) {
return !fieldName.equalsIgnoreCase("Connection")
&& !fieldName.equalsIgnoreCase("Keep-Alive")
&& !fieldName.equalsIgnoreCase("Proxy-Authenticate")
&& !fieldName.equalsIgnoreCase("Proxy-Authorization")
&& !fieldName.equalsIgnoreCase("TE")
&& !fieldName.equalsIgnoreCase("Trailers")
&& !fieldName.equalsIgnoreCase("Transfer-Encoding")
&& !fieldName.equalsIgnoreCase("Upgrade");
}
}