View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
3    * use this file except in compliance with the License. You may obtain a copy of
4    * the License at
5    * 
6    * http://www.apache.org/licenses/LICENSE-2.0
7    * 
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11   * License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  
15  package org.esigate.cache;
16  
17  import java.io.IOException;
18  import java.util.Arrays;
19  import java.util.Date;
20  import java.util.Properties;
21  
22  import org.apache.http.HttpException;
23  import org.apache.http.HttpStatus;
24  import org.apache.http.client.cache.CacheResponseStatus;
25  import org.apache.http.client.cache.HttpCacheContext;
26  import org.apache.http.client.methods.CloseableHttpResponse;
27  import org.apache.http.client.methods.HttpExecutionAware;
28  import org.apache.http.client.methods.HttpRequestWrapper;
29  import org.apache.http.client.protocol.HttpClientContext;
30  import org.apache.http.conn.routing.HttpRoute;
31  import org.apache.http.impl.execchain.ClientExecChain;
32  import org.esigate.ConfigurationException;
33  import org.esigate.Parameters;
34  import org.esigate.http.DateUtils;
35  import org.esigate.http.OutgoingRequestContext;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * This class is changes the behavior of the HttpCache by transforming the headers in the requests or response.
41   * 
42   * @author Francois-Xavier Bonnet
43   * 
44   */
45  public class CacheAdapter {
46      private static final Logger LOG = LoggerFactory.getLogger(CacheAdapter.class);
47      private int staleIfError;
48      private int staleWhileRevalidate;
49      private int ttl;
50      private boolean xCacheHeader;
51      private boolean viaHeader;
52  
53      /**
54       * Inititalize the instance.
55       * 
56       * @param properties
57       *            properties
58       */
59      public void init(Properties properties) {
60          staleIfError = Parameters.STALE_IF_ERROR.getValue(properties);
61          staleWhileRevalidate = Parameters.STALE_WHILE_REVALIDATE.getValue(properties);
62          int maxAsynchronousWorkers = Parameters.MAX_ASYNCHRONOUS_WORKERS.getValue(properties);
63          if (staleWhileRevalidate > 0 && maxAsynchronousWorkers == 0) {
64              throw new ConfigurationException("You must set a positive value for maxAsynchronousWorkers "
65                      + "in order to enable background revalidation (staleWhileRevalidate)");
66          }
67          ttl = Parameters.TTL.getValue(properties);
68          xCacheHeader = Parameters.X_CACHE_HEADER.getValue(properties);
69          viaHeader = Parameters.VIA_HEADER.getValue(properties);
70          LOG.info("Initializing cache for provider " + Arrays.toString(Parameters.REMOTE_URL_BASE.getValue(properties))
71                  + " staleIfError=" + staleIfError + " staleWhileRevalidate=" + staleWhileRevalidate + " ttl=" + ttl
72                  + " xCacheHeader=" + xCacheHeader + " viaHeader=" + viaHeader);
73      }
74  
75      public ClientExecChain wrapCachingHttpClient(final ClientExecChain wrapped) {
76          return new ClientExecChain() {
77  
78              /**
79               * Removes client http cache directives like "Cache-control" and "Pragma". Users must not be able to bypass
80               * the cache just by making a refresh in the browser. Generates X-cache header.
81               * 
82               */
83              @Override
84              public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
85                      HttpClientContext httpClientContext, HttpExecutionAware execAware) throws IOException,
86                      HttpException {
87                  OutgoingRequestContext context = OutgoingRequestContext.adapt(httpClientContext);
88  
89                  // Switch route for the cache to generate the right cache key
90                  CloseableHttpResponse response = wrapped.execute(route, request, context, execAware);
91  
92                  // Remove previously added Cache-control header
93                  if (request.getRequestLine().getMethod().equalsIgnoreCase("GET")
94                          && (staleWhileRevalidate > 0 || staleIfError > 0)) {
95                      response.removeHeader(response.getLastHeader("Cache-control"));
96                  }
97                  // Add X-cache header
98                  if (xCacheHeader) {
99                      if (context != null) {
100                         CacheResponseStatus cacheResponseStatus =
101                                 (CacheResponseStatus) context.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
102                         String xCacheString;
103                         if (cacheResponseStatus.equals(CacheResponseStatus.CACHE_HIT)) {
104                             xCacheString = "HIT";
105                         } else if (cacheResponseStatus.equals(CacheResponseStatus.VALIDATED)) {
106                             xCacheString = "VALIDATED";
107                         } else {
108                             xCacheString = "MISS";
109                         }
110                         xCacheString += " from " + route.getTargetHost().toHostString();
111                         xCacheString +=
112                                 " (" + request.getRequestLine().getMethod() + " " + request.getRequestLine().getUri()
113                                         + ")";
114                         response.addHeader("X-Cache", xCacheString);
115                     }
116                 }
117 
118                 // Remove Via header
119                 if (!viaHeader && response.containsHeader("Via")) {
120                     response.removeHeaders("Via");
121                 }
122                 return response;
123             }
124         };
125     }
126 
127     public ClientExecChain wrapBackendHttpClient(final ClientExecChain wrapped) {
128         return new ClientExecChain() {
129 
130             private boolean isCacheableStatus(int statusCode) {
131                 return (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MOVED_PERMANENTLY
132                         || statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_NOT_FOUND
133                         || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
134                         || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE || statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT);
135             }
136 
137             /**
138              * Fire pre-fetch and post-fetch events Enables cache for all GET requests if cache ttl was forced to a
139              * certain duration in the configuration. This is done even for non 200 return codes! This is a very
140              * aggressive but efficient caching policy. Adds "stale-while-revalidate" and "stale-if-error" cache-control
141              * directives depending on the configuration.
142              * 
143              * @throws HttpException
144              * @throws IOException
145              */
146             @Override
147             public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
148                     HttpClientContext httpClientContext, HttpExecutionAware execAware) throws IOException,
149                     HttpException {
150                 OutgoingRequestContext context = OutgoingRequestContext.adapt(httpClientContext);
151 
152                 CloseableHttpResponse response = wrapped.execute(route, request, context, execAware);
153 
154                 String method = request.getRequestLine().getMethod();
155                 int statusCode = response.getStatusLine().getStatusCode();
156 
157                 // If ttl is set, force caching even for error pages
158                 if (ttl > 0 && method.equalsIgnoreCase("GET") && isCacheableStatus(statusCode)) {
159                     response.removeHeaders("Date");
160                     response.removeHeaders("Cache-control");
161                     response.removeHeaders("Expires");
162                     response.setHeader("Date", DateUtils.formatDate(new Date(System.currentTimeMillis())));
163                     response.setHeader("Cache-control", "public, max-age=" + ttl);
164                     response.setHeader("Expires",
165                             DateUtils.formatDate(new Date(System.currentTimeMillis() + ((long) ttl) * 1000)));
166                 }
167                 if (request.getRequestLine().getMethod().equalsIgnoreCase("GET")) {
168                     String cacheControlHeader = "";
169                     if (staleWhileRevalidate > 0) {
170                         cacheControlHeader += "stale-while-revalidate=" + staleWhileRevalidate;
171                     }
172                     if (staleIfError > 0) {
173                         if (cacheControlHeader.length() > 0) {
174                             cacheControlHeader += ",";
175                         }
176                         cacheControlHeader += "stale-if-error=" + staleIfError;
177                     }
178                     if (cacheControlHeader.length() > 0) {
179                         response.addHeader("Cache-control", cacheControlHeader);
180                     }
181                 }
182 
183                 return response;
184             }
185 
186         };
187     }
188 }