001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.webapp.content;
021
022import java.io.IOException;
023import java.text.DateFormat;
024import java.text.SimpleDateFormat;
025import java.util.ArrayList;
026import java.util.Date;
027import java.util.Locale;
028import java.util.TimeZone;
029
030import javax.servlet.Filter;
031import javax.servlet.FilterChain;
032import javax.servlet.FilterConfig;
033import javax.servlet.ServletException;
034import javax.servlet.ServletRequest;
035import javax.servlet.ServletResponse;
036import javax.servlet.http.HttpServletRequest;
037import javax.servlet.http.HttpServletResponse;
038
039/**
040 * Adapted from {@link http
041 * ://www.digitalsanctuary.com/tech-blog/java/jboss/setting
042 * -cache-headers-from-jboss.html}
043 * 
044 * <p>
045 * Usage:
046 * 
047 * <pre>
048 * &lt;filter>
049 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
050 *   &lt;filter-class>org.apache.isis.core.webapp.content.ResourceCachingFilter&lt;/filter-class>
051 *   &lt;init-param>
052 *     &lt;param-name>CacheTime&lt;/param-name>
053 *     &lt;param-value>86400&lt;/param-value>
054 *   &lt;/init-param>
055 * &lt;/filter>
056 * ...
057 * &lt;filter-mapping>
058 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
059 *   &lt;url-pattern>*.js&lt;/url-pattern>
060 * &lt;/filter-mapping>
061 * &lt;filter-mapping>
062 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
063 *   &lt;url-pattern>*.css&lt;/url-pattern>
064 * &lt;/filter-mapping>
065 * &lt;filter-mapping>
066 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
067 *   &lt;url-pattern>*.jpg&lt;/url-pattern>
068 * &lt;/filter-mapping>
069 * &lt;filter-mapping>
070 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
071 *   &lt;url-pattern>*.png&lt;/url-pattern>
072 * &lt;/filter-mapping>
073 * &lt;filter-mapping>
074 *   &lt;filter-name>ResourceCachingFilter&lt;/filter-name>
075 *   &lt;url-pattern>*.gif&lt;/url-pattern>
076 * &lt;/filter-mapping>
077 * </pre>
078 */
079public class ResourceCachingFilter implements Filter {
080
081    /**
082     * Attribute set on {@link HttpServletRequest} if the filter has been
083     * applied.
084     * 
085     * <p>
086     * This is intended to inform other filters.
087     */
088    private static final String REQUEST_ATTRIBUTE = ResourceCachingFilter.class.getName() + ".resource";
089
090    /**
091     * To allow other filters to ask whether a request is mapped to the resource
092     * caching filter.
093     * 
094     * <p>
095     * For example, the <tt>IsisSessionFilter</tt> uses this in order to skip
096     * any session handling.
097     */
098    public static boolean isCachedResource(final HttpServletRequest request) {
099        return request.getAttribute(REQUEST_ATTRIBUTE) != null;
100    }
101
102    /**
103     * The Constant MILLISECONDS_IN_SECOND.
104     */
105    private static final int MILLISECONDS_IN_SECOND = 1000;
106
107    /** The Constant POST_CHECK_VALUE. */
108    private static final String POST_CHECK_VALUE = "post-check=";
109
110    /** The Constant PRE_CHECK_VALUE. */
111    private static final String PRE_CHECK_VALUE = "pre-check=";
112
113    /** The Constant MAX_AGE_VALUE. */
114    private static final String MAX_AGE_VALUE = "max-age=";
115
116    /** The Constant ZERO_STRING_VALUE. */
117    private static final String ZERO_STRING_VALUE = "0";
118
119    /** The Constant NO_STORE_VALUE. */
120    private static final String NO_STORE_VALUE = "no-store";
121
122    /** The Constant NO_CACHE_VALUE. */
123    private static final String NO_CACHE_VALUE = "no-cache";
124
125    /** The Constant PRAGMA_HEADER. */
126    private static final String PRAGMA_HEADER = "Pragma";
127
128    /** The Constant CACHE_CONTROL_HEADER. */
129    private static final String CACHE_CONTROL_HEADER = "Cache-Control";
130
131    /** The Constant EXPIRES_HEADER. */
132    private static final String EXPIRES_HEADER = "Expires";
133
134    /** The Constant LAST_MODIFIED_HEADER. */
135    private static final String LAST_MODIFIED_HEADER = "Last-Modified";
136
137    /** The Constant CACHE_TIME_PARAM_NAME. */
138    private static final String CACHE_TIME_PARAM_NAME = "CacheTime";
139
140    /** The default for {@link #CACHE_TIME_PARAM_NAME}. */
141    private static final String CACHE_TIME_PARAM_NAME_DEFAULT = "" + 86400;
142
143    /** The reply headers. */
144    private String[][] mReplyHeaders = { {} };
145
146    /** The cache time in seconds. */
147    private Long cacheTime = 0L;
148
149    /**
150     * Initializes the Servlet filter with the cache time and sets up the
151     * unchanging headers.
152     * 
153     * @param pConfig
154     *            the config
155     * 
156     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
157     */
158    @Override
159    public void init(final FilterConfig pConfig) {
160        final ArrayList<String[]> newReplyHeaders = new ArrayList<String[]>();
161        final String cacheTime = pConfig.getInitParameter(CACHE_TIME_PARAM_NAME);
162        this.cacheTime = Long.parseLong(cacheTime != null ? cacheTime : CACHE_TIME_PARAM_NAME_DEFAULT);
163        if (this.cacheTime > 0L) {
164            newReplyHeaders.add(new String[] { CACHE_CONTROL_HEADER, MAX_AGE_VALUE + this.cacheTime.longValue() });
165            newReplyHeaders.add(new String[] { CACHE_CONTROL_HEADER, PRE_CHECK_VALUE + this.cacheTime.longValue() });
166            newReplyHeaders.add(new String[] { CACHE_CONTROL_HEADER, POST_CHECK_VALUE + this.cacheTime.longValue() });
167        } else {
168            newReplyHeaders.add(new String[] { PRAGMA_HEADER, NO_CACHE_VALUE });
169            newReplyHeaders.add(new String[] { EXPIRES_HEADER, ZERO_STRING_VALUE });
170            newReplyHeaders.add(new String[] { CACHE_CONTROL_HEADER, NO_CACHE_VALUE });
171            newReplyHeaders.add(new String[] { CACHE_CONTROL_HEADER, NO_STORE_VALUE });
172        }
173        this.mReplyHeaders = new String[newReplyHeaders.size()][2];
174        newReplyHeaders.toArray(this.mReplyHeaders);
175    }
176
177    /**
178     * Do filter.
179     * 
180     * @param servletRequest
181     *            the request
182     * @param servletResponse
183     *            the response
184     * @param chain
185     *            the chain
186     * 
187     * @throws IOException
188     *             Signals that an I/O exception has occurred.
189     * @throws ServletException
190     *             the servlet exception
191     * 
192     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
193     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
194     */
195    @Override
196    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain chain) throws IOException, ServletException {
197        // Apply the headers
198        final HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
199        final HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
200        for (final String[] replyHeader : this.mReplyHeaders) {
201            final String name = replyHeader[0];
202            final String value = replyHeader[1];
203            httpResponse.addHeader(name, value);
204        }
205        if (this.cacheTime > 0L) {
206            final long now = System.currentTimeMillis();
207            final DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
208            httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
209            httpResponse.addHeader(LAST_MODIFIED_HEADER, httpDateFormat.format(new Date(now)));
210            httpResponse.addHeader(EXPIRES_HEADER, httpDateFormat.format(new Date(now + (this.cacheTime.longValue() * MILLISECONDS_IN_SECOND))));
211        }
212        httpRequest.setAttribute(REQUEST_ATTRIBUTE, true);
213        chain.doFilter(servletRequest, servletResponse);
214    }
215
216    /**
217     * Destroy all humans!
218     * 
219     * @see javax.servlet.Filter#destroy()
220     */
221    @Override
222    public void destroy() {
223    }
224
225}