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      private int timeToLiveInSeconds = -1;
85  
86      /**
87       *
88       * @param contentType MIME type of the cached content.
89       * @param characterEncoding Character encoding of the cached content.
90       * @param statusCode HTTP response status code (E.g. 200 - OK);
91       * @param headers Additional HTTP headers to be sent when serving this cached content.
92       * @param modificationDate Content modification date to set in the response.
93       * @param timeToLiveInSeconds
94       * @throws IOException when failing to compress the content.
95       */
96      public ContentCachedEntry(String contentType, String characterEncoding, int statusCode, MultiMap headers, long modificationDate, String originalUrl, int timeToLiveInSeconds) throws IOException {
97          this.contentType = contentType;
98          this.characterEncoding = characterEncoding;
99          this.statusCode = statusCode;
100         this.headers = headers;
101         this.lastModificationTime = modificationDate;
102         this.originalUrl = originalUrl;
103         this.timeToLiveInSeconds = timeToLiveInSeconds;
104     }
105 
106     public String getOriginalURL() {
107         return this.originalUrl;
108     }
109 
110     // TODO : replacing getOut() with streamTo(OutputStream out) could help subclasses stream content
111     // TODO : from a File buffer for example, instead of holding byte[]s.
112     // TODO : but this would require pushing a dependency on servlet api in here - because we need
113     // TODO : to know if we can push gzipped content... or this would need to be passed as an explicit
114     // TODO : parameter, which isn't too exciting either...
115 
116 
117     public String getContentType() {
118         return contentType;
119     }
120 
121     public String getCharacterEncoding() {
122         return characterEncoding;
123     }
124 
125     public int getStatusCode() {
126         return statusCode;
127     }
128 
129     public MultiMap getHeaders() {
130         return headers;
131     }
132 
133     public long getLastModificationTime() {
134         return lastModificationTime;
135     }
136 
137     public int getTimeToLiveInSeconds() {
138         return timeToLiveInSeconds;
139     }
140 
141     @Override
142     public String toString() {
143         return ToStringBuilder.reflectionToString(this, BYTE_ARRAY_SIZE_STYLE);
144     }
145 
146     // serialization support until commons  collection 3.3 is released
147     private void writeObject(ObjectOutputStream out) throws IOException {
148         serializableHeadersBackingList = new HashMap();
149         Iterator iter = headers.entrySet().iterator();
150         while (iter.hasNext()) {
151             Map.Entry entry = (Entry) iter.next();
152             serializableHeadersBackingList.put(entry.getKey(), new ArrayList((Collection)entry.getValue()));
153         }
154         out.defaultWriteObject();
155     }
156 
157     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
158         in.defaultReadObject();
159         headers = new MultiValueMap();
160         Iterator iter = serializableHeadersBackingList.entrySet().iterator();
161         while (iter.hasNext()) {
162             Map.Entry entry = (Entry) iter.next();
163             Collection c = (Collection) entry.getValue();
164             for (Iterator ic = c.iterator(); ic.hasNext();) {
165                 headers.put(entry.getKey(), ic.next());
166             }
167         }
168         serializableHeadersBackingList = null;
169    }
170 
171     public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
172         response.setStatus(getStatusCode());
173 
174         boolean acceptsGzipEncoding = isAcceptsGzip(request) && canServeGzipContent();
175         addHeaders(acceptsGzipEncoding, response);
176 
177         // TODO : cookies ?
178         response.setContentType(getContentType());
179         response.setCharacterEncoding(getCharacterEncoding());
180 
181         writeContent(request, response, chain, acceptsGzipEncoding);
182     }
183 
184     protected abstract void writeContent(HttpServletRequest request, HttpServletResponse response, FilterChain chain, boolean acceptsGzipEncoding) throws IOException, ServletException;
185 
186     /**
187      * Sets headers in the response object.
188      */
189     protected void addHeaders(final boolean acceptsGzipEncoding, final HttpServletResponse response) {
190         final MultiMap headers = getHeaders();
191 
192         final Iterator it = headers.keySet().iterator();
193         while (it.hasNext()) {
194             final String header = (String) it.next();
195             if (!acceptsGzipEncoding) {
196                 //TODO: this should not be necessary any more ...
197                 if ("Content-Encoding".equals(header) || "Vary".equals(header)) {
198                     continue;
199                 }
200             }
201             if (response.containsHeader(header)) {
202                 // 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.
203                 continue;
204             }
205 
206             final Collection values = (Collection) headers.get(header);
207             final Iterator valIt = values.iterator();
208             while (valIt.hasNext()) {
209                 final Object val = valIt.next();
210                 RequestHeaderUtil.setHeader(response, header, val);
211             }
212         }
213 
214         if(acceptsGzipEncoding){
215             // write the headers as well (if not written already)
216             if (!response.containsHeader("Content-Encoding")) {
217                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
218                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
219             }
220         }
221     }
222 
223     protected boolean isAcceptsGzip(HttpServletRequest request){
224         return GZipUtil.isAcceptsGzip(request);
225     }
226 
227     abstract protected boolean canServeGzipContent();
228 
229 }