View Javadoc

1   /**
2    * This file Copyright (c) 2008-2011 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.filter;
35  
36  import info.magnolia.cms.util.RequestHeaderUtil;
37  import info.magnolia.module.cache.util.GZipUtil;
38  
39  import java.io.IOException;
40  import java.io.ObjectInputStream;
41  import java.io.ObjectOutputStream;
42  import java.io.Serializable;
43  import java.util.ArrayList;
44  import java.util.Collection;
45  import java.util.HashMap;
46  import java.util.Iterator;
47  import java.util.Map;
48  import java.util.Map.Entry;
49  
50  import javax.servlet.FilterChain;
51  import javax.servlet.ServletException;
52  import javax.servlet.http.HttpServletRequest;
53  import javax.servlet.http.HttpServletResponse;
54  
55  import org.apache.commons.collections.MultiMap;
56  import org.apache.commons.collections.map.MultiValueMap;
57  import org.apache.commons.lang.builder.ToStringBuilder;
58  import org.apache.commons.lang.builder.ToStringStyle;
59  
60  /**
61   * Wraps a page response. It is assumed that the given content is gzipped
62   * if appropriate (i.e if the gzip filter is in the chain) and this class
63   * thus ungzips it to be able to serve both contents.
64   *
65   * @author gjoseph
66   * @version $Revision: $ ($Author: $)
67   */
68  public abstract class ContentCachedEntry implements CachedEntry, Serializable {
69  
70      private static final ToStringStyle BYTE_ARRAY_SIZE_STYLE = new ToStringStyle() {
71          protected void appendDetail(StringBuffer buffer, String fieldName,
72                  byte[] array) {
73              super.appendDetail(buffer, fieldName, array.length + " bytes");
74          }
75      };
76  
77      private final String contentType;
78      private final String characterEncoding;
79      private final int statusCode;
80      private transient MultiMap headers;
81      private Map serializableHeadersBackingList;
82      private final long lastModificationTime;
83      private String originalUrl;
84  
85      /**
86       * @param out Cached content.
87       * @param contentType MIME type of the cached content.
88       * @param characterEncoding Character encoding of the cached content.
89       * @param statusCode HTTP response status code (E.g. 200 - OK);
90       * @param headers Additional HTTP headers to be sent when serving this cached content.
91       * @param modificationDate Content modification date to set in the response.
92       * @param shouldCompress Flag marking this content as desirable to be sent in compressed form (should the client support such compression). Setting this to true means cache entry will contain both, compressed and flat version of the content. Compression is applied here only if content is not gzipped already.
93       * @throws IOException when failing to compress the content.
94       */
95      public ContentCachedEntry(String contentType, String characterEncoding, int statusCode, MultiMap headers, long modificationDate, String originalUrl) throws IOException {
96          this.contentType = contentType;
97          this.characterEncoding = characterEncoding;
98          this.statusCode = statusCode;
99          this.headers = headers;
100         this.lastModificationTime = modificationDate;
101         this.originalUrl = originalUrl;
102     }
103 
104     public String getOriginalURL() {
105         return this.originalUrl;
106     }
107 
108     // TODO : replacing getOut() with streamTo(OutputStream out) could help subclasses stream content
109     // TODO : from a File buffer for example, instead of holding byte[]s.
110     // TODO : but this would require pushing a dependency on servlet api in here - because we need
111     // TODO : to know if we can push gzipped content... or this would need to be passed as an explicit
112     // TODO : parameter, which isn't too exciting either...
113 
114 
115     public String getContentType() {
116         return contentType;
117     }
118 
119     public String getCharacterEncoding() {
120         return characterEncoding;
121     }
122 
123     public int getStatusCode() {
124         return statusCode;
125     }
126 
127     public MultiMap getHeaders() {
128         return headers;
129     }
130 
131     public long getLastModificationTime() {
132         return lastModificationTime;
133     }
134 
135     public String toString() {
136         return ToStringBuilder.reflectionToString(this, BYTE_ARRAY_SIZE_STYLE);
137     }
138 
139     // serialization support until commons  collection 3.3 is released
140     private void writeObject(ObjectOutputStream out) throws IOException {
141         serializableHeadersBackingList = new HashMap();
142         Iterator iter = headers.entrySet().iterator();
143         while (iter.hasNext()) {
144             Map.Entry entry = (Entry) iter.next();
145             serializableHeadersBackingList.put(entry.getKey(), new ArrayList((Collection)entry.getValue()));
146         }
147         out.defaultWriteObject();
148     }
149 
150     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
151         in.defaultReadObject();
152         headers = new MultiValueMap();
153         Iterator iter = serializableHeadersBackingList.entrySet().iterator();
154         while (iter.hasNext()) {
155             Map.Entry entry = (Entry) iter.next();
156             Collection c = (Collection) entry.getValue();
157             for (Iterator ic = c.iterator(); ic.hasNext();) {
158                 headers.put(entry.getKey(), ic.next());
159             }
160         }
161         serializableHeadersBackingList = null;
162    }
163 
164     public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
165         response.setStatus(getStatusCode());
166 
167         boolean acceptsGzipEncoding = isAcceptsGzip(request) && canServeGzipContent();
168         addHeaders(acceptsGzipEncoding, response);
169 
170         // TODO : cookies ?
171         response.setContentType(getContentType());
172         response.setCharacterEncoding(getCharacterEncoding());
173 
174         writeContent(request, response, chain, acceptsGzipEncoding);
175     }
176 
177     protected abstract void writeContent(HttpServletRequest request, HttpServletResponse response, FilterChain chain, boolean acceptsGzipEncoding) throws IOException, ServletException;
178 
179     /**
180      * Sets headers in the response object.
181      */
182     protected void addHeaders(final boolean acceptsGzipEncoding, final HttpServletResponse response) {
183         final MultiMap headers = getHeaders();
184 
185         final Iterator it = headers.keySet().iterator();
186         while (it.hasNext()) {
187             final String header = (String) it.next();
188             if (!acceptsGzipEncoding) {
189                 //TODO: this should not be necessary any more ...
190                 if ("Content-Encoding".equals(header) || "Vary".equals(header)) {
191                     continue;
192                 }
193             }
194             if (response.containsHeader(header)) {
195                 // 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.
196                 continue;
197             }
198 
199             final Collection values = (Collection) headers.get(header);
200             final Iterator valIt = values.iterator();
201             while (valIt.hasNext()) {
202                 final Object val = valIt.next();
203                 RequestHeaderUtil.setHeader(response, header, val);
204             }
205         }
206 
207         if(acceptsGzipEncoding){
208             // write the headers as well (if not written already)
209             if (!response.containsHeader("Content-Encoding")) {
210                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
211                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
212             }
213         }
214     }
215 
216     protected boolean isAcceptsGzip(HttpServletRequest request){
217         return GZipUtil.isAcceptsGzip(request);
218     }
219 
220     abstract protected boolean canServeGzipContent();
221 
222 }