/** * Copyright (c) 2013-2016, The SeedStack authors <http://seedstack.org> * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.seedstack.seed.web.internal.resources; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.inject.Injector; import org.seedstack.seed.Application; import org.seedstack.seed.SeedException; import org.seedstack.seed.web.ResourceInfo; import org.seedstack.seed.web.ResourceRequest; import org.seedstack.seed.web.WebConfig; import org.seedstack.seed.web.WebResourceResolver; import org.seedstack.seed.web.WebResourceResolverFactory; import org.seedstack.seed.web.internal.ServletContextUtils; import org.seedstack.seed.web.internal.WebErrorCode; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.zip.GZIPOutputStream; /** * This web resource filter provides automatic static resource serving from the classpath and the docroot with some * benefits over the container default resource serving: * <p> * <ul> * <li>Multiples locations can be aggregated and served under the same path,</li> * <li>Automatic serving of pre-minified and/or pre-gzipped versions of resources,</li> * <li>On-the-fly gzipping of resources,</li> * <li>Cache friendly.</li> * </ul> */ public class WebResourcesFilter implements Filter { private static final String HEADER_IFMODSINCE = "If-Modified-Since"; private static final String HEADER_LASTMOD = "Last-Modified"; private static final String WEB_INF = "/WEB-INF/"; private static final String SLASH = "/"; private int bufferSize; private LoadingCache<ResourceRequest, Optional<ResourceInfo>> resourceInfoCache; private long servletInitTime; private WebResourceResolver webResourceResolver; @Override public void init(FilterConfig config) throws ServletException { Injector injector = ServletContextUtils.getInjector(config.getServletContext()); WebConfig.StaticResourcesConfig staticResourcesConfig = injector.getInstance(Application.class).getConfiguration().get(WebConfig.class).staticResources(); this.bufferSize = staticResourcesConfig.getBufferSize(); // round the time to nearest second for proper comparison with If-Modified-Since header this.servletInitTime = System.currentTimeMillis() / 1000L * 1000L; WebConfig.StaticResourcesConfig.CacheConfig cacheConfig = staticResourcesConfig.cacheConfig(); this.resourceInfoCache = CacheBuilder.newBuilder() .maximumSize(cacheConfig.getMaxSize()) .concurrencyLevel(cacheConfig.getConcurrencyLevel()) .initialCapacity(cacheConfig.getInitialSize()) .build(new CacheLoader<ResourceRequest, Optional<ResourceInfo>>() { @Override public Optional<ResourceInfo> load(ResourceRequest key) { return java.util.Optional.ofNullable(webResourceResolver.resolveResourceInfo(key)); } }); this.webResourceResolver = injector.getInstance(WebResourceResolverFactory.class).createWebResourceResolver(config.getServletContext()); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; String path = httpServletRequest.getRequestURI().substring(httpServletRequest.getContextPath().length()); String acceptEncodingHeader = httpServletRequest.getHeader("Accept-Encoding"); boolean acceptGzip = acceptEncodingHeader != null && acceptEncodingHeader.contains("gzip"); if (path.isEmpty() || path.endsWith(SLASH) || path.startsWith(WEB_INF)) { filterChain.doFilter(servletRequest, servletResponse); } else { // Find resource Optional<ResourceInfo> optionalResourceInfo; try { optionalResourceInfo = resourceInfoCache.get(new ResourceRequest(path, acceptGzip)); } catch (ExecutionException e) { throw SeedException.wrap(e, WebErrorCode.UNABLE_TO_DETERMINE_RESOURCE_INFO).put("path", path); } if (!optionalResourceInfo.isPresent()) { filterChain.doFilter(servletRequest, servletResponse); } else { long ifModifiedSince = ((HttpServletRequest) servletRequest).getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < servletInitTime) { // Set last modified header httpServletResponse.setDateHeader(HEADER_LASTMOD, servletInitTime); // Prepare response ResourceInfo resourceInfo = optionalResourceInfo.get(); httpServletResponse.setContentType(resourceInfo.getContentType()); ResourceData resourceData = prepareResourceData(resourceInfo, acceptGzip); if (resourceData.gzipped) { httpServletResponse.addHeader("Content-Encoding", "gzip"); } httpServletResponse.addHeader("Content-Length", Integer.toString(resourceData.data.length)); // Write data httpServletResponse.getOutputStream().write(resourceData.data); } else { // Send that resource was not modified httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } } @Override public void destroy() { // nothing to do here } private ResourceData prepareResourceData(ResourceInfo resourceInfo, boolean acceptGzip) throws IOException { boolean gzippedOnTheFly = false; OutputStream os; ByteArrayOutputStream baos; if (acceptGzip && webResourceResolver.isCompressible(resourceInfo)) { baos = new ByteArrayOutputStream(); os = new GZIPOutputStream(baos); gzippedOnTheFly = true; } else { os = baos = new ByteArrayOutputStream(); } // Copy data InputStream is = null; try { is = resourceInfo.getUrl().openStream(); byte[] buffer = new byte[bufferSize]; int readBytes = is.read(buffer); while (readBytes != -1) { os.write(buffer, 0, readBytes); readBytes = is.read(buffer); } os.close(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // nothing to do } } } return new ResourceData(baos.toByteArray(), resourceInfo.isGzipped() || gzippedOnTheFly); } private static class ResourceData { final byte[] data; final boolean gzipped; ResourceData(byte[] data, boolean gzipped) { //NOSONAR this.data = data; this.gzipped = gzipped; } } }