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