View Javadoc

1   /**
2    * This file Copyright (c) 2008-2010 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.cache.executor;
35  
36  import info.magnolia.cms.util.RequestHeaderUtil;
37  import info.magnolia.module.cache.Cache;
38  import info.magnolia.module.cache.CachePolicyResult;
39  import info.magnolia.module.cache.filter.CachedEntry;
40  import info.magnolia.module.cache.filter.CachedError;
41  import info.magnolia.module.cache.filter.CachedPage;
42  import info.magnolia.module.cache.filter.CachedRedirect;
43  import info.magnolia.voting.voters.UserAgentVoter;
44  
45  import java.io.IOException;
46  import java.util.Collection;
47  import java.util.Iterator;
48  
49  import javax.servlet.FilterChain;
50  import javax.servlet.ServletException;
51  import javax.servlet.http.HttpServletRequest;
52  import javax.servlet.http.HttpServletResponse;
53  
54  import org.apache.commons.collections.MultiMap;
55  
56  /**
57   * Serves the content from the cache.
58   *
59   * @author pbracher
60   * @version $Revision: $ ($Author: $)
61   */
62  public class UseCache extends AbstractExecutor {
63      private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UseCache.class);
64  
65      public void processCacheRequest(HttpServletRequest request,
66              HttpServletResponse response, FilterChain chain, Cache cache,
67              CachePolicyResult cachePolicy) throws IOException, ServletException {
68          CachedEntry cached = (CachedEntry) cachePolicy.getCachedEntry();
69          processCachedEntry(cached, request, response);
70      }
71  
72      protected void processCachedEntry(CachedEntry cached, HttpServletRequest request, HttpServletResponse response) throws IOException {
73          log.debug("Serving {}", cached);
74          if (cached instanceof CachedPage) {
75              final CachedPage page = (CachedPage) cached;
76              if (!ifModifiedSince(request, page.getLastModificationTime())) {
77                  if (response.isCommitted() && page.getPreCacheStatusCode() != HttpServletResponse.SC_NOT_MODIFIED) {
78                      // this should not happen ... if it does, log it and _serve_the_data_ otherwise we will confuse client
79                      log.warn("Unable to change status on already commited response {}.", response.getClass().getName());
80                  } else {
81                      // not newly cached anymore, reset the code ...
82                      page.setPreCacheStatusCode(0);
83                      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
84                      return;
85                  }
86              }
87  
88              writePage(request, response, page);
89              response.flushBuffer();
90          } else if (cached instanceof CachedError) {
91              final CachedError error = (CachedError) cached;
92              if (!response.isCommitted()) {
93                  response.sendError(error.getStatusCode());
94              } else {
95                  //this usually happens first time the error occurs and is put in cache - since setting page as error causes it to be committed
96                  // TODO: is there a better work around to make sure we do not swallow some exception accidentally?
97                  log.debug("Failed to serve cached error due to response already committed.");
98              }
99          } else if (cached instanceof CachedRedirect) {
100             final CachedRedirect redir = (CachedRedirect) cached;
101             // we'll ignore the redirection code for now - especially since the servlet api doesn't really let us choose anyway
102             // except if someone sets the header manually ?
103             if (!response.isCommitted()) {
104                 response.sendRedirect(redir.getLocation());
105             }
106         } else if (cached == null) {
107             // 304 or nothing to write to the output
108             return;
109         } else {
110             throw new IllegalStateException("Unexpected CachedEntry type: " + cached);
111         }
112     }
113 
114     protected void writePage(final HttpServletRequest request, final HttpServletResponse response, final CachedPage cachedEntry) throws IOException {
115         int vote = getCompressionVote(request, UserAgentVoter.class);
116         log.debug("On user agent {} voted {} ", request.getHeader("User-Agent"), "" + vote);
117         // write gzip header only if accepts gzip and we have compressed and uncompressed entries
118         final boolean acceptsGzipEncoding = vote == 0 && RequestHeaderUtil.acceptsGzipEncoding(request) && cachedEntry.getUngzippedContent() != null;
119         log.debug("Accepts gzip encoding: {}", "" + acceptsGzipEncoding);
120 
121         response.setStatus(cachedEntry.getStatusCode());
122         addHeaders(cachedEntry, acceptsGzipEncoding, response);
123 
124         // TODO : cookies ?
125         response.setContentType(cachedEntry.getContentType());
126         response.setCharacterEncoding(cachedEntry.getCharacterEncoding());
127         writeContent(response, cachedEntry, acceptsGzipEncoding);
128     }
129 
130     /**
131      * Set the headers in the response object
132      */
133     protected void addHeaders(final CachedPage cachedEntry, final boolean acceptsGzipEncoding, final HttpServletResponse response) {
134         final MultiMap headers = cachedEntry.getHeaders();
135 
136         final Iterator it = headers.keySet().iterator();
137         while (it.hasNext()) {
138             final String header = (String) it.next();
139             if (!acceptsGzipEncoding) {
140                 //TODO: this should not be necessary any more ...
141                 if ("Content-Encoding".equals(header) || "Vary".equals(header)) {
142                     continue;
143                 }
144             }
145             if (response.containsHeader(header)) {
146                 // do not duplicate headers. Some of the headers we have to set in Store to have them added to the cache entry, on the other hand we don't want to duplicate them if they are already set.
147                 continue;
148             }
149 
150             final Collection values = (Collection) headers.get(header);
151             final Iterator valIt = values.iterator();
152             while (valIt.hasNext()) {
153                 final Object val = valIt.next();
154                 if (val instanceof Long) {
155                     response.addDateHeader(header, ((Long) val).longValue());
156                 } else if (val instanceof Integer) {
157                     response.addIntHeader(header, ((Integer) val).intValue());
158                 } else if (val instanceof String) {
159                     response.addHeader(header, (String) val);
160                 } else {
161                     throw new IllegalStateException("Unrecognized type for header [" + header + "], value is: " + val);
162                 }
163 
164             }
165         }
166     }
167 
168     protected void writeContent(final HttpServletResponse response, final CachedPage cachedEntry, boolean acceptsGzipEncoding) throws IOException {
169         final byte[] body;
170         if (!acceptsGzipEncoding ) {
171             if (cachedEntry.getUngzippedContent() != null) {
172                 // we have both zipped and unzipped version, serve unzipped
173                 body = cachedEntry.getUngzippedContent();
174             } else {
175                 // we have only one version as the content can't be zipped or is not desirable to zip it
176                 body = cachedEntry.getDefaultContent();
177             }
178         } else {
179             // zipped is always default (when both exists)
180             body = cachedEntry.getDefaultContent();
181             // write the headers as well (if not written already)
182             if (!response.isCommitted() && !response.containsHeader("Content-Encoding")) {
183                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
184                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
185             }
186         }
187 
188         // TODO : check for empty responses
189         // (HttpServletResponse.SC_NO_CONTENT, HttpServletResponse.SC_NOT_MODIFIED, or 20bytes which is an empty gzip
190 //        if (shouldBodyBeEmpty) {
191 //            body = new byte[0];
192 //        }
193 
194         response.setContentLength(body.length);
195         response.getOutputStream().write(body);
196     }
197 
198 }