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