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