Java tutorial
/* * Copyright (c) 2017 Public Library of Science * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package org.ambraproject.wombat.controller; import com.google.common.net.HttpHeaders; import org.ambraproject.wombat.config.site.Site; import org.ambraproject.wombat.config.site.SiteParam; import org.ambraproject.wombat.config.theme.Theme; import org.ambraproject.wombat.service.AssetService; import org.ambraproject.wombat.service.AssetService.AssetUrls; import org.ambraproject.wombat.util.HttpMessageUtil; import org.ambraproject.wombat.util.PathUtil; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @Controller public class StaticResourceController extends WombatController { /** * Path prefix for compiled assets (.js and .css). */ private static final String COMPILED_NAMESPACE = AssetUrls.RESOURCE_NAMESPACE + '/' + AssetUrls.COMPILED_PATH_PREFIX; @Autowired private AssetService assetService; /** * Return a portion of a path from a given token forward. * * @param path a path containing slash-separated tokens * @param token the token to look for * @return a slash-separated substring of path containing {@code token} and everything after it * @throws IllegalArgumentException if {@code token} is not in {@code path} */ private static String pathFrom(String path, String token) { List<String> pathTokens = PathUtil.SPLITTER.splitToList(path); int index = pathTokens.indexOf(token); if (index < 0) { throw new IllegalArgumentException(String.format("\"%s\" not found in %s", token, pathTokens)); } List<String> targetTokens = pathTokens.subList(index, pathTokens.size()); return PathUtil.JOINER.join(targetTokens); } @RequestMapping(name = "staticResource", value = "/" + AssetUrls.RESOURCE_NAMESPACE + "/**") public void serveResource(HttpServletRequest request, HttpServletResponse response, HttpSession session, @SiteParam Site site) throws IOException { Theme theme = site.getTheme(); // Kludge to get "resource/**" String servletPath = request.getRequestURI(); String filePath = pathFrom(servletPath, AssetUrls.RESOURCE_NAMESPACE); if (filePath.length() <= AssetUrls.RESOURCE_NAMESPACE.length() + 1) { throw new NotFoundException(); // in case of a request to "resource/" root } if (corsEnabled(site, filePath)) { response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); } response.setContentType(session.getServletContext().getMimeType(servletPath)); if (filePath.startsWith(COMPILED_NAMESPACE)) { serveCompiledAsset(filePath, request, response); } else { serveFile(filePath, request, response, theme); } } private static boolean corsEnabled(Site site, String filePath) throws IOException { Map<String, Object> resourceConfig = site.getTheme().getConfigMap("resource"); List<String> corsPrefixes = (List<String>) resourceConfig.get("cors"); if (corsPrefixes == null) return false; String resourceName = filePath.substring(AssetUrls.RESOURCE_NAMESPACE.length() + 1); for (String prefix : corsPrefixes) { if (resourceName.startsWith(prefix)) { return true; } } return false; } /** * Serves a file provided by a theme. * * @param filePath the path to the file (relative to the theme) * @param response response object * @param theme specifies the theme from which we are loading the file * @throws IOException */ private void serveFile(String filePath, HttpServletRequest request, HttpServletResponse response, Theme theme) throws IOException { try (InputStream inputStream = theme.getStaticResource(filePath)) { if (inputStream == null) { throw new NotFoundException(); } else { Theme.ResourceAttributes attributes = theme.getResourceAttributes(filePath); // We use a "weak" etag, that is, one prepended by "W/". This means that the resource should be // considered semantically-equivalent, but not byte-identical, if the etags match. It's probably // splitting hairs, but this is most appropriate since we don't use a fingerprint of the contents // here (instead concatenating length and mtime). This is what the legacy ambra does for all // resources. String etag = String.format("W/\"%d-%d\"", attributes.getContentLength(), attributes.getLastModified()); if (HttpMessageUtil.checkIfModifiedSince(request, attributes.getLastModified(), etag)) { response.setHeader("Etag", etag); response.setDateHeader(HttpHeaders.LAST_MODIFIED, attributes.getLastModified()); try (OutputStream outputStream = response.getOutputStream()) { IOUtils.copy(inputStream, outputStream); } } else { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("Etag", etag); } } } catch (FileNotFoundException e) { // In case filePath refers to a directory throw new NotFoundException(e); } } private static final Pattern COMPILED_ASSET_PATTERN = Pattern .compile("" + COMPILED_NAMESPACE + AssetUrls.COMPILED_NAME_PREFIX + "(\\w+)" // The asset hash in base 32 + "\\.\\w+"); // The file extension. /** * Serves a .js or .css asset that has already been concatenated and minified. See {@link AssetService} for details on * this process. * * @param filePath the path to the file (relative to the theme) * @param response response object * @throws IOException */ private void serveCompiledAsset(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException { // The hash is already included in the compiled asset's filename, so we take advantage // of that here and use it as the etag. Matcher matcher = COMPILED_ASSET_PATTERN.matcher(filePath); if (!matcher.matches()) { throw new IllegalArgumentException(filePath + " is not a valid compiled asset path"); } String basename = filePath.substring(COMPILED_NAMESPACE.length()); // This is a "strong" etag since it's based on a fingerprint of the contents. String etag = String.format("\"%s\"", matcher.group(1)); long lastModified = assetService.getLastModifiedTime(basename); if (HttpMessageUtil.checkIfModifiedSince(request, lastModified, etag)) { response.setHeader("Etag", etag); response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified); assetService.serveCompiledAsset(basename, response.getOutputStream()); } else { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("Etag", etag); } } }