Clover icon

Magnolia Module Cache 5.5.9

  1. Project Clover database Mon Nov 25 2019 16:46:50 CET
  2. Package info.magnolia.module.cache.filter

File ResponseExpirationCalculator.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart9.png
24% of files have more coverage

Code metrics

54
77
8
2
261
153
42
0.55
9.62
4
5.25
2.8% of code in this file is excluded from these metrics.

Classes

Class Line # Actions
ResponseExpirationCalculator 66 70 0% 35 11
0.9098360591%
ResponseExpirationCalculator.HeaderEntry 68 7 19% 7 8
0.529411852.9%
 

Contributing tests

This file is covered by 23 tests. .

Source view

1    /**
2    * This file Copyright (c) 2011-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.cache.CacheConstants;
37   
38    import java.util.ArrayList;
39    import java.util.Date;
40    import java.util.List;
41   
42    import org.apache.http.HeaderElement;
43    import org.apache.http.client.utils.DateUtils;
44    import org.apache.http.message.BasicHeaderValueParser;
45    import org.slf4j.Logger;
46    import org.slf4j.LoggerFactory;
47   
48    /**
49    * Calculates how long a shared cache may hold a response based on its response headers. The most restrictive policy
50    * gets used while respecting the precedence rules dictated by RFC-2616. More specifically:
51    * <ul>
52    * <li>Cache-Control: s-maxage has precedence over</li>
53    * <li>Cache-Control: max-age which in turn has precedence over</li>
54    * <li>Expires:</li>
55    * </ul>
56    * <p>
57    * Given Cache-Control: max-age=5 and Cache-Control: max-age=15 the most restrictive is 5.
58    * </p>
59    * <p>
60    * Given Cache-Control: max-age=5 and Cache-Control: s-maxage=15 the latter has precedence resulting in 15.
61    * </p>
62    * If either of Pragma: no-cache, Cache-Control: no-cache and Cache-Control: private is present the response is
63    * considered to be already-expired.
64    * Uses Apache HttpClient to parse the headers.
65    */
 
