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.HeaderConstants;
33  import org.apache.hc.client5.http.cache.HttpCacheEntry;
34  import org.apache.hc.client5.http.cache.Resource;
35  import org.apache.hc.client5.http.cache.ResourceIOException;
36  import org.apache.hc.client5.http.utils.DateUtils;
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.HttpVersion;
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          response.setVersion(HttpVersion.DEFAULT);
70  
71          response.setHeaders(entry.getHeaders());
72  
73          if (responseShouldContainEntity(request, entry)) {
74              final Resource resource = entry.getResource();
75              final Header h = entry.getFirstHeader(HttpHeaders.CONTENT_TYPE);
76              final ContentType contentType = h != null ? ContentType.parse(h.getValue()) : null;
77              final byte[] content = resource.get();
78              addMissingContentLengthHeader(response, content);
79              response.setBody(content, contentType);
80          }
81  
82          final TimeValue age = this.validityStrategy.getCurrentAge(entry, now);
83          if (TimeValue.isPositive(age)) {
84              if (age.compareTo(CacheValidityPolicy.MAX_AGE) >= 0) {
85                  response.setHeader(HeaderConstants.AGE, "" + CacheValidityPolicy.MAX_AGE.toSeconds());
86              } else {
87                  response.setHeader(HeaderConstants.AGE, "" + age.toSeconds());
88              }
89          }
90  
91          return response;
92      }
93  
94      /**
95       * Generate a 304 - Not Modified response from the {@link HttpCacheEntry}. This should be
96       * used to respond to conditional requests, when the entry exists or has been re-validated.
97       */
98      SimpleHttpResponse generateNotModifiedResponse(final HttpCacheEntry entry) {
99  
100         final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
101 
102         // The response MUST include the following headers
103         //  (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
104 
105         // - Date, unless its omission is required by section 14.8.1
106         Header dateHeader = entry.getFirstHeader(HttpHeaders.DATE);
107         if (dateHeader == null) {
108             dateHeader = new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
109         }
110         response.addHeader(dateHeader);
111 
112         // - ETag and/or Content-Location, if the header would have been sent
113         //   in a 200 response to the same request
114         final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
115         if (etagHeader != null) {
116             response.addHeader(etagHeader);
117         }
118 
119         final Header contentLocationHeader = entry.getFirstHeader(HttpHeaders.CONTENT_LOCATION);
120         if (contentLocationHeader != null) {
121             response.addHeader(contentLocationHeader);
122         }
123 
124         // - Expires, Cache-Control, and/or Vary, if the field-value might
125         //   differ from that sent in any previous response for the same
126         //   variant
127         final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
128         if (expiresHeader != null) {
129             response.addHeader(expiresHeader);
130         }
131 
132         final Header cacheControlHeader = entry.getFirstHeader(HeaderConstants.CACHE_CONTROL);
133         if (cacheControlHeader != null) {
134             response.addHeader(cacheControlHeader);
135         }
136 
137         final Header varyHeader = entry.getFirstHeader(HeaderConstants.VARY);
138         if (varyHeader != null) {
139             response.addHeader(varyHeader);
140         }
141 
142         return response;
143     }
144 
145     private void addMissingContentLengthHeader(final HttpResponse response, final byte[] body) {
146         if (transferEncodingIsPresent(response)) {
147             return;
148         }
149         // Some well known proxies respond with Content-Length=0, when returning 304. For robustness, always
150         // use the cached entity's content length, as modern browsers do.
151         response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(body.length));
152     }
153 
154     private boolean transferEncodingIsPresent(final HttpResponse response) {
155         final Header hdr = response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING);
156         return hdr != null;
157     }
158 
159     private boolean responseShouldContainEntity(final HttpRequest request, final HttpCacheEntry cacheEntry) {
160         return request.getMethod().equals(HeaderConstants.GET_METHOD) && cacheEntry.getResource() != null;
161     }
162 
163     /**
164      * Extract error information about the {@link HttpRequest} telling the 'caller'
165      * that a problem occurred.
166      *
167      * @param errorCheck What type of error should I get
168      * @return The {@link HttpResponse} that is the error generated
169      */
170     public SimpleHttpResponse getErrorForRequest(final RequestProtocolError errorCheck) {
171         switch (errorCheck) {
172             case BODY_BUT_NO_LENGTH_ERROR:
173                 return SimpleHttpResponse.create(HttpStatus.SC_LENGTH_REQUIRED);
174 
175             case WEAK_ETAG_AND_RANGE_ERROR:
176                 return SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST,
177                         "Weak eTag not compatible with byte range", ContentType.DEFAULT_TEXT);
178 
179             case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR:
180                 return SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST,
181                         "Weak eTag not compatible with PUT or DELETE requests");
182 
183             case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME:
184                 return SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST,
185                         "No-Cache directive MUST NOT include a field name");
186 
187             default:
188                 throw new IllegalStateException(
189                         "The request was compliant, therefore no error can be generated for it.");
190 
191         }
192     }
193 
194 }