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.Map;
47  import java.util.Map.Entry;
48  
49  import javax.servlet.FilterChain;
50  import javax.servlet.ServletException;
51  import javax.servlet.http.HttpServletRequest;
52  import javax.servlet.http.HttpServletResponse;
53  
54  import org.apache.commons.collections4.MultiMap;
55  import org.apache.commons.collections4.map.MultiValueMap;
56  import org.apache.commons.lang3.builder.ToStringBuilder;
57  import org.apache.commons.lang3.builder.ToStringStyle;
58  
59  /**
60   * Wraps a page response. It is assumed that the given content is gzipped
61   * if appropriate (i.e if the gzip filter is in the chain) and this class
62   * thus ungzips it to be able to serve both contents.
63   *
64   * @author gjoseph
65   * @version $Revision: $ ($Author: $)
66   */
67  public abstract class ContentCachedEntry implements CachedEntry, Serializable {
68  
69      private static final ToStringStyle BYTE_ARRAY_SIZE_STYLE = new ToStringStyle() {
70          @Override
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<Object, Object> 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     @Override
107     public String getOriginalURL() {
108         return this.originalUrl;
109     }
110 
111     // TODO : replacing getOut() with streamTo(OutputStream out) could help subclasses stream content
112     // TODO : from a File buffer for example, instead of holding byte[]s.
113     // TODO : but this would require pushing a dependency on servlet api in here - because we need
114     // TODO : to know if we can push gzipped content... or this would need to be passed as an explicit
115     // TODO : parameter, which isn't too exciting either...
116 
117 
118     public String getContentType() {
119         return contentType;
120     }
121 
122     public String getCharacterEncoding() {
123         return characterEncoding;
124     }
125 
126     public int getStatusCode() {
127         return statusCode;
128     }
129 
130     public MultiMap getHeaders() {
131         return headers;
132     }
133 
134     @Override
135     public long getLastModificationTime() {
136         return lastModificationTime;
137     }
138 
139     @Override
140     public int getTimeToLiveInSeconds() {
141         return timeToLiveInSeconds;
142     }
143 
144     @Override
145     public String toString() {
146         return ToStringBuilder.reflectionToString(this, BYTE_ARRAY_SIZE_STYLE);
147     }
148 
149     // serialization support until commons  collection 3.3 is released
150     private void writeObject(ObjectOutputStream out) throws IOException {
151         serializableHeadersBackingList = new HashMap<>();
152         for (Object o : headers.entrySet()) {
153             Entry entry = (Entry) o;
154             serializableHeadersBackingList.put(entry.getKey(), new ArrayList<Object>((Collection) entry.getValue()));
155         }
156         out.defaultWriteObject();
157     }
158 
159     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
160         in.defaultReadObject();
161         headers = new MultiValueMap<>();
162         for (Entry<Object, Object> objectObjectEntry : serializableHeadersBackingList.entrySet()) {
163             Collection c = (Collection) objectObjectEntry.getValue();
164             for (Object o : c) {
165                 headers.put(objectObjectEntry.getKey(), o);
166             }
167         }
168         serializableHeadersBackingList = null;
169    }
170 
171     @Override
172     public void replay(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
173         response.setStatus(getStatusCode());
174 
175         boolean acceptsGzipEncoding = isAcceptsGzip(request) && canServeGzipContent();
176         addHeaders(acceptsGzipEncoding, response);
177 
178         // TODO : cookies ?
179         response.setContentType(getContentType());
180         response.setCharacterEncoding(getCharacterEncoding());
181 
182         writeContent(request, response, chain, acceptsGzipEncoding);
183     }
184 
185     protected abstract void writeContent(HttpServletRequest request, HttpServletResponse response, FilterChain chain, boolean acceptsGzipEncoding) throws IOException, ServletException;
186 
187     /**
188      * Sets headers in the response object.
189      */
190     protected void addHeaders(final boolean acceptsGzipEncoding, final HttpServletResponse response) {
191         final MultiMap headers = getHeaders();
192 
193         for (Object o : headers.keySet()) {
194             final String header = (String) o;
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             final Collection values = (Collection) headers.get(header);
202             for (Object value : values) {
203                 //we don't use RequestHeaderUtil.setHeader(response, header, val) since we want to be able override headers (e.g with those set in rendering)
204                 if (value instanceof Long) {
205                     response.setDateHeader(header, ((Long) value).longValue());
206                 } else if (value instanceof Integer) {
207                     response.setIntHeader(header, ((Integer) value).intValue());
208                 } else if (value instanceof String) {
209                     response.setHeader(header, (String) value);
210                 } else {
211                     throw new IllegalStateException("Unrecognized type for header [" + header + "], value is: " + value);
212                 }
213             }
214         }
215 
216         if(acceptsGzipEncoding){
217             // write the headers as well (if not written already)
218             if (!response.containsHeader("Content-Encoding")) {
219                 RequestHeaderUtil.addAndVerifyHeader(response, "Content-Encoding", "gzip");
220                 RequestHeaderUtil.addAndVerifyHeader(response, "Vary", "Accept-Encoding"); // needed for proxies
221             }
222         }
223     }
224 
225     protected boolean isAcceptsGzip(HttpServletRequest request){
226         return GZipUtil.isAcceptsGzip(request);
227     }
228 
229     abstract protected boolean canServeGzipContent();
230 
231 }