66    public class ResponseExpirationCalculator {
67   
 
68    private static class HeaderEntry {
69   
70    private final String name;
71    private final Object value;
72    private HeaderElement[] elements;
73   
 
74  25 toggle public HeaderEntry(String name, Object value) {
75  25 this.name = name;
76  25 this.value = value;
77    }
78   
 
79    toggle public String getName() {
80    return name;
81    }
82   
 
83    toggle public Object getValue() {
84    return value;
85    }
86   
 
87  37 toggle public HeaderElement[] getElements() {
88  37 if (elements == null) {
89  13 elements = BasicHeaderValueParser.parseElements(value.toString(), BasicHeaderValueParser.INSTANCE);
90    }
91  37 return elements;
92    }
93   
 
94  0 toggle public String toExternalFormat() {
95  0 return (name != null ? name : "") + ": " + (value != null ? value : "");
96    }
97   
 
98  0 toggle @Override
99    public String toString() {
100  0 return toExternalFormat();
101    }
102    }
103   
104    private static final Logger logger = LoggerFactory.getLogger(ResponseExpirationCalculator.class);
105   
106    private final List<HeaderEntry> headers = new ArrayList<HeaderEntry>();
107   
 
108  34 toggle public boolean addHeader(String name, Object value) {
109   
110  34 if (CacheConstants.HEADER_EXPIRES.equals(name)) {
111  8 this.headers.add(new HeaderEntry(name, value));
112  8 return true;
113    }
114   
115  26 if (CacheConstants.HEADER_CACHE_CONTROL.equals(name)) {
116  15 this.headers.add(new HeaderEntry(name, value));
117  15 return true;
118    }
119   
120  11 if (CacheConstants.HEADER_PRAGMA.equals(name)) {
121  2 HeaderEntry headerEntry = new HeaderEntry(name, value);
122  2 if (isHeaderWithElement(headerEntry, CacheConstants.HEADER_PRAGMA, CacheConstants.HEADER_VALUE_NO_CACHE)) {
123  2 this.headers.add(headerEntry);
124  2 return true;
125    }
126    }
127   
128  9 return false;
129    }
130   
131    /**
132    * Returns the number of seconds the response can be cached where 0 means that the its already expired and must not
133    * be cached and -1 means that there's no information on how long it can be cached.
134    */
 
135  14 toggle public int getMaxAgeInSeconds() {
136   
137  14 for (HeaderEntry header : headers) {
138   
139    // Pragma no-cache as response header is no specified by HTTP but is widely used
140  19 if (isHeaderWithElement(header, CacheConstants.HEADER_PRAGMA, CacheConstants.HEADER_VALUE_NO_CACHE)) {
141  1 return 0;
142    }
143   
144    // RFC-2616 Section 14.9.1 - [...] a cache MUST NOT use the response to satisfy a subsequent request [...]
145  18 if (isHeaderWithElement(header, CacheConstants.HEADER_CACHE_CONTROL, CacheConstants.HEADER_VALUE_NO_CACHE)) {
146  1 return 0;
147    }
148   
149    // RFC-2616 Section 14.9.1 - Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache.
150  17 if (isHeaderWithElement(header, CacheConstants.HEADER_CACHE_CONTROL, CacheConstants.HEADER_VALUE_PRIVATE)) {
151  1 return 0;
152    }
153    }
154   
155  11 int maxAge = -1;
156   
157    // RFC-2616 Section 14.9.3 - If a response includes an s-maxage directive, then for a shared cache (but not for a
158    // private cache), the maximum age specified by this directive overrides the maximum age specified by either the
159    // max-age directive or the Expires header.
160   
161  11 for (HeaderEntry header : headers) {
162  16 HeaderElement element = getHeaderElement(header, CacheConstants.HEADER_CACHE_CONTROL, CacheConstants.HEADER_VALUE_S_MAXAGE);
163  16 if (element != null) {
164  5 try {
165  5 int n = Integer.parseInt(element.getValue());
166  5 if (maxAge == -1 || n < maxAge) {
167  4 maxAge = n;
168    }
169    } catch (NumberFormatException e) {
170  0 logger.warn("Ignoring unparseable " + CacheConstants.HEADER_CACHE_CONTROL + " header [" + header.toExternalFormat() + "]");
171    }
172    }
173    }
174   
175  11 if (maxAge != -1) {
176  4 return maxAge;
177    }
178   
179    // RFC-2616 Section 14.9.3 - If a response includes both an Expires header and a max-age directive, the max-age
180    // directive overrides the Expires header, even if the Expires header is more restrictive.
181   
182  7 for (HeaderEntry header : headers) {
183  10 HeaderElement element = getHeaderElement(header, CacheConstants.HEADER_CACHE_CONTROL, CacheConstants.HEADER_VALUE_MAX_AGE);
184  10 if (element != null) {
185  4 try {
186  4 int n = Integer.parseInt(element.getValue());
187  4 if (maxAge == -1 || n < maxAge) {
188  3 maxAge = n;
189    }
190    } catch (NumberFormatException e) {
191  0 logger.warn("Ignoring unparseable " + CacheConstants.HEADER_CACHE_CONTROL + " header [" + header.toExternalFormat() + "]");
192    }
193    }
194    }
195   
196  7 if (maxAge != -1) {
197  3 return maxAge;
198    }
199   
200    // Expires header, RFC-2616 Section 14.21
201   
202  4 for (HeaderEntry header : headers) {
203  5 if (CacheConstants.HEADER_EXPIRES.equals(header.getName())) {
204   
205  5 Object value = header.getValue();
206   
207  5 if (value instanceof Integer) {
208    // the set value is in seconds since the epoch
209  1 int n = (int) ((Integer) value - (System.currentTimeMillis() / 1000L));
210  1 if (maxAge == -1 || n < maxAge) {
211  1 maxAge = n;
212    }
213    }
214  5 if (value instanceof Long) {
215    // the set value is in milliseconds since the epoch
216  3 int n = (int) (((Long) value - System.currentTimeMillis()) / 1000L);
217  3 if (maxAge == -1 || n < maxAge) {
218  2 maxAge = n;
219    }
220    }
221  5 if (value instanceof String) {
222  1 String s = (String) value;
223   
224    // RFC2616 Section 14.21 - must treat 0 as already expired
225  1 if ("0".equals(s)) {
226  0 return 0;
227    }
228   
229    // A http-date as specified in RFC2616 Section 3.3.1, one of three possible date formats
230  1 Date expires = DateUtils.parseDate(s);
231  1 if (expires == null) {
232  0 logger.warn("Ignoring unparsable {} header [{}]", CacheConstants.HEADER_EXPIRES, header.toExternalFormat());
233  0 continue;
234    }
235  1 int n = (int) (expires.getTime() - System.currentTimeMillis());
236  1 if (maxAge == -1 || n < maxAge) {
237  1 maxAge = n;
238    }
239    }
240    }
241    }
242   
243  4 return maxAge;
244    }
245   
 
246  56 toggle private boolean isHeaderWithElement(HeaderEntry header, String headerName, String elementName) {
247  56 return getHeaderElement(header, headerName, elementName) != null;
248    }
249   
 
250  82 toggle private HeaderElement getHeaderElement(HeaderEntry header, String headerName, String elementName) {
251  82 if (headerName.equals(header.getName())) {
252  37 HeaderElement[] elements = header.getElements();
253  37 for (HeaderElement element : elements) {
254  43 if (element.getName().equals(elementName)) {
255  14 return element;
256    }
257    }
258    }
259  68 return null;
260    }
261    }