/** * 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.inject.assistedinject.Assisted; 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.internal.WebErrorCode; import org.seedstack.shed.ClassLoaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.activation.MimetypesFileTypeMap; import javax.inject.Inject; import javax.servlet.ServletContext; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; class WebResourcesResolverImpl implements WebResourceResolver { private static final Logger LOGGER = LoggerFactory.getLogger(WebResourcesResolverImpl.class); private static final Pattern EXTENSION_PATTERN = Pattern.compile("\\.(\\w+)$", Pattern.CASE_INSENSITIVE); private static final Pattern CONSECUTIVE_SLASHES_PATTERN = Pattern.compile("(/)\\1+"); private static final String CLASSPATH_LOCATION = "META-INF/resources"; private static final String MINIFIED_GZIPPED_EXT_PATTERN = ".min.$1.gz"; private static final String GZIPPED_EXT_PATTERN = ".$1.gz"; private static final String MINIFIED_EXT_PATTERN = ".min.$1"; private final MimetypesFileTypeMap mimetypesFileTypeMap; private final boolean serveMinifiedResources; private final boolean serveGzippedResources; private final boolean onTheFlyGzipping; private final ClassLoader classLoader; private final ServletContext servletContext; @Inject WebResourcesResolverImpl(final Application application, @Assisted ServletContext servletContext) { WebConfig.StaticResourcesConfig staticResourcesConfig = application.getConfiguration().get(WebConfig.class).staticResources(); this.servletContext = servletContext; this.classLoader = ClassLoaders.findMostCompleteClassLoader(WebResourcesResolverImpl.class); this.mimetypesFileTypeMap = new MimetypesFileTypeMap(); this.serveMinifiedResources = staticResourcesConfig.isMinificationEnabled(); this.serveGzippedResources = staticResourcesConfig.isGzipEnabled(); this.onTheFlyGzipping = staticResourcesConfig.isOnTheFlyGzipEnabled(); } @Override public ResourceInfo resolveResourceInfo(ResourceRequest resourceRequest) { String normalizedPath; if (resourceRequest.getPath() == null) { normalizedPath = ""; } else { normalizedPath = CONSECUTIVE_SLASHES_PATTERN.matcher(resourceRequest.getPath()).replaceAll("$1"); } if (normalizedPath.startsWith("/")) { normalizedPath = "" + normalizedPath; } else { normalizedPath = "/" + normalizedPath; } // Determine content type with the normalized path String contentType = mimetypesFileTypeMap.getContentType(normalizedPath); if (contentType == null) { contentType = "application/octet-stream"; } Matcher matcher = EXTENSION_PATTERN.matcher(normalizedPath); URL resourceUrl; // search in docroot first (and META-INF/resources if servlet version is >= 3.0) try { if (resourceRequest.isAcceptGzip() && serveGzippedResources) { resourceUrl = this.servletContext.getResource(matcher.replaceAll(MINIFIED_GZIPPED_EXT_PATTERN)); if (serveMinifiedResources && resourceUrl != null) { return new ResourceInfo(resourceUrl, true, contentType); } resourceUrl = this.servletContext.getResource(matcher.replaceAll(GZIPPED_EXT_PATTERN)); if (resourceUrl != null) { return new ResourceInfo(resourceUrl, true, contentType); } } resourceUrl = this.servletContext.getResource(matcher.replaceAll(MINIFIED_EXT_PATTERN)); if (serveMinifiedResources && resourceUrl != null) { return new ResourceInfo(resourceUrl, false, contentType); } resourceUrl = this.servletContext.getResource(normalizedPath); if (resourceUrl != null) { return new ResourceInfo(resourceUrl, false, contentType); } } catch (MalformedURLException e) { throw SeedException.wrap(e, WebErrorCode.ERROR_RETRIEVING_RESOURCE); } // search in classpath last if (resourceRequest.isAcceptGzip() && serveGzippedResources) { resourceUrl = classLoader.getResource(CLASSPATH_LOCATION + matcher.replaceAll(MINIFIED_GZIPPED_EXT_PATTERN)); if (serveMinifiedResources && resourceUrl != null) { return new ResourceInfo(resourceUrl, true, contentType); } resourceUrl = classLoader.getResource(CLASSPATH_LOCATION + matcher.replaceAll(GZIPPED_EXT_PATTERN)); if (resourceUrl != null) { return new ResourceInfo(resourceUrl, true, contentType); } } resourceUrl = classLoader.getResource(CLASSPATH_LOCATION + matcher.replaceAll(MINIFIED_EXT_PATTERN)); if (serveMinifiedResources && resourceUrl != null) { return new ResourceInfo(resourceUrl, false, contentType); } resourceUrl = classLoader.getResource(CLASSPATH_LOCATION + normalizedPath); if (resourceUrl != null) { return new ResourceInfo(resourceUrl, false, contentType); } return null; } @Override public URI resolveURI(String path) { String contextPath = this.servletContext.getContextPath(); // Context path with a value of / is invalid per spec but may still be provided by server if ("/".equals(contextPath)) { contextPath = ""; } if (path.startsWith(CLASSPATH_LOCATION)) { try { StringBuilder sb = new StringBuilder(); if (!contextPath.isEmpty()) { sb.append(contextPath); } sb.append(path.substring(CLASSPATH_LOCATION.length())); return new URI(null, sb.toString(), null); } catch (URISyntaxException e) { LOGGER.debug("Error during resolution of " + path, e); return null; } } return null; } @Override public boolean isCompressible(ResourceInfo resourceInfo) { return serveGzippedResources && onTheFlyGzipping && !resourceInfo.isGzipped() && (resourceInfo.getContentType().startsWith("text/") || "application/json".equals(resourceInfo.getContentType())); } }