View Javadoc
1   /**
2    * This file Copyright (c) 2008-2018 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.collections4.MultiMap;
56  import org.apache.commons.collections4.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             final Collection values = (Collection) headers.get(header);
207             final Iterator valIt = values.iterator();
208             while (valIt.hasNext()) {
209                 final Object value = valIt.next();
210                 //we don't use RequestHeaderUtil.setHeader(response, header, val) since we want to be able override headers (e.g with those set in rendering)
211                 if (value instanceof Long) {
212                     response.setDateHeader(header, ((Long) value).longValue());
213                 } else if (value instanceof Integer) {
214                     response.setIntHeader(header, ((Integer) value).intValue());
215                 } else if (value instanceof String) {
216                     response.setHeader(header, (String) value);
217                 } else {
218                     throw new IllegalStateException("Unrecognized type for header [" + header + "], value is: " + value);
219                 }
220             }
221         }
222 
223         if(acceptsGzipEncoding){
224             // write the headers as well (if not written already)
225             if (!response.containsHeader("Content-Encoding")) {
226                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
227                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
228             }
229         }
230     }
231 
232     protected boolean isAcceptsGzip(HttpServletRequest request){
233         return GZipUtil.isAcceptsGzip(request);
234     }
235 
236     abstract protected boolean canServeGzipContent();
237 
238 }