View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.http.impl.client.cache;
28  
29  import java.util.Arrays;
30  import java.util.Date;
31  import java.util.HashSet;
32  import java.util.Set;
33  
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.apache.http.Header;
37  import org.apache.http.HeaderElement;
38  import org.apache.http.HttpMessage;
39  import org.apache.http.HttpRequest;
40  import org.apache.http.HttpResponse;
41  import org.apache.http.HttpStatus;
42  import org.apache.http.HttpVersion;
43  import org.apache.http.annotation.Contract;
44  import org.apache.http.annotation.ThreadingBehavior;
45  import org.apache.http.client.cache.HeaderConstants;
46  import org.apache.http.client.utils.DateUtils;
47  import org.apache.http.protocol.HTTP;
48  
49  /**
50   * Determines if an HttpResponse can be cached.
51   *
52   * @since 4.1
53   */
54  @Contract(threading = ThreadingBehavior.IMMUTABLE)
55  class ResponseCachingPolicy {
56  
57      private static final String[] AUTH_CACHEABLE_PARAMS = {
58              "s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC
59      };
60      private final long maxObjectSizeBytes;
61      private final boolean sharedCache;
62      private final boolean neverCache1_0ResponsesWithQueryString;
63      private final Log log = LogFactory.getLog(getClass());
64      private static final Set<Integer> cacheableStatuses =
65          new HashSet<Integer>(Arrays.asList(HttpStatus.SC_OK,
66                  HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
67                  HttpStatus.SC_MULTIPLE_CHOICES,
68                  HttpStatus.SC_MOVED_PERMANENTLY,
69                  HttpStatus.SC_GONE));
70      private final Set<Integer> uncacheableStatuses;
71  
72      /**
73       * Define a cache policy that limits the size of things that should be stored
74       * in the cache to a maximum of {@link HttpResponse} bytes in size.
75       *
76       * @param maxObjectSizeBytes the size to limit items into the cache
77       * @param sharedCache whether to behave as a shared cache (true) or a
78       * non-shared/private cache (false)
79       * @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
80       * to cache if explicit cache headers are found.
81       * @param allow303Caching if this policy is permitted to cache 303 response
82       */
83      public ResponseCachingPolicy(final long maxObjectSizeBytes,
84              final boolean sharedCache,
85              final boolean neverCache1_0ResponsesWithQueryString,
86              final boolean allow303Caching) {
87          this.maxObjectSizeBytes = maxObjectSizeBytes;
88          this.sharedCache = sharedCache;
89          this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
90          if (allow303Caching) {
91              uncacheableStatuses = new HashSet<Integer>(
92                      Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT));
93          } else {
94              uncacheableStatuses = new HashSet<Integer>(Arrays.asList(
95                      HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
96          }
97      }
98  
99      /**
100      * Determines if an HttpResponse can be cached.
101      *
102      * @param httpMethod What type of request was this, a GET, PUT, other?
103      * @param response The origin response
104      * @return {@code true} if response is cacheable
105      */
106     public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
107         boolean cacheable = false;
108 
109         if (!(HeaderConstants.GET_METHOD.equals(httpMethod) ||
110                 HeaderConstants.HEAD_METHOD.equals(httpMethod))) {
111             log.debug("Response was not cacheable.");
112             return false;
113         }
114 
115         final int status = response.getStatusLine().getStatusCode();
116         if (cacheableStatuses.contains(status)) {
117             // these response codes MAY be cached
118             cacheable = true;
119         } else if (uncacheableStatuses.contains(status)) {
120             return false;
121         } else if (unknownStatusCode(status)) {
122             // a response with an unknown status code MUST NOT be
123             // cached
124             return false;
125         }
126 
127         final Header contentLength = response.getFirstHeader(HTTP.CONTENT_LEN);
128         if (contentLength != null) {
129             final long contentLengthValue = Long.parseLong(contentLength.getValue());
130             if (contentLengthValue > this.maxObjectSizeBytes) {
131                 return false;
132             }
133         }
134 
135         final Header[] ageHeaders = response.getHeaders(HeaderConstants.AGE);
136 
137         if (ageHeaders.length > 1) {
138             return false;
139         }
140 
141         final Header[] expiresHeaders = response.getHeaders(HeaderConstants.EXPIRES);
142 
143         if (expiresHeaders.length > 1) {
144             return false;
145         }
146 
147         final Header[] dateHeaders = response.getHeaders(HTTP.DATE_HEADER);
148 
149         if (dateHeaders.length != 1) {
150             return false;
151         }
152 
153         final Date date = DateUtils.parseDate(dateHeaders[0].getValue());
154         if (date == null) {
155             return false;
156         }
157 
158         for (final Header varyHdr : response.getHeaders(HeaderConstants.VARY)) {
159             for (final HeaderElement elem : varyHdr.getElements()) {
160                 if ("*".equals(elem.getName())) {
161                     return false;
162                 }
163             }
164         }
165 
166         if (isExplicitlyNonCacheable(response)) {
167             return false;
168         }
169 
170         return (cacheable || isExplicitlyCacheable(response));
171     }
172 
173     private boolean unknownStatusCode(final int status) {
174         if (status >= 100 && status <= 101) {
175             return false;
176         }
177         if (status >= 200 && status <= 206) {
178             return false;
179         }
180         if (status >= 300 && status <= 307) {
181             return false;
182         }
183         if (status >= 400 && status <= 417) {
184             return false;
185         }
186         if (status >= 500 && status <= 505) {
187             return false;
188         }
189         return true;
190     }
191 
192     protected boolean isExplicitlyNonCacheable(final HttpResponse response) {
193         final Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL);
194         for (final Header header : cacheControlHeaders) {
195             for (final HeaderElement elem : header.getElements()) {
196                 if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName())
197                         || HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName())
198                         || (sharedCache && HeaderConstants.PRIVATE.equals(elem.getName()))) {
199                     return true;
200                 }
201             }
202         }
203         return false;
204     }
205 
206     protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
207         final Header[] cacheControlHeaders = msg.getHeaders(HeaderConstants.CACHE_CONTROL);
208         for (final Header header : cacheControlHeaders) {
209             for (final HeaderElement elem : header.getElements()) {
210                 for (final String param : params) {
211                     if (param.equalsIgnoreCase(elem.getName())) {
212                         return true;
213                     }
214                 }
215             }
216         }
217         return false;
218     }
219 
220     protected boolean isExplicitlyCacheable(final HttpResponse response) {
221         if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) {
222             return true;
223         }
224         final String[] cacheableParams = { HeaderConstants.CACHE_CONTROL_MAX_AGE, "s-maxage",
225                 HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE,
226                 HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE,
227                 HeaderConstants.PUBLIC
228         };
229         return hasCacheControlParameterFrom(response, cacheableParams);
230     }
231 
232     /**
233      * Determine if the {@link HttpResponse} gotten from the origin is a
234      * cacheable response.
235      *
236      * @param request the {@link HttpRequest} that generated an origin hit
237      * @param response the {@link HttpResponse} from the origin
238      * @return {@code true} if response is cacheable
239      */
240     public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
241         if (requestProtocolGreaterThanAccepted(request)) {
242             log.debug("Response was not cacheable.");
243             return false;
244         }
245 
246         final String[] uncacheableRequestDirectives = { HeaderConstants.CACHE_CONTROL_NO_STORE };
247         if (hasCacheControlParameterFrom(request,uncacheableRequestDirectives)) {
248             return false;
249         }
250 
251         if (request.getRequestLine().getUri().contains("?")) {
252             if (neverCache1_0ResponsesWithQueryString && from1_0Origin(response)) {
253                 log.debug("Response was not cacheable as it had a query string.");
254                 return false;
255             } else if (!isExplicitlyCacheable(response)) {
256                 log.debug("Response was not cacheable as it is missing explicit caching headers.");
257                 return false;
258             }
259         }
260 
261         if (expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(response)) {
262             return false;
263         }
264 
265         if (sharedCache) {
266             final Header[] authNHeaders = request.getHeaders(HeaderConstants.AUTHORIZATION);
267             if (authNHeaders != null && authNHeaders.length > 0
268                     && !hasCacheControlParameterFrom(response, AUTH_CACHEABLE_PARAMS)) {
269                 return false;
270             }
271         }
272 
273         final String method = request.getRequestLine().getMethod();
274         return isResponseCacheable(method, response);
275     }
276 
277     private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(
278             final HttpResponse response) {
279         if (response.getFirstHeader(HeaderConstants.CACHE_CONTROL) != null) {
280             return false;
281         }
282         final Header expiresHdr = response.getFirstHeader(HeaderConstants.EXPIRES);
283         final Header dateHdr = response.getFirstHeader(HTTP.DATE_HEADER);
284         if (expiresHdr == null || dateHdr == null) {
285             return false;
286         }
287         final Date expires = DateUtils.parseDate(expiresHdr.getValue());
288         final Date date = DateUtils.parseDate(dateHdr.getValue());
289         if (expires == null || date == null) {
290             return false;
291         }
292         return expires.equals(date) || expires.before(date);
293     }
294 
295     private boolean from1_0Origin(final HttpResponse response) {
296         final Header via = response.getFirstHeader(HeaderConstants.VIA);
297         if (via != null) {
298             for(final HeaderElement elt : via.getElements()) {
299                 final String proto = elt.toString().split("\\s")[0];
300                 if (proto.contains("/")) {
301                     return proto.equals("HTTP/1.0");
302                 } else {
303                     return proto.equals("1.0");
304                 }
305             }
306         }
307         return HttpVersion.HTTP_1_0.equals(response.getProtocolVersion());
308     }
309 
310     private boolean requestProtocolGreaterThanAccepted(final HttpRequest req) {
311         return req.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) > 0;
312     }
313 
314 }