Skip to content

Instantly share code, notes, and snippets.

@arienkock
Last active October 8, 2015 08:38
Show Gist options
  • Save arienkock/c0a236aad1ed7e31f134 to your computer and use it in GitHub Desktop.
Save arienkock/c0a236aad1ed7e31f134 to your computer and use it in GitHub Desktop.
A 2.3 Servlet Filter that rewrites URI's pointing to .js and .css files that run through HttpServletResponse#encodeURL(String) to contain hashes of the target files. This serves the purpose of assuring that caching headers set on those resources don't stop newer/changed versions of the resources from being downloaded by browsers.
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.webapp.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.io.IOUtils;
import org.springframework.util.AntPathMatcher;
/**
* A 2.3 Servlet Filter that rewrites URI's pointing to .js and .css files that
* run through {@link HttpServletResponse#encodeURL(String)} to point contain
* hashes of the target files. This serves the purpose of assuring that caching
* headers set on those resources don't stop newer/changed versions of the
* resources from being downloaded by browsers.
*
* We use a {@link CheckedInputStream} with a {@link CRC32} checksum to
* calculate the hash. This limits memory usage. The rewritten filenames are
* cached.
*
* Depends on {@link AntPathMatcher} and {@link IOUtils}.
*
* @author Arien Kock
* @since 17.11.2014
*/
public class CacheBustFilter implements Filter {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final String pattern = "/**/*-{hash:[\\w]+}.{ext:(js|css)}";
private final Map<String, String> cachedNamesMap = new Hashtable<>();
private ServletContext servletContext;
private boolean enabled;
private int contextPathEndIndex;
/**
* The init method reads active Spring Profiles to see if a profile named
* dev is active and disables rewriting if there is. It's very important to
* disable the filter during development, because this filter does NOT watch
* the files for changes.
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
servletContext = filterConfig.getServletContext();
enabled = !Arrays.asList(
System.getProperty("spring.profiles.active", "").split(","))
.contains("dev");
contextPathEndIndex = servletContext.getContextPath().length();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (enabled
&& HttpServletRequest.class
.isAssignableFrom(request.getClass())) {
final String requestURI = ((HttpServletRequest) request)
.getRequestURI();
if (pathMatcher.match(pattern, requestURI)) {
/**
* This request contains a hashed resource URI, we must remove
* the hash.
*/
String newUri = requestURI.substring(contextPathEndIndex,
requestURI.lastIndexOf('-'))
+ requestURI.substring(requestURI.lastIndexOf('.'));
/**
* Forward this request to the default handling mechanism for
* the unhashed URI
*/
request.getRequestDispatcher(newUri).forward(request, response);
} else {
/**
* Any HTTP request may call
* {@link HttpServletResponse#encodeURL(String)} so we wrap the
* response object with our hashing logic.
*/
chain.doFilter(request, new HttpServletResponseWrapper(
(HttpServletResponse) response) {
@Override
public String encodeURL(String url) {
/**
* we obviously don't want to do double I/O whenever we
* request a resource, so we use a simple cache.
*/
String cachedMappedUrl = cachedNamesMap.get(url);
if (cachedMappedUrl == null && isResource(url)) {
/**
* No cache found and the URI actually points to a
* JS/CSS file
*/
InputStream stream = servletContext
.getResourceAsStream(url.substring(contextPathEndIndex));
if (stream != null) {
try (CheckedInputStream checkedInputStream = new CheckedInputStream(
stream, new CRC32())) {
IOUtils.skip(checkedInputStream,
Long.MAX_VALUE);
String hexString = Long
.toHexString(checkedInputStream
.getChecksum().getValue());
String hashedUrl = url.substring(0,
url.lastIndexOf('.'))
+ "-"
+ hexString
+ url.substring(url
.lastIndexOf('.'));
cachedNamesMap.put(url, hashedUrl);
url = hashedUrl;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} else if (cachedMappedUrl != null) {
url = cachedMappedUrl;
}
return super.encodeURL(url);
}
});
}
} else {
chain.doFilter(request, response);
}
}
private boolean isResource(String requestUri) {
return requestUri.endsWith(".css") || requestUri.endsWith(".js");
}
@Override
public void destroy() {
}
}
@gjoseph
Copy link

gjoseph commented Nov 18, 2014

Dude, you're instantiating your wrapper for every request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment