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          @Override
72          protected void appendDetail(StringBuffer buffer, String fieldName,
73                  byte[] array) {
74              super.appendDetail(buffer, fieldName, array.length + " bytes");
75          }
76      };
77  
78      private final String contentType;
79      private final String characterEncoding;
80      private final int statusCode;
81      private transient MultiMap headers;
82      private Map serializableHeadersBackingList;
83      private final long lastModificationTime;
84      private String originalUrl;
85  
86      /**
87       * @param out Cached content.
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 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.
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) 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     }
104 
105     @Override
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     @Override
134     public long getLastModificationTime() {
135         return lastModificationTime;
136     }
137 
138     @Override
139     public String toString() {
140         return ToStringBuilder.reflectionToString(this, BYTE_ARRAY_SIZE_STYLE);
141     }
142 
143     // serialization support until commons  collection 3.3 is released
144     private void writeObject(ObjectOutputStream out) throws IOException {
145         serializableHeadersBackingList = new HashMap();
146         Iterator iter = headers.entrySet().iterator();
147         while (iter.hasNext()) {
148             Map.Entry entry = (Entry) iter.next();
149             serializableHeadersBackingList.put(entry.getKey(), new ArrayList((Collection)entry.getValue()));
150         }
151         out.defaultWriteObject();
152     }
153 
154     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
155         in.defaultReadObject();
156         headers = new MultiValueMap();
157         Iterator iter = serializableHeadersBackingList.entrySet().iterator();
158         while (iter.hasNext()) {
159             Map.Entry entry = (Entry) iter.next();
160             Collection c = (Collection) entry.getValue();
161             for (Iterator ic = c.iterator(); ic.hasNext();) {
162                 headers.put(entry.getKey(), ic.next());
163             }
164         }
165         serializableHeadersBackingList = null;
166    }
167 
168     @Override
169     public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
170         response.setStatus(getStatusCode());
171 
172         boolean acceptsGzipEncoding = isAcceptsGzip(request) && canServeGzipContent();
173         addHeaders(acceptsGzipEncoding, response);
174 
175         // TODO : cookies ?
176         response.setContentType(getContentType());
177         response.setCharacterEncoding(getCharacterEncoding());
178 
179         writeContent(request, response, chain, acceptsGzipEncoding);
180     }
181 
182     protected abstract void writeContent(HttpServletRequest request, HttpServletResponse response, FilterChain chain, boolean acceptsGzipEncoding) throws IOException, ServletException;
183 
184     /**
185      * Sets headers in the response object.
186      */
187     protected void addHeaders(final boolean acceptsGzipEncoding, final HttpServletResponse response) {
188         final MultiMap headers = getHeaders();
189 
190         final Iterator it = headers.keySet().iterator();
191         while (it.hasNext()) {
192             final String header = (String) it.next();
193             if (!acceptsGzipEncoding) {
194                 //TODO: this should not be necessary any more ...
195                 if ("Content-Encoding".equals(header) || "Vary".equals(header)) {
196                     continue;
197                 }
198             }
199             if (response.containsHeader(header)) {
200                 // 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.
201                 continue;
202             }
203 
204             final Collection values = (Collection) headers.get(header);
205             final Iterator valIt = values.iterator();
206             while (valIt.hasNext()) {
207                 final Object val = valIt.next();
208                 RequestHeaderUtil.setHeader(response, header, val);
209             }
210         }
211 
212         if(acceptsGzipEncoding){
213             // write the headers as well (if not written already)
214             if (!response.containsHeader("Content-Encoding")) {
215                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
216                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
217             }
218         }
219     }
220 
221     protected boolean isAcceptsGzip(HttpServletRequest request){
222         return GZipUtil.isAcceptsGzip(request);
223     }
224 
225     abstract protected boolean canServeGzipContent();
226 
227 }