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.hc.client5.http.impl.cache;
28  
29  import java.time.Instant;
30  
31  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
32  import org.apache.hc.client5.http.cache.HttpCacheEntry;
33  import org.apache.hc.client5.http.cache.Resource;
34  import org.apache.hc.client5.http.cache.ResourceIOException;
35  import org.apache.hc.client5.http.utils.DateUtils;
36  import org.apache.hc.client5.http.validator.ETag;
37  import org.apache.hc.core5.http.ContentType;
38  import org.apache.hc.core5.http.Header;
39  import org.apache.hc.core5.http.HttpHeaders;
40  import org.apache.hc.core5.http.HttpRequest;
41  import org.apache.hc.core5.http.HttpResponse;
42  import org.apache.hc.core5.http.HttpStatus;
43  import org.apache.hc.core5.http.Method;
44  import org.apache.hc.core5.http.message.BasicHeader;
45  import org.apache.hc.core5.util.TimeValue;
46  
47  /**
48   * Rebuilds an {@link HttpResponse} from a {@link HttpCacheEntry}
49   */
50  class CachedHttpResponseGenerator {
51  
52      private final CacheValidityPolicy validityStrategy;
53  
54      CachedHttpResponseGenerator(final CacheValidityPolicy validityStrategy) {
55          super();
56          this.validityStrategy = validityStrategy;
57      }
58  
59      /**
60       * If it is legal to use cached content in response response to the {@link HttpRequest} then
61       * generate an {@link HttpResponse} based on {@link HttpCacheEntry}.
62       * @param request {@link HttpRequest} to generate the response for
63       * @param entry {@link HttpCacheEntry} to transform into an {@link HttpResponse}
64       * @return {@link SimpleHttpResponse} constructed response
65       */
66      SimpleHttpResponse generateResponse(final HttpRequest request, final HttpCacheEntry entry) throws ResourceIOException {
67          final Instant now = Instant.now();
68          final SimpleHttpResponse response = new SimpleHttpResponse(entry.getStatus());
69  
70          response.setHeaders(entry.getHeaders());
71  
72          if (responseShouldContainEntity(request, entry)) {
73              final Resource resource = entry.getResource();
74              final Header h = entry.getFirstHeader(HttpHeaders.CONTENT_TYPE);
75              final ContentType contentType = h != null ? ContentType.parse(h.getValue()) : null;
76              final byte[] content = resource.get();
77              generateContentLength(response, content);
78              response.setBody(content, contentType);
79          }
80  
81          final TimeValue age = this.validityStrategy.getCurrentAge(entry, now);
82          if (TimeValue.isPositive(age)) {
83              if (age.compareTo(CacheSupport.MAX_AGE) >= 0) {
84                  response.setHeader(HttpHeaders.AGE, Long.toString(CacheSupport.MAX_AGE.toSeconds()));
85              } else {
86                  response.setHeader(HttpHeaders.AGE, Long.toString(age.toSeconds()));
87              }
88          }
89  
90          return response;
91      }
92  
93      /**
94       * Generate a 304 - Not Modified response from the {@link HttpCacheEntry}. This should be
95       * used to respond to conditional requests, when the entry exists or has been re-validated.
96       */
97      SimpleHttpResponse generateNotModifiedResponse(final HttpCacheEntry entry) {
98  
99          final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
100 
101         // The response MUST include the following headers
102 
103         // - Date
104         Header dateHeader = entry.getFirstHeader(HttpHeaders.DATE);
105         if (dateHeader == null) {
106             dateHeader = new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
107         }
108         response.addHeader(dateHeader);
109 
110         // - ETag and/or Content-Location, if the header would have been sent
111         //   in a 200 response to the same request
112         final ETag eTag = entry.getETag();
113         if (eTag != null) {
114             response.addHeader(new BasicHeader(HttpHeaders.ETAG, eTag.toString()));
115         }
116 
117         final Header contentLocationHeader = entry.getFirstHeader(HttpHeaders.CONTENT_LOCATION);
118         if (contentLocationHeader != null) {
119             response.addHeader(contentLocationHeader);
120         }
121 
122         // - Expires, Cache-Control, and/or Vary, if the field-value might
123         //   differ from that sent in any previous response for the same
124         //   variant
125         final Header expiresHeader = entry.getFirstHeader(HttpHeaders.EXPIRES);
126         if (expiresHeader != null) {
127             response.addHeader(expiresHeader);
128         }
129 
130         final Header cacheControlHeader = entry.getFirstHeader(HttpHeaders.CACHE_CONTROL);
131         if (cacheControlHeader != null) {
132             response.addHeader(cacheControlHeader);
133         }
134 
135         final Header varyHeader = entry.getFirstHeader(HttpHeaders.VARY);
136         if (varyHeader != null) {
137             response.addHeader(varyHeader);
138         }
139 
140         //Since the goal of a 304 response is to minimize information transfer
141         //when the recipient already has one or more cached representations, a
142         //sender SHOULD NOT generate representation metadata other than the
143         //above listed fields unless said metadata exists for the purpose of
144         //guiding cache updates (e.g., Last-Modified might be useful if the
145         //response does not have an ETag field).
146         if (eTag == null) {
147             final Header lastModifiedHeader = entry.getFirstHeader(HttpHeaders.LAST_MODIFIED);
148             if (lastModifiedHeader != null) {
149                 response.addHeader(lastModifiedHeader);
150             }
151         }
152         return response;
153     }
154 
155     private void generateContentLength(final HttpResponse response, final byte[] body) {
156         response.removeHeaders(HttpHeaders.TRANSFER_ENCODING);
157         response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(body.length));
158     }
159 
160     private boolean responseShouldContainEntity(final HttpRequest request, final HttpCacheEntry cacheEntry) {
161         return Method.GET.isSame(request.getMethod()) && cacheEntry.getResource() != null;
162     }
163 
164 }