| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.catalina.servlets; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.RandomAccessFile; |
| import java.io.Reader; |
| import java.io.Serial; |
| import java.io.Serializable; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.function.Function; |
| |
| import javax.xml.transform.Source; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.stream.StreamResult; |
| import javax.xml.transform.stream.StreamSource; |
| |
| import jakarta.servlet.DispatcherType; |
| import jakarta.servlet.RequestDispatcher; |
| import jakarta.servlet.ServletContext; |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.ServletOutputStream; |
| import jakarta.servlet.ServletResponse; |
| import jakarta.servlet.ServletResponseWrapper; |
| import jakarta.servlet.UnavailableException; |
| import jakarta.servlet.http.HttpServlet; |
| import jakarta.servlet.http.HttpServletRequest; |
| import jakarta.servlet.http.HttpServletResponse; |
| |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Globals; |
| import org.apache.catalina.WebResource; |
| import org.apache.catalina.WebResourceRoot; |
| import org.apache.catalina.connector.RequestFacade; |
| import org.apache.catalina.connector.ResponseFacade; |
| import org.apache.catalina.util.IOTools; |
| import org.apache.catalina.util.ServerInfo; |
| import org.apache.catalina.util.URLEncoder; |
| import org.apache.catalina.webresources.CachedResource; |
| import org.apache.tomcat.util.buf.B2CConverter; |
| import org.apache.tomcat.util.http.FastHttpDateFormat; |
| import org.apache.tomcat.util.http.Method; |
| import org.apache.tomcat.util.http.ResponseUtil; |
| import org.apache.tomcat.util.http.parser.ContentRange; |
| import org.apache.tomcat.util.http.parser.EntityTag; |
| import org.apache.tomcat.util.http.parser.Ranges; |
| import org.apache.tomcat.util.res.StringManager; |
| import org.apache.tomcat.util.security.Escape; |
| |
| |
| /** |
| * <p> |
| * The default resource-serving servlet for most web applications, used to serve static resources such as HTML pages and |
| * images. |
| * </p> |
| * <p> |
| * This servlet is intended to be mapped to <em>/</em> e.g.: |
| * </p> |
| * |
| * <pre> |
| * <servlet-mapping> |
| * <servlet-name>default</servlet-name> |
| * <url-pattern>/</url-pattern> |
| * </servlet-mapping> |
| * </pre> |
| * <p> |
| * It can be mapped to sub-paths, however in all cases resources are served from the web application resource root using |
| * the full path from the root of the web application context. <br> |
| * e.g. given a web application structure: |
| * </p> |
| * |
| * <pre> |
| * /context |
| * /images |
| * tomcat2.jpg |
| * /static |
| * /images |
| * tomcat.jpg |
| * </pre> |
| * <p> |
| * ... and a servlet mapping that maps only <code>/static/*</code> to the default servlet: |
| * </p> |
| * |
| * <pre> |
| * <servlet-mapping> |
| * <servlet-name>default</servlet-name> |
| * <url-pattern>/static/*</url-pattern> |
| * </servlet-mapping> |
| * </pre> |
| * <p> |
| * Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed while a request to |
| * <code>/context/images/tomcat2.jpg</code> will fail. |
| * </p> |
| */ |
| public class DefaultServlet extends HttpServlet { |
| |
| @Serial |
| private static final long serialVersionUID = 1L; |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected static final StringManager sm = StringManager.getManager(DefaultServlet.class); |
| |
| /** |
| * Full range marker. |
| */ |
| protected static final Ranges FULL = new Ranges(null, new ArrayList<>()); |
| |
| private static final ContentRange IGNORE = new ContentRange(null, 0, 0, 0); |
| |
| /** |
| * MIME multipart separation string |
| */ |
| protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY"; |
| |
| /** |
| * Size of file transfer buffer in bytes. |
| */ |
| protected static final int BUFFER_SIZE = 4096; |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * The debugging detail level for this servlet. |
| */ |
| protected int debug = 0; |
| |
| /** |
| * The input buffer size to use when serving resources. |
| */ |
| protected int input = 2048; |
| |
| /** |
| * Should we generate directory listings? |
| */ |
| protected boolean listings = false; |
| |
| /** |
| * Status code to use for directory redirects. |
| */ |
| protected int directoryRedirectStatusCode = HttpServletResponse.SC_FOUND; |
| |
| /** |
| * Read only flag. By default, it's set to true. |
| */ |
| protected boolean readOnly = true; |
| |
| /** |
| * List of compression formats to serve and their preference order. |
| */ |
| protected CompressionFormat[] compressionFormats; |
| |
| /** |
| * The output buffer size to use when serving resources. |
| */ |
| protected int output = 2048; |
| |
| /** |
| * Allow customized directory listing per directory. |
| */ |
| protected String localXsltFile = null; |
| |
| /** |
| * Allow customized directory listing per context. |
| */ |
| protected String contextXsltFile = null; |
| |
| /** |
| * Allow customized directory listing per instance. |
| */ |
| protected String globalXsltFile = null; |
| |
| /** |
| * Allow a readme file to be included. |
| */ |
| protected String readmeFile = null; |
| |
| /** |
| * The complete set of web application resources |
| */ |
| protected transient WebResourceRoot resources = null; |
| |
| /** |
| * File encoding to be used when reading static files. If none is specified the platform default is used. |
| */ |
| protected String fileEncoding = null; |
| private transient Charset fileEncodingCharset = null; |
| |
| /** |
| * If a file has a BOM, should that be used in preference to fileEncoding? Will default to {@link BomConfig#TRUE} in |
| * {@link #init()}. |
| */ |
| private BomConfig useBomIfPresent = null; |
| |
| /** |
| * Minimum size for sendfile usage in bytes. |
| */ |
| protected int sendfileSize = 48 * 1024; |
| |
| /** |
| * Flag to determine if server information is presented. |
| */ |
| protected boolean showServerInfo = true; |
| |
| /** |
| * Flag to determine if resources should be sorted. |
| */ |
| protected boolean sortListings = false; |
| |
| /** |
| * The sorting manager for sorting files and directories. |
| */ |
| protected transient SortManager sortManager; |
| |
| /** |
| * Flag that indicates whether partial PUTs are permitted. |
| */ |
| private boolean allowPartialPut = true; |
| |
| /** |
| * Use strong etags whenever possible. |
| */ |
| private boolean useStrongETags = false; |
| |
| /** |
| * Will direct ({@link DispatcherType#REQUEST} or {@link DispatcherType#ASYNC}) requests using the POST method be |
| * processed as GET requests. If not allowed, direct requests using the POST method will be rejected with a 405 |
| * (method not allowed). |
| */ |
| private boolean allowPostAsGet = false; |
| |
| |
| // --------------------------------------------------------- Public Methods |
| |
| @Override |
| public void destroy() { |
| // NOOP |
| } |
| |
| |
| @Override |
| public void init() throws ServletException { |
| |
| if (getServletConfig().getInitParameter("debug") != null) { |
| debug = Integer.parseInt(getServletConfig().getInitParameter("debug")); |
| } |
| |
| if (getServletConfig().getInitParameter("input") != null) { |
| input = Integer.parseInt(getServletConfig().getInitParameter("input")); |
| } |
| |
| if (getServletConfig().getInitParameter("output") != null) { |
| output = Integer.parseInt(getServletConfig().getInitParameter("output")); |
| } |
| |
| listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings")); |
| |
| if (getServletConfig().getInitParameter("directoryRedirectStatusCode") != null) { |
| String statusCodeString = getServletConfig().getInitParameter("directoryRedirectStatusCode"); |
| int statusCode = Integer.parseInt(statusCodeString); |
| switch (statusCode) { |
| case HttpServletResponse.SC_MOVED_PERMANENTLY: |
| case HttpServletResponse.SC_FOUND: |
| case HttpServletResponse.SC_TEMPORARY_REDIRECT: |
| case HttpServletResponse.SC_PERMANENT_REDIRECT: |
| directoryRedirectStatusCode = statusCode; |
| break; |
| default: |
| log(sm.getString("defaultServlet.invalidRedirectStatusCode", Integer.valueOf(statusCode))); |
| } |
| } |
| |
| if (getServletConfig().getInitParameter("readonly") != null) { |
| readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly")); |
| } |
| |
| compressionFormats = parseCompressionFormats(getServletConfig().getInitParameter("precompressed"), |
| getServletConfig().getInitParameter("gzip")); |
| |
| if (getServletConfig().getInitParameter("sendfileSize") != null) { |
| sendfileSize = Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024; |
| } |
| |
| fileEncoding = getServletConfig().getInitParameter("fileEncoding"); |
| if (fileEncoding == null) { |
| fileEncodingCharset = Charset.defaultCharset(); |
| fileEncoding = fileEncodingCharset.name(); |
| } else { |
| try { |
| fileEncodingCharset = B2CConverter.getCharset(fileEncoding); |
| } catch (UnsupportedEncodingException e) { |
| throw new ServletException(e); |
| } |
| } |
| |
| String useBomIfPresent = getServletConfig().getInitParameter("useBomIfPresent"); |
| if (useBomIfPresent == null) { |
| // Use default |
| this.useBomIfPresent = BomConfig.TRUE; |
| } else { |
| for (BomConfig bomConfig : BomConfig.values()) { |
| if (bomConfig.configurationValue.equalsIgnoreCase(useBomIfPresent)) { |
| this.useBomIfPresent = bomConfig; |
| break; |
| } |
| } |
| if (this.useBomIfPresent == null) { |
| // Unrecognised configuration value |
| IllegalArgumentException iae = |
| new IllegalArgumentException(sm.getString("defaultServlet.unknownBomConfig", useBomIfPresent)); |
| throw new ServletException(iae); |
| } |
| } |
| |
| globalXsltFile = getServletConfig().getInitParameter("globalXsltFile"); |
| contextXsltFile = getServletConfig().getInitParameter("contextXsltFile"); |
| localXsltFile = getServletConfig().getInitParameter("localXsltFile"); |
| readmeFile = getServletConfig().getInitParameter("readmeFile"); |
| |
| // Prevent the use of buffer sizes that are too small |
| if (input < 256) { |
| input = 256; |
| } |
| if (output < 256) { |
| output = 256; |
| } |
| |
| if (debug > 0) { |
| log("DefaultServlet.init: input buffer size=" + input + ", output buffer size=" + output); |
| } |
| |
| // Load the web resources |
| resources = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR); |
| |
| if (resources == null) { |
| throw new UnavailableException(sm.getString("defaultServlet.noResources")); |
| } |
| |
| if (getServletConfig().getInitParameter("showServerInfo") != null) { |
| showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo")); |
| } |
| |
| if (getServletConfig().getInitParameter("sortListings") != null) { |
| sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings")); |
| |
| if (sortListings) { |
| boolean sortDirectoriesFirst; |
| if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) { |
| sortDirectoriesFirst = |
| Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst")); |
| } else { |
| sortDirectoriesFirst = false; |
| } |
| |
| sortManager = new SortManager(sortDirectoriesFirst); |
| } |
| } |
| |
| if (getServletConfig().getInitParameter("allowPartialPut") != null) { |
| allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut")); |
| } |
| |
| if (getServletConfig().getInitParameter("useStrongETags") != null) { |
| useStrongETags = Boolean.parseBoolean(getServletConfig().getInitParameter("useStrongETags")); |
| } |
| |
| if (getServletConfig().getInitParameter("allowPostAsGet") != null) { |
| allowPostAsGet = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPostAsGet")); |
| } |
| } |
| |
| private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) { |
| List<CompressionFormat> ret = new ArrayList<>(); |
| if (precompressed != null && precompressed.indexOf('=') > 0) { |
| for (String pair : precompressed.split(",")) { |
| String[] setting = pair.split("="); |
| String encoding = setting[0]; |
| String extension = setting[1]; |
| ret.add(new CompressionFormat(extension, encoding)); |
| } |
| } else if (precompressed != null) { |
| if (Boolean.parseBoolean(precompressed)) { |
| ret.add(new CompressionFormat(".br", "br")); |
| ret.add(new CompressionFormat(".gz", "gzip")); |
| } |
| } else if (Boolean.parseBoolean(gzip)) { |
| // gzip handling is for backwards compatibility with Tomcat 8.x |
| ret.add(new CompressionFormat(".gz", "gzip")); |
| } |
| return ret.toArray(new CompressionFormat[0]); |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Return the relative path associated with this servlet. |
| * |
| * @param request The servlet request we are processing |
| * |
| * @return the relative path |
| */ |
| protected String getRelativePath(HttpServletRequest request) { |
| return getRelativePath(request, false); |
| } |
| |
| protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) { |
| // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always |
| // serves resources from the web app root with context rooted paths. |
| // i.e. it cannot be used to mount the web app root under a sub-path |
| // This method must construct a complete context rooted path, although |
| // subclasses can change this behaviour. |
| |
| String servletPath; |
| String pathInfo; |
| |
| if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { |
| // For includes, get the info from the attributes |
| pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); |
| servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); |
| } else { |
| pathInfo = request.getPathInfo(); |
| servletPath = request.getServletPath(); |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| if (!servletPath.isEmpty()) { |
| result.append(servletPath); |
| } |
| if (pathInfo != null) { |
| result.append(pathInfo); |
| } |
| if (result.isEmpty() && !allowEmptyPath) { |
| result.append('/'); |
| } |
| |
| return result.toString(); |
| } |
| |
| |
| /** |
| * Determines the appropriate path to prepend resources with when generating directory listings. Depending on the |
| * behaviour of {@link #getRelativePath(HttpServletRequest)} this will change. |
| * |
| * @param request the request to determine the path for |
| * |
| * @return the prefix to apply to all resources in the listing. |
| */ |
| protected String getPathPrefix(final HttpServletRequest request) { |
| return request.getContextPath(); |
| } |
| |
| |
| protected boolean isListings() { |
| return listings; |
| } |
| |
| |
| protected boolean isReadOnly() { |
| return readOnly || resources.isReadOnly(); |
| } |
| |
| |
| @Override |
| protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
| |
| if (req.getDispatcherType() == DispatcherType.ERROR) { |
| doGet(req, resp); |
| } else { |
| super.service(req, resp); |
| } |
| } |
| |
| |
| @Override |
| protected void doGet(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| |
| // Serve the requested resource, including the data content |
| serveResource(request, response, true, fileEncoding); |
| |
| } |
| |
| |
| @Override |
| protected void doHead(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| // Serve the requested resource, without the data content unless we are |
| // being included since in that case the content needs to be provided so |
| // the correct content length is reported for the including resource |
| boolean serveContent = DispatcherType.INCLUDE.equals(request.getDispatcherType()); |
| serveResource(request, response, serveContent, fileEncoding); |
| } |
| |
| |
| /** |
| * Override default implementation to ensure that TRACE is correctly handled. |
| * |
| * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet |
| * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client |
| * |
| * @exception IOException if an input or output error occurs while the servlet is handling the OPTIONS request |
| * @exception ServletException if the request for the OPTIONS cannot be handled |
| */ |
| @Override |
| protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
| |
| resp.setHeader("Allow", determineMethodsAllowed(req)); |
| } |
| |
| |
| /** |
| * Determines the methods normally allowed for the resource. |
| * |
| * @param req The Servlet request |
| * |
| * @return The allowed HTTP methods |
| */ |
| protected String determineMethodsAllowed(HttpServletRequest req) { |
| StringBuilder allow = new StringBuilder(); |
| |
| // Start with methods that are always allowed |
| allow.append("OPTIONS, GET, HEAD"); |
| |
| if (allowPostAsGet) { |
| allow.append(", POST"); |
| } |
| |
| // PUT and DELETE depend on readonly |
| if (!isReadOnly()) { |
| allow.append(", PUT, DELETE"); |
| } |
| |
| // Trace - assume disabled unless we can prove otherwise |
| if (req instanceof RequestFacade && ((RequestFacade) req).getAllowTrace()) { |
| allow.append(", TRACE"); |
| } |
| |
| return allow.toString(); |
| } |
| |
| |
| protected void sendNotAllowed(HttpServletRequest req, HttpServletResponse resp) throws IOException { |
| resp.addHeader("Allow", determineMethodsAllowed(req)); |
| resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); |
| } |
| |
| |
| @Override |
| protected void doPost(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| if (allowPostAsGet) { |
| doGet(request, response); |
| } else { |
| // Use a switch without a default to ensure all possibilities are explicitly handled |
| switch (request.getDispatcherType()) { |
| case ASYNC: |
| case REQUEST: { |
| // Direct POST requests may not be processed as GET |
| sendNotAllowed(request, response); |
| break; |
| } |
| case ERROR: |
| case FORWARD: |
| case INCLUDE: { |
| /* |
| * Forward and Include are processed as GET as it is possible that a POST to a servlet may use a |
| * forward or an include as part of generating the response. |
| * |
| * Error should have already been converted to GET but convert here anyway as that is better than |
| * failing the request. |
| */ |
| doGet(request, response); |
| break; |
| } |
| } |
| } |
| } |
| |
| |
| @Override |
| protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
| |
| if (isReadOnly()) { |
| sendNotAllowed(req, resp); |
| return; |
| } |
| |
| String path = getRelativePath(req); |
| |
| WebResource resource = resources.getResource(path); |
| |
| ContentRange range = parseContentRange(req, resp); |
| |
| if (range == null) { |
| // Processing error. parseContentRange() set the error code |
| return; |
| } |
| |
| if (!checkIfHeaders(req, resp, resource)) { |
| return; |
| } |
| |
| InputStream resourceInputStream = null; |
| File tempContentFile = null; |
| try { |
| // Append data specified in ranges to existing content for this |
| // resource - create a temp. file on the local filesystem to |
| // perform this operation |
| // Assume just one range is specified for now |
| if (range == IGNORE) { |
| resourceInputStream = req.getInputStream(); |
| } else { |
| tempContentFile = executePartialPut(req, range, path); |
| if (tempContentFile != null) { |
| resourceInputStream = new FileInputStream(tempContentFile); |
| } |
| } |
| |
| if (resourceInputStream != null && resources.write(path, resourceInputStream, true)) { |
| if (resource.exists()) { |
| resp.setStatus(HttpServletResponse.SC_NO_CONTENT); |
| } else { |
| resp.setStatus(HttpServletResponse.SC_CREATED); |
| } |
| } else { |
| try { |
| resp.sendError(resourceInputStream != null ? HttpServletResponse.SC_CONFLICT : |
| HttpServletResponse.SC_BAD_REQUEST); |
| } catch (IllegalStateException e) { |
| // Already committed, ignore |
| } |
| } |
| } finally { |
| if (resourceInputStream != null) { |
| try { |
| resourceInputStream.close(); |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| } |
| if (tempContentFile != null) { |
| if (!tempContentFile.delete()) { |
| log(sm.getString("defaultServlet.deleteTempFileFailed", tempContentFile.getAbsolutePath())); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Handle a partial PUT. New content specified in request is appended to existing content in oldRevisionContent (if |
| * present). This code does not support simultaneous partial updates to the same resource. |
| * |
| * @param req The Servlet request |
| * @param range The range that will be written |
| * @param path The path |
| * |
| * @return the associated file object |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected File executePartialPut(HttpServletRequest req, ContentRange range, String path) throws IOException { |
| |
| // Append data specified in ranges to existing content for this |
| // resource - create a temp. file on the local filesystem to |
| // perform this operation |
| File tempDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR); |
| File contentFile = File.createTempFile("put-part-", null, tempDir); |
| |
| try (RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw")) { |
| |
| WebResource oldResource = resources.getResource(path); |
| |
| // Copy data in oldRevisionContent to contentFile |
| if (oldResource.isFile()) { |
| try (BufferedInputStream bufOldRevStream = |
| new BufferedInputStream(oldResource.getInputStream(), BUFFER_SIZE)) { |
| |
| int numBytesRead; |
| byte[] copyBuffer = new byte[BUFFER_SIZE]; |
| while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) { |
| randAccessContentFile.write(copyBuffer, 0, numBytesRead); |
| } |
| |
| } |
| } |
| |
| randAccessContentFile.setLength(range.getLength()); |
| |
| // Append data in request input stream to contentFile |
| randAccessContentFile.seek(range.getStart()); |
| long received = 0; |
| int numBytesRead; |
| byte[] transferBuffer = new byte[BUFFER_SIZE]; |
| try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) { |
| long rangeBytes = range.getEnd() - range.getStart() + 1L; |
| while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) { |
| received += numBytesRead; |
| if (received > rangeBytes) { |
| throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange", |
| String.valueOf(received), String.valueOf(rangeBytes))); |
| } |
| randAccessContentFile.write(transferBuffer, 0, numBytesRead); |
| } |
| if (received < rangeBytes) { |
| throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange", |
| String.valueOf(received), String.valueOf(rangeBytes))); |
| } |
| } |
| |
| } catch (IOException | RuntimeException | Error e) { |
| // This has to be done this way to be able to close the file without changing the method signature |
| if (!contentFile.delete()) { |
| log(sm.getString("defaultServlet.deleteTempFileFailed", contentFile.getAbsolutePath())); |
| } |
| return null; |
| } |
| |
| return contentFile; |
| } |
| |
| |
| @Override |
| protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
| |
| if (isReadOnly()) { |
| sendNotAllowed(req, resp); |
| return; |
| } |
| |
| String path = getRelativePath(req); |
| |
| WebResource resource = resources.getResource(path); |
| |
| if (!checkIfHeaders(req, resp, resource)) { |
| return; |
| } |
| |
| if (resource.exists()) { |
| if (resource.delete()) { |
| resp.setStatus(HttpServletResponse.SC_NO_CONTENT); |
| } else { |
| sendNotAllowed(req, resp); |
| } |
| } else { |
| resp.sendError(HttpServletResponse.SC_NOT_FOUND); |
| } |
| } |
| |
| |
| /** |
| * Check if the conditions specified in the optional If headers are satisfied. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return <code>true</code> if the resource meets all the specified conditions, and <code>false</code> if any of |
| * the conditions is not satisfied, in which case request processing is stopped |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource) |
| throws IOException { |
| if (request.getHeader("If-Match") != null) { |
| if (!checkIfMatch(request, response, resource)) { |
| return false; |
| } |
| } else if (request.getHeader("If-Unmodified-Since") != null) { |
| if (!checkIfUnmodifiedSince(request, response, resource)) { |
| return false; |
| } |
| } |
| if (request.getHeader("If-None-Match") != null) { |
| return checkIfNoneMatch(request, response, resource); |
| } else if (request.getHeader("If-Modified-Since") != null) { |
| return checkIfModifiedSince(request, response, resource); |
| } |
| return true; |
| } |
| |
| |
| /** |
| * URL rewriter. |
| * |
| * @param path Path which has to be rewritten |
| * |
| * @return the rewritten path |
| */ |
| protected String rewriteUrl(String path) { |
| return URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8); |
| } |
| |
| |
| /** |
| * Serve the specified resource, optionally including the data content. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param content Should the content be included? |
| * @param inputEncoding The encoding to use if it is necessary to access the source as characters rather than as |
| * bytes |
| * |
| * @exception IOException if an input/output error occurs |
| * @exception ServletException if a servlet-specified error occurs |
| */ |
| protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, |
| String inputEncoding) throws IOException, ServletException { |
| |
| boolean serveContent = content; |
| |
| // Identify the requested resource path |
| String path = getRelativePath(request, true); |
| |
| if (debug > 0) { |
| if (serveContent) { |
| log("DefaultServlet.serveResource: Serving resource '" + path + "' headers and data"); |
| } else { |
| log("DefaultServlet.serveResource: Serving resource '" + path + "' headers only"); |
| } |
| } |
| |
| if (path.isEmpty()) { |
| // Context root redirect |
| doDirectoryRedirect(request, response); |
| return; |
| } |
| |
| WebResource resource = resources.getResource(path); |
| boolean isError = DispatcherType.ERROR == request.getDispatcherType(); |
| |
| if (!resource.exists()) { |
| // Check if we're included so we can return the appropriate |
| // missing resource name in the error |
| String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); |
| if (requestUri == null) { |
| requestUri = request.getRequestURI(); |
| } else { |
| // We're included |
| // SRV.9.3 says we must throw a FNFE |
| throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri)); |
| } |
| |
| if (isError) { |
| response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue()); |
| } else { |
| // Need to check If headers before we return a 404 |
| if (!checkIfHeaders(request, response, resource)) { |
| return; |
| } |
| response.sendError(HttpServletResponse.SC_NOT_FOUND, |
| sm.getString("defaultServlet.missingResource", requestUri)); |
| } |
| return; |
| } |
| |
| if (!resource.canRead()) { |
| // Check if we're included so we can return the appropriate |
| // missing resource name in the error |
| String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); |
| if (requestUri == null) { |
| requestUri = request.getRequestURI(); |
| } else { |
| // We're included |
| // Spec doesn't say what to do in this case but a FNFE seems |
| // reasonable |
| throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri)); |
| } |
| |
| if (isError) { |
| response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue()); |
| } else { |
| response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri); |
| } |
| return; |
| } |
| |
| boolean included = false; |
| |
| // Find content type. |
| String contentType = resource.getMimeType(); |
| if (contentType == null) { |
| contentType = getServletContext().getMimeType(resource.getName()); |
| resource.setMimeType(contentType); |
| } |
| |
| // These need to reflect the original resource, not the potentially |
| // precompressed version of the resource so get them now if they are going to |
| // be needed later |
| String eTag = null; |
| String lastModifiedHttp = null; |
| |
| if (resource.isFile() && !isError) { |
| eTag = generateETag(resource); |
| lastModifiedHttp = resource.getLastModifiedHttp(); |
| } |
| |
| // Check if the conditions specified in the optional If headers are |
| // satisfied. |
| if (resource.isFile()) { |
| // Checking If headers |
| included = (request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null); |
| if (!included && !isError && !checkIfHeaders(request, response, resource)) { |
| return; |
| } |
| } |
| |
| // Serve a precompressed version of the file if present |
| boolean usingPrecompressedVersion = false; |
| if (compressionFormats.length > 0 && !included && resource.isFile() && !pathEndsWithCompressedExtension(path)) { |
| List<PrecompressedResource> precompressedResources = getAvailablePrecompressedResources(path); |
| if (!precompressedResources.isEmpty()) { |
| ResponseUtil.addVaryFieldName(response, "accept-encoding"); |
| PrecompressedResource bestResource = getBestPrecompressedResource(request, precompressedResources); |
| if (bestResource != null) { |
| response.addHeader("Content-Encoding", bestResource.format.encoding); |
| resource = bestResource.resource; |
| usingPrecompressedVersion = true; |
| } |
| } |
| } |
| |
| Ranges ranges = FULL; |
| long contentLength = -1L; |
| |
| if (resource.isDirectory()) { |
| if (!path.endsWith("/")) { |
| doDirectoryRedirect(request, response); |
| return; |
| } |
| |
| // Skip directory listings if we have been configured to |
| // suppress them |
| if (!isListings()) { |
| response.sendError(HttpServletResponse.SC_NOT_FOUND, |
| sm.getString("defaultServlet.missingResource", request.getRequestURI())); |
| return; |
| } |
| contentType = "text/html;charset=UTF-8"; |
| } else { |
| if (!isError) { |
| // Accept ranges header |
| response.setHeader("Accept-Ranges", "bytes"); |
| |
| // Parse range specifier |
| ranges = parseRange(request, response, resource); |
| if (ranges == null) { |
| return; |
| } |
| |
| // ETag header |
| response.setHeader("ETag", eTag); |
| |
| // Last-Modified header |
| response.setHeader("Last-Modified", lastModifiedHttp); |
| } |
| |
| // Get content length |
| contentLength = resource.getContentLength(); |
| // Special case for zero length files, which would cause a |
| // (silent) ISE when setting the output buffer size |
| if (contentLength == 0L) { |
| serveContent = false; |
| } |
| } |
| |
| ServletOutputStream ostream = null; |
| PrintWriter writer = null; |
| |
| if (serveContent) { |
| // Trying to retrieve the servlet output stream |
| try { |
| ostream = response.getOutputStream(); |
| } catch (IllegalStateException e) { |
| // If it fails, we try to get a Writer instead if we're |
| // trying to serve a text file |
| if (!usingPrecompressedVersion && isText(contentType)) { |
| writer = response.getWriter(); |
| // Cannot reliably serve partial content with a Writer |
| ranges = FULL; |
| } else { |
| throw e; |
| } |
| } |
| } |
| |
| // Check to see if a Filter, Valve or wrapper has written some content. |
| // If it has, disable range requests and setting of a content length |
| // since neither can be done reliably. |
| ServletResponse r = response; |
| long contentWritten = 0; |
| while (r instanceof ServletResponseWrapper) { |
| r = ((ServletResponseWrapper) r).getResponse(); |
| } |
| if (r instanceof ResponseFacade) { |
| contentWritten = ((ResponseFacade) r).getContentWritten(); |
| } |
| if (contentWritten > 0) { |
| ranges = FULL; |
| } |
| |
| String outputEncoding = response.getCharacterEncoding(); |
| Charset charset = B2CConverter.getCharset(outputEncoding); |
| boolean conversionRequired; |
| /* |
| * The test below deliberately uses != to compare two Strings. This is because the code is looking to see if the |
| * default character encoding has been returned because no explicit character encoding has been defined. There |
| * is no clean way of doing this via the Servlet API. It would be possible to add a Tomcat specific API but that |
| * would require quite a bit of code to get to the Tomcat specific request object that may have been wrapped. |
| * The != test is a (slightly hacky) quick way of doing this. |
| */ |
| boolean outputEncodingSpecified = outputEncoding != org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name() && |
| outputEncoding != resources.getContext().getResponseCharacterEncoding(); |
| if (!usingPrecompressedVersion && isText(contentType) && outputEncodingSpecified && |
| !charset.equals(fileEncodingCharset)) { |
| conversionRequired = true; |
| // Conversion often results fewer/more/different bytes. |
| // That does not play nicely with range requests. |
| ranges = FULL; |
| } else { |
| conversionRequired = false; |
| } |
| |
| if (resource.isDirectory() || isError || ranges == FULL) { |
| // Set the appropriate output headers |
| if (contentType != null) { |
| if (debug > 0) { |
| log("DefaultServlet.serveFile: contentType='" + contentType + "'"); |
| } |
| // Don't override a previously set content type |
| if (response.getContentType() == null) { |
| response.setContentType(contentType); |
| } |
| } |
| if (resource.isFile() && contentLength >= 0 && (!serveContent || ostream != null || writer != null)) { |
| if (debug > 0) { |
| log("DefaultServlet.serveFile: contentLength=" + contentLength); |
| } |
| // Don't set a content length if something else has already |
| // written to the response or if conversion will be taking place |
| if (contentWritten == 0 && !conversionRequired) { |
| response.setContentLengthLong(contentLength); |
| } |
| } |
| |
| if (serveContent) { |
| try { |
| response.setBufferSize(output); |
| } catch (IllegalStateException ignore) { |
| // Content has already been written - this must be an include. Ignore the error and continue. |
| } |
| InputStream renderResult = null; |
| if (ostream == null) { |
| // Output via a writer so can't use sendfile or write |
| // content directly. |
| if (resource.isDirectory()) { |
| renderResult = render(request, getPathPrefix(request), resource, inputEncoding); |
| } else { |
| renderResult = resource.getInputStream(); |
| if (included) { |
| // Need to make sure any BOM is removed |
| if (!renderResult.markSupported()) { |
| renderResult = new BufferedInputStream(renderResult); |
| } |
| Charset bomCharset = processBom(renderResult, useBomIfPresent.stripBom); |
| if (bomCharset != null && useBomIfPresent.useBomEncoding) { |
| inputEncoding = bomCharset.name(); |
| } |
| } |
| } |
| copy(renderResult, writer, inputEncoding); |
| } else { |
| // Output is via an OutputStream |
| if (resource.isDirectory()) { |
| renderResult = render(request, getPathPrefix(request), resource, inputEncoding); |
| } else { |
| // Output is content of resource |
| // Check to see if conversion is required |
| if (conversionRequired || included) { |
| // When including a file, we need to check for a BOM |
| // to determine if a conversion is required, so we |
| // might as well always convert |
| InputStream source = resource.getInputStream(); |
| if (!source.markSupported()) { |
| source = new BufferedInputStream(source); |
| } |
| Charset bomCharset = processBom(source, useBomIfPresent.stripBom); |
| if (bomCharset != null && useBomIfPresent.useBomEncoding) { |
| inputEncoding = bomCharset.name(); |
| } |
| // Following test also ensures included resources |
| // are converted if an explicit output encoding was |
| // specified |
| if (outputEncodingSpecified) { |
| OutputStreamWriter osw = new OutputStreamWriter(ostream, charset); |
| PrintWriter pw = new PrintWriter(osw); |
| copy(source, pw, inputEncoding); |
| pw.flush(); |
| } else { |
| // Just included but no conversion |
| renderResult = source; |
| } |
| } else { |
| if (!checkSendfile(request, response, resource, contentLength, null)) { |
| // sendfile not possible so check if resource |
| // content is available directly via |
| // CachedResource. Do not want to call |
| // getContent() on other resource |
| // implementations as that could trigger loading |
| // the contents of a very large file into memory |
| byte[] resourceBody = null; |
| if (resource instanceof CachedResource) { |
| resourceBody = resource.getContent(); |
| } |
| if (resourceBody == null) { |
| // Resource content not directly available, |
| // use InputStream |
| renderResult = resource.getInputStream(); |
| } else { |
| // Use the resource content directly |
| ostream.write(resourceBody); |
| } |
| } |
| } |
| } |
| // If a stream was configured, it needs to be copied to |
| // the output (this method closes the stream) |
| if (renderResult != null) { |
| copy(renderResult, ostream); |
| } |
| } |
| } |
| |
| } else { |
| |
| if (ranges.getEntries().isEmpty()) { |
| return; |
| } |
| |
| // Partial content response. |
| |
| response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); |
| |
| if (ranges.getEntries().size() == 1) { |
| |
| Ranges.Entry range = ranges.getEntries().getFirst(); |
| long start = getStart(range, contentLength); |
| long end = getEnd(range, contentLength); |
| response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength); |
| long length = end - start + 1; |
| response.setContentLengthLong(length); |
| |
| if (contentType != null) { |
| if (debug > 0) { |
| log("DefaultServlet.serveFile: contentType='" + contentType + "'"); |
| } |
| response.setContentType(contentType); |
| } |
| |
| if (serveContent) { |
| try { |
| response.setBufferSize(output); |
| } catch (IllegalStateException ignore) { |
| // Content has already been written - this must be an include. Ignore the error and continue. |
| } |
| if (ostream != null) { |
| if (!checkSendfile(request, response, resource, contentLength, range)) { |
| copy(resource, contentLength, ostream, range); |
| } |
| } else { |
| // we should not get here |
| throw new IllegalStateException(); |
| } |
| } |
| } else { |
| response.setContentType("multipart/byteranges; boundary=" + mimeSeparation); |
| if (serveContent) { |
| try { |
| response.setBufferSize(output); |
| } catch (IllegalStateException e) { |
| // Content has already been written - this must be an include. Ignore the error and continue. |
| } |
| if (ostream != null) { |
| copy(resource, contentLength, ostream, ranges, contentType); |
| } else { |
| // we should not get here |
| throw new IllegalStateException(); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| /* |
| * Code borrowed heavily from Jasper's EncodingDetector |
| */ |
| private static Charset processBom(InputStream is, boolean stripBom) throws IOException { |
| // Java supported character sets do not use BOMs longer than 4 bytes |
| byte[] bom = new byte[4]; |
| is.mark(bom.length); |
| |
| int count = is.read(bom); |
| |
| // BOMs are at least 2 bytes |
| if (count < 2) { |
| skip(is, 0, stripBom); |
| return null; |
| } |
| |
| // Look for two byte BOMs |
| int b0 = bom[0] & 0xFF; |
| int b1 = bom[1] & 0xFF; |
| if (b0 == 0xFE && b1 == 0xFF) { |
| skip(is, 2, stripBom); |
| return StandardCharsets.UTF_16BE; |
| } |
| // Delay the UTF_16LE check if there are more than 2 bytes since it |
| // overlaps with UTF-32LE. |
| if (count == 2 && b0 == 0xFF && b1 == 0xFE) { |
| skip(is, 2, stripBom); |
| return StandardCharsets.UTF_16LE; |
| } |
| |
| // Remaining BOMs are at least 3 bytes |
| if (count < 3) { |
| skip(is, 0, stripBom); |
| return null; |
| } |
| |
| // UTF-8 is only 3-byte BOM |
| int b2 = bom[2] & 0xFF; |
| if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) { |
| skip(is, 3, stripBom); |
| return StandardCharsets.UTF_8; |
| } |
| |
| if (count < 4) { |
| skip(is, 0, stripBom); |
| return null; |
| } |
| |
| // Look for 4-byte BOMs |
| int b3 = bom[3] & 0xFF; |
| if (b0 == 0x00 && b1 == 0x00 && b2 == 0xFE && b3 == 0xFF) { |
| return Charset.forName("UTF-32BE"); |
| } |
| if (b0 == 0xFF && b1 == 0xFE && b2 == 0x00 && b3 == 0x00) { |
| return Charset.forName("UTF-32LE"); |
| } |
| |
| // Now we can check for UTF16-LE. There is an assumption here that we |
| // won't see a UTF16-LE file with a BOM where the first real data is |
| // 0x00 0x00 |
| if (b0 == 0xFF && b1 == 0xFE) { |
| skip(is, 2, stripBom); |
| return StandardCharsets.UTF_16LE; |
| } |
| |
| skip(is, 0, stripBom); |
| return null; |
| } |
| |
| |
| private static void skip(InputStream is, int skip, boolean stripBom) throws IOException { |
| is.reset(); |
| if (stripBom) { |
| while (skip-- > 0) { |
| if (is.read() < 0) { |
| // Ignore since included |
| break; |
| } |
| } |
| } |
| } |
| |
| |
| private static boolean isText(String contentType) { |
| return contentType == null || contentType.startsWith("text") || contentType.endsWith("xml") || |
| contentType.contains("/javascript"); |
| } |
| |
| private static boolean validate(Ranges ranges, long length) { |
| List<long[]> rangeContext = new ArrayList<>(); |
| int overlapCount = 0; |
| for (Ranges.Entry range : ranges.getEntries()) { |
| long start = getStart(range, length); |
| long end = getEnd(range, length); |
| if (start < 0 || start > end) { |
| // Invalid range |
| return false; |
| } |
| /* |
| * See https://www.rfc-editor.org/rfc/rfc9110.html#name-range and |
| * https://www.rfc-editor.org/rfc/rfc9110.html#status.416 |
| * |
| * The server MAY ignore or reject Range headers with: |
| * |
| * - "Many" (undefined) small ranges not in ascending order - not currently enforced. |
| * |
| * - More than two overlapping ranges (enforced) |
| */ |
| for (long[] r : rangeContext) { |
| long s2 = r[0]; |
| long e2 = r[1]; |
| // Given valid [s1,e1] and [s2,e2] |
| // If { s1>e2 || s2>e1 } then no overlap |
| // equivalent to |
| // If not { s1>e2 || s2>e1 } then overlap |
| // De Morgan's law |
| if (start <= e2 && s2 <= end) { |
| overlapCount++; |
| // Off by one is deliberate. There is 1 more overlapping range than there are overlaps. |
| if (overlapCount > 1) { |
| return false; |
| } |
| } |
| } |
| rangeContext.add(new long[] { start, end }); |
| } |
| return true; |
| } |
| |
| private static long getStart(Ranges.Entry range, long length) { |
| long start = range.getStart(); |
| if (start == -1) { |
| long end = range.getEnd(); |
| // If there is no start, then the start is based on the end |
| if (end >= length) { |
| return 0; |
| } else { |
| return length - end; |
| } |
| } else { |
| return start; |
| } |
| } |
| |
| private static long getEnd(Ranges.Entry range, long length) { |
| long end = range.getEnd(); |
| if (range.getStart() == -1 || end == -1 || end >= length) { |
| return length - 1; |
| } else { |
| return end; |
| } |
| } |
| |
| private boolean pathEndsWithCompressedExtension(String path) { |
| for (CompressionFormat format : compressionFormats) { |
| if (path.endsWith(format.extension)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private List<PrecompressedResource> getAvailablePrecompressedResources(String path) { |
| List<PrecompressedResource> ret = new ArrayList<>(compressionFormats.length); |
| for (CompressionFormat format : compressionFormats) { |
| WebResource precompressedResource = resources.getResource(path + format.extension); |
| if (precompressedResource.exists() && precompressedResource.isFile()) { |
| ret.add(new PrecompressedResource(precompressedResource, format)); |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Match the client preferred encoding formats to the available precompressed resources. |
| * |
| * @param request The servlet request we are processing |
| * @param precompressedResources List of available precompressed resources. |
| * |
| * @return The best matching precompressed resource or null if no match was found. |
| */ |
| private PrecompressedResource getBestPrecompressedResource(HttpServletRequest request, |
| List<PrecompressedResource> precompressedResources) { |
| Enumeration<String> headers = request.getHeaders("Accept-Encoding"); |
| PrecompressedResource bestResource = null; |
| double bestResourceQuality = 0; |
| int bestResourcePreference = Integer.MAX_VALUE; |
| while (headers.hasMoreElements()) { |
| String header = headers.nextElement(); |
| for (String preference : header.split(",")) { |
| double quality = 1; |
| int qualityIdx = preference.indexOf(';'); |
| if (qualityIdx > 0) { |
| int equalsIdx = preference.indexOf('=', qualityIdx + 1); |
| if (equalsIdx == -1) { |
| continue; |
| } |
| quality = Double.parseDouble(preference.substring(equalsIdx + 1).trim()); |
| } |
| if (quality >= bestResourceQuality) { |
| String encoding = preference; |
| if (qualityIdx > 0) { |
| encoding = encoding.substring(0, qualityIdx); |
| } |
| encoding = encoding.trim(); |
| if ("identity".equals(encoding)) { |
| bestResource = null; |
| bestResourceQuality = quality; |
| bestResourcePreference = Integer.MAX_VALUE; |
| continue; |
| } |
| if ("*".equals(encoding)) { |
| bestResource = precompressedResources.getFirst(); |
| bestResourceQuality = quality; |
| bestResourcePreference = 0; |
| continue; |
| } |
| for (int i = 0; i < precompressedResources.size(); ++i) { |
| PrecompressedResource resource = precompressedResources.get(i); |
| if (encoding.equals(resource.format.encoding)) { |
| if (quality > bestResourceQuality || i < bestResourcePreference) { |
| bestResource = resource; |
| bestResourceQuality = quality; |
| bestResourcePreference = i; |
| } |
| break; |
| } |
| } |
| } |
| } |
| } |
| return bestResource; |
| } |
| |
| private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { |
| StringBuilder location = new StringBuilder(request.getRequestURI()); |
| location.append('/'); |
| if (request.getQueryString() != null) { |
| location.append('?'); |
| location.append(request.getQueryString()); |
| } |
| // Avoid protocol relative redirects |
| while (location.length() > 1 && location.charAt(1) == '/') { |
| location.deleteCharAt(0); |
| } |
| response.sendRedirect(response.encodeRedirectURL(location.toString()), directoryRedirectStatusCode); |
| } |
| |
| /** |
| * Parse the content-range header. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * |
| * @return the partial content-range, {@code null} if the content-range header was invalid or {@code #IGNORE} if |
| * there is no header to process |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected ContentRange parseContentRange(HttpServletRequest request, HttpServletResponse response) |
| throws IOException { |
| |
| // Retrieving the content-range header (if any is specified |
| String contentRangeHeader = request.getHeader("Content-Range"); |
| |
| if (contentRangeHeader == null) { |
| return IGNORE; |
| } |
| |
| if (!allowPartialPut) { |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return null; |
| } |
| |
| ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader)); |
| |
| if (contentRange == null) { |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return null; |
| } |
| // bytes is the only range unit supported |
| if (!"bytes".equals(contentRange.getUnits())) { |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return null; |
| } |
| |
| return contentRange; |
| } |
| |
| |
| /** |
| * Parse the range header. |
| * <p> |
| * The caller is required to have confirmed that the requested resource exists and is a file before calling this |
| * method. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return a list of ranges, {@code null} if the range header was invalid or {@code #FULL} if the Range header |
| * should be ignored. |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected Ranges parseRange(HttpServletRequest request, HttpServletResponse response, WebResource resource) |
| throws IOException { |
| |
| // Retrieving the range header (if any is specified) |
| String rangeHeader = request.getHeader("Range"); |
| |
| if (rangeHeader == null) { |
| // No Range header is the same as ignoring any Range header |
| return FULL; |
| } |
| |
| if (!Method.GET.equals(request.getMethod()) || !isRangeRequestsSupported()) { |
| // RFC 9110 - Section 14.2: GET is the only method for which range handling is defined. |
| // Otherwise MUST ignore a Range header field |
| return FULL; |
| } |
| |
| // Evaluate If-Range |
| if (!checkIfRange(request, response, resource)) { |
| if (response.isCommitted()) { |
| /* |
| * Ideally, checkIfRange() would be changed to return Boolean so the three states (satisfied, |
| * unsatisfied and error) could each be communicated via the return value. There isn't a backwards |
| * compatible way to do that that doesn't involve changing the method name and there are benefits to |
| * retaining the consistency of the existing method name pattern. Hence, this 'trick'. For the error |
| * state, checkIfRange() will call response.sendError() which will commit the response which this method |
| * can then detect. |
| */ |
| return null; |
| } |
| // No error but If-Range not satisfied |
| return FULL; |
| } |
| |
| long fileLength = resource.getContentLength(); |
| |
| if (fileLength == 0) { |
| // Range header makes no sense for a zero length resource. Tomcat |
| // therefore opts to ignore it. |
| return FULL; |
| } |
| |
| |
| Ranges ranges = Ranges.parse(new StringReader(rangeHeader)); |
| |
| if (ranges == null) { |
| // The Range header is present but not formatted correctly. |
| // Could argue for a 400 response but 416 is more specific. |
| // There is also the option to ignore the (invalid) Range header. |
| // RFC7233#4.4 notes that many servers do ignore the Range header in |
| // these circumstances but Tomcat has always returned a 416. |
| response.addHeader("Content-Range", "bytes */" + fileLength); |
| response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); |
| return null; |
| } |
| |
| // bytes is the only range unit supported (and I don't see the point |
| // of adding new ones). |
| if (!ranges.getUnits().equals("bytes")) { |
| // RFC7233#3.1 Servers must ignore range units they don't understand |
| return FULL; |
| } |
| |
| if (!validate(ranges, fileLength)) { |
| response.addHeader("Content-Range", "bytes */" + fileLength); |
| response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); |
| return null; |
| } |
| |
| return ranges; |
| } |
| |
| |
| /** |
| * Decide which way to render. HTML or XML. |
| * |
| * @param request The HttpServletRequest being served |
| * @param contextPath The path |
| * @param resource The resource |
| * @param encoding The encoding to use to process the readme (if any) |
| * |
| * @return the input stream with the rendered output |
| * |
| * @throws IOException an IO error occurred |
| * @throws ServletException rendering error |
| */ |
| protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding) |
| throws IOException, ServletException { |
| |
| Source xsltSource = findXsltSource(resource); |
| |
| if (xsltSource == null) { |
| return renderHtml(request, contextPath, resource, encoding); |
| } |
| return renderXml(request, contextPath, resource, xsltSource, encoding); |
| } |
| |
| |
| /** |
| * Return an InputStream to an XML representation of the contents this directory. |
| * |
| * @param request The HttpServletRequest being served |
| * @param contextPath Context path to which our internal paths are relative |
| * @param resource The associated resource |
| * @param xsltSource The XSL stylesheet |
| * @param encoding The encoding to use to process the readme (if any) |
| * |
| * @return the XML data |
| * |
| * @throws IOException an IO error occurred |
| * @throws ServletException rendering error |
| */ |
| protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource, |
| Source xsltSource, String encoding) throws IOException, ServletException { |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| sb.append("<?xml version=\"1.0\"?>"); |
| sb.append("<listing "); |
| sb.append(" contextPath='"); |
| sb.append(contextPath); |
| sb.append('\''); |
| sb.append(" directory='"); |
| sb.append(resource.getName()); |
| sb.append("' "); |
| sb.append(" hasParent='").append(!resource.getName().equals("/")); |
| sb.append("'>"); |
| |
| sb.append("<entries>"); |
| |
| String[] entries = resources.list(resource.getWebappPath()); |
| |
| // rewriteUrl(contextPath) is expensive. cache result for later reuse |
| String rewrittenContextPath = rewriteUrl(contextPath); |
| String directoryWebappPath = resource.getWebappPath(); |
| |
| for (String entry : entries) { |
| |
| if (entry.equalsIgnoreCase("WEB-INF") || entry.equalsIgnoreCase("META-INF") || |
| entry.equalsIgnoreCase(localXsltFile)) { |
| continue; |
| } |
| |
| if ((directoryWebappPath + entry).equals(contextXsltFile)) { |
| continue; |
| } |
| |
| WebResource childResource = resources.getResource(directoryWebappPath + entry); |
| if (!childResource.exists()) { |
| continue; |
| } |
| |
| sb.append("<entry"); |
| sb.append(" type='").append(childResource.isDirectory() ? "dir" : "file").append('\''); |
| sb.append(" urlPath='").append(rewrittenContextPath) |
| .append(Escape.xml(rewriteUrl(directoryWebappPath + entry))) |
| .append(childResource.isDirectory() ? "/" : "").append('\''); |
| if (childResource.isFile()) { |
| sb.append(" size='").append(renderSize(childResource.getContentLength())).append('\''); |
| } |
| sb.append(" date='").append(childResource.getLastModifiedHttp()).append('\''); |
| sb.append(" longDate='").append(childResource.getLastModified()).append('\''); |
| |
| sb.append('>'); |
| sb.append(Escape.htmlElementContent(entry)); |
| if (childResource.isDirectory()) { |
| sb.append('/'); |
| } |
| sb.append("</entry>"); |
| } |
| sb.append("</entries>"); |
| |
| String readme = getReadme(resource, encoding); |
| |
| if (readme != null) { |
| sb.append("<readme><![CDATA["); |
| sb.append(readme); |
| sb.append("]]></readme>"); |
| } |
| |
| sb.append("</listing>"); |
| |
| // Prevent possible memory leak. Ensure Transformer and |
| // TransformerFactory are not loaded from the web application. |
| Thread currentThread = Thread.currentThread(); |
| ClassLoader original = currentThread.getContextClassLoader(); |
| try { |
| currentThread.setContextClassLoader(DefaultServlet.class.getClassLoader()); |
| |
| TransformerFactory tFactory = TransformerFactory.newInstance(); |
| Source xmlSource = new StreamSource(new StringReader(sb.toString())); |
| Transformer transformer = tFactory.newTransformer(xsltSource); |
| |
| ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
| OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8); |
| StreamResult out = new StreamResult(osWriter); |
| transformer.transform(xmlSource, out); |
| osWriter.flush(); |
| return new ByteArrayInputStream(stream.toByteArray()); |
| } catch (TransformerException e) { |
| throw new ServletException(sm.getString("defaultServlet.xslError"), e); |
| } finally { |
| currentThread.setContextClassLoader(original); |
| } |
| } |
| |
| /** |
| * Return an InputStream to an HTML representation of the contents of this directory. |
| * |
| * @param request The HttpServletRequest being served |
| * @param contextPath Context path to which our internal paths are relative |
| * @param resource The associated resource |
| * @param encoding The encoding to use to process the readme (if any) |
| * |
| * @return the HTML data |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource, |
| String encoding) throws IOException { |
| |
| // Prepare a writer to a buffered area |
| ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
| OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8); |
| PrintWriter writer = new PrintWriter(osWriter); |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| // Get the right strings |
| StringManager sm = StringManager.getManager(DefaultServlet.class.getPackageName(), request.getLocales()); |
| |
| String directoryWebappPath = resource.getWebappPath(); |
| WebResource[] entries = resources.listResources(directoryWebappPath); |
| |
| // rewriteUrl(contextPath) is expensive. cache result for later reuse |
| String rewrittenContextPath = rewriteUrl(contextPath); |
| |
| // Render the page header |
| sb.append("<!doctype html>\r\n"); |
| sb.append("<html lang=\"").append(sm.getLocale().getLanguage()).append("\">\r\n"); |
| sb.append("<head>\r\n"); |
| sb.append("<title>"); |
| sb.append(sm.getString("defaultServlet.directory.title", directoryWebappPath)); |
| sb.append("</title>\r\n"); |
| sb.append("<style>"); |
| sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS); |
| sb.append("</style>\r\n"); |
| sb.append("</head>\r\n"); |
| sb.append("<body>\r\n"); |
| sb.append("<h1>"); |
| sb.append(sm.getString("defaultServlet.directory.title", directoryWebappPath)); |
| |
| // Render the link to our parent (if required) |
| String parentDirectory = directoryWebappPath; |
| if (parentDirectory.endsWith("/")) { |
| parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1); |
| } |
| int slash = parentDirectory.lastIndexOf('/'); |
| if (slash >= 0) { |
| String parent = directoryWebappPath.substring(0, slash); |
| sb.append(" \u2013 <a href=\""); |
| sb.append(rewrittenContextPath); |
| if (parent.isEmpty()) { |
| parent = "/"; |
| } |
| sb.append(rewriteUrl(parent)); |
| if (!parent.endsWith("/")) { |
| sb.append('/'); |
| } |
| sb.append("\">"); |
| sb.append("<b>"); |
| sb.append(sm.getString("defaultServlet.directory.parent", parent)); |
| sb.append("</b>"); |
| sb.append("</a>"); |
| } |
| |
| sb.append("</h1>\r\n"); |
| sb.append("<hr class=\"line\">\r\n"); |
| |
| sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n"); |
| |
| SortManager.Order order; |
| if (sortListings) { |
| order = sortManager.getOrder(request.getQueryString()); |
| } else { |
| order = null; |
| } |
| // Render the column headings |
| sb.append("<thead>\r\n"); |
| sb.append("<tr>\r\n"); |
| sb.append("<th align=\"left\"><font size=\"+1\"><strong>"); |
| if (order != null) { |
| sb.append("<a href=\"?C=N;O="); |
| sb.append(getOrderChar(order, 'N')); |
| sb.append("\">"); |
| sb.append(sm.getString("defaultServlet.resource.name")); |
| sb.append("</a>"); |
| } else { |
| sb.append(sm.getString("defaultServlet.resource.name")); |
| } |
| sb.append("</strong></font></th>\r\n"); |
| sb.append("<th align=\"center\"><font size=\"+1\"><strong>"); |
| if (order != null) { |
| sb.append("<a href=\"?C=S;O="); |
| sb.append(getOrderChar(order, 'S')); |
| sb.append("\">"); |
| sb.append(sm.getString("defaultServlet.resource.size")); |
| sb.append("</a>"); |
| } else { |
| sb.append(sm.getString("defaultServlet.resource.size")); |
| } |
| sb.append("</strong></font></th>\r\n"); |
| sb.append("<th align=\"right\"><font size=\"+1\"><strong>"); |
| if (order != null) { |
| sb.append("<a href=\"?C=M;O="); |
| sb.append(getOrderChar(order, 'M')); |
| sb.append("\">"); |
| sb.append(sm.getString("defaultServlet.resource.lastModified")); |
| sb.append("</a>"); |
| } else { |
| sb.append(sm.getString("defaultServlet.resource.lastModified")); |
| } |
| sb.append("</strong></font></th>\r\n"); |
| sb.append("</tr>\r\n"); |
| sb.append("</thead>\r\n"); |
| |
| if (null != sortManager) { |
| sortManager.sort(entries, request.getQueryString()); |
| } |
| |
| boolean shade = false; |
| sb.append("<tbody>\r\n"); |
| for (WebResource childResource : entries) { |
| String filename = childResource.getName(); |
| if (filename.equalsIgnoreCase("WEB-INF") || filename.equalsIgnoreCase("META-INF")) { |
| continue; |
| } |
| |
| if (!childResource.exists()) { |
| continue; |
| } |
| |
| sb.append("<tr"); |
| if (shade) { |
| sb.append(" bgcolor=\"#eeeeee\""); |
| } |
| sb.append(">\r\n"); |
| shade = !shade; |
| |
| sb.append("<td align=\"left\"> \r\n"); |
| sb.append("<a href=\""); |
| sb.append(rewrittenContextPath); |
| sb.append(rewriteUrl(childResource.getWebappPath())); |
| if (childResource.isDirectory()) { |
| sb.append('/'); |
| } |
| sb.append("\"><tt>"); |
| sb.append(Escape.htmlElementContent(filename)); |
| if (childResource.isDirectory()) { |
| sb.append('/'); |
| } |
| sb.append("</tt></a></td>\r\n"); |
| |
| sb.append("<td align=\"right\"><tt>"); |
| if (childResource.isDirectory()) { |
| sb.append(" "); |
| } else { |
| sb.append(renderSize(childResource.getContentLength())); |
| } |
| sb.append("</tt></td>\r\n"); |
| |
| sb.append("<td align=\"right\"><tt>"); |
| sb.append(renderTimestamp(childResource.getLastModified())); |
| sb.append("</tt></td>\r\n"); |
| |
| sb.append("</tr>\r\n"); |
| } |
| sb.append("</tbody>\r\n"); |
| |
| // Render the page footer |
| sb.append("</table>\r\n"); |
| |
| sb.append("<hr class=\"line\">\r\n"); |
| |
| String readme = getReadme(resource, encoding); |
| if (readme != null) { |
| sb.append(readme); |
| sb.append("<hr class=\"line\">\r\n"); |
| } |
| |
| if (showServerInfo) { |
| sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>\r\n"); |
| } |
| sb.append("</body>\r\n"); |
| sb.append("</html>\r\n"); |
| |
| // Return an input stream to the underlying bytes |
| writer.write(sb.toString()); |
| writer.flush(); |
| return new ByteArrayInputStream(stream.toByteArray()); |
| |
| } |
| |
| |
| /** |
| * Render the specified file size (in bytes). |
| * |
| * @param size File size (in bytes) |
| * |
| * @return the formatted size |
| */ |
| protected String renderSize(long size) { |
| |
| long leftSide = size / 1024; |
| long rightSide = (size % 1024) / 103; // Makes 1 digit |
| if ((leftSide == 0) && (rightSide == 0) && (size > 0)) { |
| rightSide = 1; |
| } |
| |
| return (String.valueOf(leftSide) + "." + String.valueOf(rightSide) + " KiB"); |
| |
| } |
| |
| |
| /** |
| * Render the specified file timestamp. |
| * |
| * @param timestamp File timestamp |
| * |
| * @return the formatted timestamp |
| */ |
| protected String renderTimestamp(long timestamp) { |
| |
| return FastHttpDateFormat.formatDate(timestamp); |
| |
| } |
| |
| |
| /** |
| * Get the readme file as a string. |
| * |
| * @param directory The directory to search |
| * @param encoding The readme encoding |
| * |
| * @return the readme for the specified directory |
| */ |
| protected String getReadme(WebResource directory, String encoding) { |
| |
| if (readmeFile != null) { |
| WebResource resource = resources.getResource(directory.getWebappPath() + readmeFile); |
| if (resource.isFile()) { |
| StringWriter buffer = new StringWriter(); |
| InputStreamReader reader = null; |
| try (InputStream is = resource.getInputStream()) { |
| if (encoding != null) { |
| reader = new InputStreamReader(is, encoding); |
| } else { |
| reader = new InputStreamReader(is); |
| } |
| IOException e = copyRange(reader, new PrintWriter(buffer)); |
| if (debug > 10) { |
| log("readme '" + readmeFile + "' output error: " + ((e != null) ? e.getMessage() : "")); |
| } |
| } catch (IOException ioe) { |
| log(sm.getString("defaultServlet.readerCloseFailed"), ioe); |
| } finally { |
| if (reader != null) { |
| try { |
| reader.close(); |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| } |
| } |
| return buffer.toString(); |
| } else { |
| if (debug > 10) { |
| log("readme '" + readmeFile + "' not found"); |
| } |
| |
| return null; |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| /** |
| * Return a Source for the xsl template (if possible). |
| * |
| * @param directory The directory to search |
| * |
| * @return the source for the specified directory |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected Source findXsltSource(WebResource directory) throws IOException { |
| |
| if (localXsltFile != null) { |
| WebResource resource = resources.getResource(directory.getWebappPath() + localXsltFile); |
| if (resource.isFile()) { |
| InputStream is = resource.getInputStream(); |
| if (is != null) { |
| return new StreamSource(is); |
| } |
| } |
| if (debug > 10) { |
| log("localXsltFile '" + localXsltFile + "' not found"); |
| } |
| } |
| |
| if (contextXsltFile != null) { |
| InputStream is = getServletContext().getResourceAsStream(contextXsltFile); |
| if (is != null) { |
| return new StreamSource(is); |
| } |
| |
| if (debug > 10) { |
| log("contextXsltFile '" + contextXsltFile + "' not found"); |
| } |
| } |
| |
| /* |
| * Open and read in file in one fell swoop to reduce the chance of leaving handle open. |
| */ |
| if (globalXsltFile != null) { |
| File f = validateGlobalXsltFile(); |
| if (f != null) { |
| long globalXsltFileSize = f.length(); |
| if (globalXsltFileSize > Integer.MAX_VALUE) { |
| log(sm.getString("defaultServlet.globalXSLTTooBig", f.getAbsolutePath())); |
| } else { |
| try (FileInputStream fis = new FileInputStream(f)) { |
| byte[] b = new byte[(int) f.length()]; |
| IOTools.readFully(fis, b); |
| return new StreamSource(new ByteArrayInputStream(b)); |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| private File validateGlobalXsltFile() { |
| Context context = resources.getContext(); |
| |
| File baseConf = new File(context.getCatalinaBase(), "conf"); |
| File result = validateGlobalXsltFile(baseConf); |
| if (result == null) { |
| File homeConf = new File(context.getCatalinaHome(), "conf"); |
| if (!baseConf.equals(homeConf)) { |
| result = validateGlobalXsltFile(homeConf); |
| } |
| } |
| |
| return result; |
| } |
| |
| |
| private File validateGlobalXsltFile(File base) { |
| File candidate = new File(globalXsltFile); |
| if (!candidate.isAbsolute()) { |
| candidate = new File(base, globalXsltFile); |
| } |
| |
| if (!candidate.isFile()) { |
| return null; |
| } |
| |
| // First check that the resulting path is under the provided base |
| try { |
| if (!candidate.getCanonicalFile().toPath().startsWith(base.getCanonicalFile().toPath())) { |
| return null; |
| } |
| } catch (IOException ioe) { |
| return null; |
| } |
| |
| // Next check that an .xsl or .xslt file has been specified |
| String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH); |
| if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) { |
| return null; |
| } |
| |
| return candidate; |
| } |
| |
| |
| // -------------------------------------------------------- protected Methods |
| |
| /** |
| * Check if sendfile can be used. |
| * |
| * @param request The Servlet request |
| * @param response The Servlet response |
| * @param resource The resource |
| * @param length The length which will be written (will be used only if range is null) |
| * @param range The range that will be written |
| * |
| * @return <code>true</code> if sendfile should be used (writing is then delegated to the endpoint) |
| */ |
| protected boolean checkSendfile(HttpServletRequest request, HttpServletResponse response, WebResource resource, |
| long length, Ranges.Entry range) { |
| String canonicalPath; |
| if (sendfileSize > 0 && length > sendfileSize && |
| (Boolean.TRUE.equals(request.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR))) && |
| (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade")) && |
| (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade")) && |
| resource.isFile() && ((canonicalPath = resource.getCanonicalPath()) != null)) { |
| request.setAttribute(Globals.SENDFILE_FILENAME_ATTR, canonicalPath); |
| if (range == null) { |
| request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0L)); |
| request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(length)); |
| } else { |
| request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(getStart(range, length))); |
| request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(getEnd(range, length) + 1)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Check if the if-match condition is satisfied. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition |
| * is not satisfied, in which case request processing is stopped |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource) |
| throws IOException { |
| |
| boolean conditionSatisfied = false; |
| Enumeration<String> headerValues = request.getHeaders("If-Match"); |
| String resourceETag = generateETag(resource); |
| |
| boolean hasAsteriskValue = false;// check existence of special header value '*' |
| int headerCount = 0; |
| while (headerValues.hasMoreElements() && !conditionSatisfied) { |
| headerCount++; |
| String headerValue = headerValues.nextElement(); |
| if ("*".equals(headerValue)) { |
| hasAsteriskValue = true; |
| if (resourceETag != null) { |
| conditionSatisfied = true; |
| } |
| } else { |
| // RFC 7232 requires strong comparison for If-Match headers |
| Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), false, resourceETag); |
| if (matched == null) { |
| if (debug > 10) { |
| log("DefaultServlet.checkIfMatch: Invalid header value [" + headerValue + "]"); |
| } |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } else { |
| conditionSatisfied = matched.booleanValue(); |
| } |
| } |
| } |
| if (headerValues.hasMoreElements()) { |
| headerCount++; |
| } |
| |
| if (hasAsteriskValue && headerCount > 1) { |
| // Note that an If-Match header field with a list value containing "*" and other values (including other |
| // instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore is |
| // unlikely to be interoperable. |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| if (!conditionSatisfied) { |
| response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); |
| return false; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Check if the if-modified-since condition is satisfied. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition |
| * is not satisfied, in which case request processing is stopped |
| */ |
| protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response, |
| WebResource resource) { |
| |
| String method = request.getMethod(); |
| if (!Method.GET.equals(method) && !Method.HEAD.equals(method)) { |
| return true; |
| } |
| |
| long resourceLastModified = resource.getLastModified(); |
| if (resourceLastModified <= 0) { |
| // MUST ignore if the resource does not have a modification date available. |
| return true; |
| } |
| |
| // Must be at least one header for this method to be called |
| Enumeration<String> headerEnum = request.getHeaders("If-Modified-Since"); |
| headerEnum.nextElement(); |
| if (headerEnum.hasMoreElements()) { |
| // If-Modified-Since is a list of dates |
| return true; |
| } |
| |
| try { |
| // Header is present so -1 will be not returned. Only a valid date or an IAE are possible. |
| long headerValue = request.getDateHeader("If-Modified-Since"); |
| if (resourceLastModified < (headerValue + 1000)) { |
| // The entity has not been modified since the date |
| // specified by the client. This is not an error case. |
| response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); |
| response.setHeader("ETag", generateETag(resource)); |
| return false; |
| } |
| } catch (IllegalArgumentException illegalArgument) { |
| return true; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Check if the if-none-match condition is satisfied. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition |
| * is not satisfied, in which case request processing is stopped |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource) |
| throws IOException { |
| |
| String resourceETag = generateETag(resource); |
| |
| Enumeration<String> headerValues = request.getHeaders("If-None-Match"); |
| boolean hasAsteriskValue = false;// check existence of special header value '*' |
| boolean conditionSatisfied = true; |
| int headerCount = 0; |
| while (headerValues.hasMoreElements()) { |
| headerCount++; |
| String headerValue = headerValues.nextElement(); |
| |
| if (headerValue.equals("*")) { |
| hasAsteriskValue = true; |
| if (headerCount > 1 || headerValues.hasMoreElements()) { |
| conditionSatisfied = false; |
| } else { |
| // asterisk '*' is the only field value. |
| // RFC9110: If the field value is "*", the condition is false if the origin server has a current |
| // representation for the target resource. |
| if (resourceETag != null) { |
| conditionSatisfied = false; |
| } |
| } |
| break; |
| } else { |
| // RFC 7232 requires weak comparison for If-None-Match headers |
| Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), true, resourceETag); |
| if (matched == null) { |
| if (debug > 10) { |
| log("DefaultServlet.checkIfNoneMatch: Invalid header value [" + headerValue + "]"); |
| } |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| if (matched.booleanValue()) { |
| // RFC9110: If the field value is a list of entity tags, the condition is false if one of the |
| // listed tags |
| // matches the entity tag of the selected representation. |
| conditionSatisfied = false; |
| break; |
| } |
| } |
| } |
| if (headerValues.hasMoreElements()) { |
| headerCount++; |
| } |
| |
| if (hasAsteriskValue && headerCount > 1) { |
| // Note that an If-None-Match header field with a list value containing "*" and other values (including |
| // other instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore |
| // is unlikely to be interoperable. |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| if (!conditionSatisfied) { |
| // For GET and HEAD, we should respond with |
| // 304 Not Modified. |
| // For every other method, 412 Precondition Failed is sent |
| // back. |
| if (Method.GET.equals(request.getMethod()) || Method.HEAD.equals(request.getMethod())) { |
| response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); |
| response.setHeader("ETag", resourceETag); |
| } else { |
| response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Check if the if-unmodified-since condition is satisfied. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition |
| * is not satisfied, in which case request processing is stopped |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected boolean checkIfUnmodifiedSince(HttpServletRequest request, HttpServletResponse response, |
| WebResource resource) throws IOException { |
| |
| long resourceLastModified = resource.getLastModified(); |
| if (resourceLastModified <= 0) { |
| // MUST ignore if the resource does not have a modification date available. |
| return true; |
| } |
| // Must be at least one header for this method to be called |
| Enumeration<String> headerEnum = request.getHeaders("If-Unmodified-Since"); |
| headerEnum.nextElement(); |
| if (headerEnum.hasMoreElements()) { |
| // If-Unmodified-Since is a list of dates |
| return true; |
| } |
| |
| try { |
| // Header is present so -1 will be not returned. Only a valid date or an IAE are possible. |
| long headerValue = request.getDateHeader("If-Unmodified-Since"); |
| if (resourceLastModified >= (headerValue + 1000)) { |
| // The entity has not been modified since the date |
| // specified by the client. This is not an error case. |
| response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); |
| return false; |
| } |
| } catch (IllegalArgumentException illegalArgument) { |
| return true; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Check if the if-range condition is satisfied. The calling method is required to ensure a Range header is present |
| * and that Range requests are supported for the current resource. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @param resource The resource |
| * |
| * @return {@code true} if the resource meets the specified condition, and {@code false} if the condition is not |
| * satisfied, resulting in transfer of the new selected representation instead of a 412 (Precondition |
| * Failed) response. If the if-range condition is not valid then an appropriate status code will be set, |
| * the response will be committed and this method will return {@code false} |
| * |
| * @throws IOException an IO error occurred |
| */ |
| protected boolean checkIfRange(HttpServletRequest request, HttpServletResponse response, WebResource resource) |
| throws IOException { |
| String resourceETag = generateETag(resource); |
| long resourceLastModified = resource.getLastModified(); |
| |
| Enumeration<String> headerEnum = request.getHeaders("If-Range"); |
| if (!headerEnum.hasMoreElements()) { |
| // If-Range is not present |
| return true; |
| } |
| String headerValue = headerEnum.nextElement().trim(); |
| if (headerEnum.hasMoreElements()) { |
| // Multiple If-Range headers |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| |
| if (headerValue.length() > 2 && (headerValue.charAt(0) == '"' || headerValue.charAt(2) == '"')) { |
| boolean weakETag = headerValue.startsWith("W/\""); |
| if ((!weakETag && headerValue.charAt(0) != '"') || headerValue.charAt(headerValue.length() - 1) != '"' || |
| headerValue.indexOf('"', weakETag ? 3 : 1) != headerValue.length() - 1) { |
| // Not a single entity tag |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| // If the ETag the client gave does not match the entity |
| // etag, then the entire entity is returned. |
| return !weakETag && resourceETag != null && resourceETag.equals(headerValue); |
| } else { |
| long headerValueTime = -1L; |
| try { |
| headerValueTime = request.getDateHeader("If-Range"); |
| } catch (IllegalArgumentException ignore) { |
| // Ignore |
| } |
| if (headerValueTime >= 0) { |
| // unit of HTTP date is second, ignore millisecond part. |
| return resourceLastModified >= headerValueTime && resourceLastModified < headerValueTime + 1000; |
| } else { |
| // Not a single entity tag and not a valid date either |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| return false; |
| } |
| } |
| |
| } |
| |
| /** |
| * Checks if range request is supported by server |
| * |
| * @return <code>true</code> server supports range requests feature. |
| */ |
| protected boolean isRangeRequestsSupported() { |
| // Range-Requests optional feature is enabled implicitly. |
| return true; |
| } |
| |
| /** |
| * Provides the entity tag (the ETag header) for the given resource. Intended to be over-ridden by custom |
| * DefaultServlet implementations that wish to use an alternative format for the entity tag. |
| * |
| * @param resource The resource for which an entity tag is required. |
| * |
| * @return The result of calling {@link WebResource#getETag()} on the given resource |
| */ |
| protected String generateETag(WebResource resource) { |
| if (useStrongETags) { |
| return resource.getStrongETag(); |
| } else { |
| return resource.getETag(); |
| } |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param is The input stream to read the source resource from |
| * @param ostream The output stream to write to |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| protected void copy(InputStream is, ServletOutputStream ostream) throws IOException { |
| |
| InputStream istream = new BufferedInputStream(is, input); |
| |
| // Copy the input stream to the output stream |
| IOException exception = copyRange(istream, ostream); |
| |
| // Clean up the input stream |
| istream.close(); |
| |
| // Rethrow any exception that has occurred |
| if (exception != null) { |
| throw exception; |
| } |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param is The input stream to read the source resource from |
| * @param writer The writer to write to |
| * @param encoding The encoding to use when reading the source input stream |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| protected void copy(InputStream is, PrintWriter writer, String encoding) throws IOException { |
| |
| Reader reader; |
| if (encoding == null) { |
| reader = new InputStreamReader(is); |
| } else { |
| reader = new InputStreamReader(is, encoding); |
| } |
| |
| // Copy the input stream to the output stream |
| IOException exception = copyRange(reader, writer); |
| |
| // Clean up the reader |
| reader.close(); |
| |
| // Rethrow any exception that has occurred |
| if (exception != null) { |
| throw exception; |
| } |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param resource The source resource |
| * @param length the resource length |
| * @param ostream The output stream to write to |
| * @param range Range the client wanted to retrieve |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges.Entry range) |
| throws IOException { |
| |
| InputStream resourceInputStream = resource.getInputStream(); |
| InputStream istream = new BufferedInputStream(resourceInputStream, input); |
| IOException exception = copyRange(istream, ostream, getStart(range, length), getEnd(range, length)); |
| |
| // Clean up the input stream |
| istream.close(); |
| |
| // Rethrow any exception that has occurred |
| if (exception != null) { |
| throw exception; |
| } |
| |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param resource The source resource |
| * @param length the resource length |
| * @param ostream The output stream to write to |
| * @param ranges Enumeration of the ranges the client wanted to retrieve |
| * @param contentType Content type of the resource |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges ranges, |
| String contentType) throws IOException { |
| |
| IOException exception = null; |
| |
| for (Ranges.Entry range : ranges.getEntries()) { |
| if (exception != null) { |
| break; |
| } |
| InputStream resourceInputStream = resource.getInputStream(); |
| try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) { |
| |
| // Writing MIME header. |
| ostream.println(); |
| ostream.println("--" + mimeSeparation); |
| if (contentType != null) { |
| ostream.println("Content-Type: " + contentType); |
| } |
| long start = getStart(range, length); |
| long end = getEnd(range, length); |
| ostream.println("Content-Range: bytes " + start + "-" + end + "/" + length); |
| ostream.println(); |
| |
| // Printing content |
| exception = copyRange(istream, ostream, start, end); |
| } |
| } |
| |
| ostream.println(); |
| ostream.print("--" + mimeSeparation + "--"); |
| |
| // Rethrow any exception that has occurred |
| if (exception != null) { |
| throw exception; |
| } |
| |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param istream The input stream to read from |
| * @param ostream The output stream to write to |
| * |
| * @return Exception which occurred during processing |
| */ |
| protected IOException copyRange(InputStream istream, ServletOutputStream ostream) { |
| |
| // Copy the input stream to the output stream |
| IOException exception = null; |
| byte[] buffer = new byte[input]; |
| while (true) { |
| try { |
| int len = istream.read(buffer); |
| if (len == -1) { |
| break; |
| } |
| ostream.write(buffer, 0, len); |
| } catch (IOException ioe) { |
| exception = ioe; |
| break; |
| } |
| } |
| return exception; |
| |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param reader The reader to read from |
| * @param writer The writer to write to |
| * |
| * @return Exception which occurred during processing |
| */ |
| protected IOException copyRange(Reader reader, PrintWriter writer) { |
| |
| // Copy the input stream to the output stream |
| IOException exception = null; |
| char[] buffer = new char[input]; |
| while (true) { |
| try { |
| int len = reader.read(buffer); |
| if (len == -1) { |
| break; |
| } |
| writer.write(buffer, 0, len); |
| } catch (IOException ioe) { |
| exception = ioe; |
| break; |
| } |
| } |
| return exception; |
| |
| } |
| |
| |
| /** |
| * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are |
| * closed before returning (even in the face of an exception). |
| * |
| * @param istream The input stream to read from |
| * @param ostream The output stream to write to |
| * @param start Start of the range which will be copied |
| * @param end End of the range which will be copied |
| * |
| * @return Exception which occurred during processing |
| */ |
| protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) { |
| |
| if (debug > 10) { |
| log("Serving bytes: " + start + "-" + end); |
| } |
| |
| long skipped; |
| try { |
| skipped = istream.skip(start); |
| } catch (IOException ioe) { |
| return ioe; |
| } |
| if (skipped < start) { |
| return new IOException(sm.getString("defaultServlet.skipfail", Long.valueOf(skipped), Long.valueOf(start))); |
| } |
| |
| IOException exception = null; |
| long bytesToRead = end - start + 1; |
| |
| byte[] buffer = new byte[input]; |
| int len = buffer.length; |
| while ((bytesToRead > 0) && (len >= buffer.length)) { |
| try { |
| len = istream.read(buffer); |
| if (bytesToRead >= len) { |
| ostream.write(buffer, 0, len); |
| bytesToRead -= len; |
| } else { |
| ostream.write(buffer, 0, (int) bytesToRead); |
| bytesToRead = 0; |
| } |
| } catch (IOException ioe) { |
| exception = ioe; |
| len = -1; |
| } |
| } |
| |
| return exception; |
| |
| } |
| |
| |
| protected record CompressionFormat(String extension, String encoding) implements Serializable { |
| @Serial |
| private static final long serialVersionUID = 1L; |
| } |
| |
| |
| private record PrecompressedResource(WebResource resource, CompressionFormat format) { |
| } |
| |
| |
| /** |
| * Gets the ordering character to be used for a particular column. |
| * |
| * @param order The order that is currently being applied |
| * @param column The column that will be rendered. |
| * |
| * @return Either 'A' or 'D', to indicate "ascending" or "descending" sort order. |
| */ |
| private char getOrderChar(SortManager.Order order, char column) { |
| if (column == order.column) { |
| if (order.ascending) { |
| return 'D'; |
| } else { |
| return 'A'; |
| } |
| } else { |
| return 'D'; |
| } |
| } |
| |
| |
| /** |
| * A class encapsulating the sorting of resources. |
| */ |
| protected static class SortManager { |
| /** |
| * The default sort. |
| */ |
| protected Comparator<WebResource> defaultResourceComparator; |
| |
| /** |
| * Comparator to use when sorting resources by name. |
| */ |
| protected Comparator<WebResource> resourceNameComparator; |
| |
| /** |
| * Comparator to use when sorting files by name, ascending (reverse). |
| */ |
| protected Comparator<WebResource> resourceNameComparatorAsc; |
| |
| /** |
| * Comparator to use when sorting resources by size. |
| */ |
| protected Comparator<WebResource> resourceSizeComparator; |
| |
| /** |
| * Comparator to use when sorting files by size, ascending (reverse). |
| */ |
| protected Comparator<WebResource> resourceSizeComparatorAsc; |
| |
| /** |
| * Comparator to use when sorting resources by last-modified date. |
| */ |
| protected Comparator<WebResource> resourceLastModifiedComparator; |
| |
| /** |
| * Comparator to use when sorting files by last-modified date, ascending (reverse). |
| */ |
| protected Comparator<WebResource> resourceLastModifiedComparatorAsc; |
| |
| SortManager(boolean directoriesFirst) { |
| resourceNameComparator = Comparator.comparing(WebResource::getName); |
| resourceNameComparatorAsc = resourceNameComparator.reversed(); |
| resourceSizeComparator = |
| Comparator.comparing(WebResource::getContentLength).thenComparing(resourceNameComparator); |
| resourceSizeComparatorAsc = resourceSizeComparator.reversed(); |
| resourceLastModifiedComparator = |
| Comparator.comparing(WebResource::getLastModified).thenComparing(resourceNameComparator); |
| resourceLastModifiedComparatorAsc = resourceLastModifiedComparator.reversed(); |
| |
| if (directoriesFirst) { |
| Comparator<WebResource> dirsFirst = comparingTrueFirst(WebResource::isDirectory); |
| resourceNameComparator = dirsFirst.thenComparing(resourceNameComparator); |
| resourceNameComparatorAsc = dirsFirst.thenComparing(resourceNameComparatorAsc); |
| resourceSizeComparator = dirsFirst.thenComparing(resourceSizeComparator); |
| resourceSizeComparatorAsc = dirsFirst.thenComparing(resourceSizeComparatorAsc); |
| resourceLastModifiedComparator = dirsFirst.thenComparing(resourceLastModifiedComparator); |
| resourceLastModifiedComparatorAsc = dirsFirst.thenComparing(resourceLastModifiedComparatorAsc); |
| } |
| |
| defaultResourceComparator = resourceNameComparator; |
| } |
| |
| /** |
| * Sorts an array of resources according to an ordering string. |
| * |
| * @param resources The array to sort. |
| * @param order The ordering string. |
| * |
| * @see #getOrder(String) |
| */ |
| public void sort(WebResource[] resources, String order) { |
| Comparator<WebResource> comparator = getComparator(order); |
| |
| if (null != comparator) { |
| Arrays.sort(resources, comparator); |
| } |
| } |
| |
| public Comparator<WebResource> getComparator(String order) { |
| return getComparator(getOrder(order)); |
| } |
| |
| public Comparator<WebResource> getComparator(Order order) { |
| if (null == order) { |
| return defaultResourceComparator; |
| } |
| |
| if ('N' == order.column) { |
| if (order.ascending) { |
| return resourceNameComparatorAsc; |
| } else { |
| return resourceNameComparator; |
| } |
| } |
| |
| if ('S' == order.column) { |
| if (order.ascending) { |
| return resourceSizeComparatorAsc; |
| } else { |
| return resourceSizeComparator; |
| } |
| } |
| |
| if ('M' == order.column) { |
| if (order.ascending) { |
| return resourceLastModifiedComparatorAsc; |
| } else { |
| return resourceLastModifiedComparator; |
| } |
| } |
| |
| return defaultResourceComparator; |
| } |
| |
| /** |
| * Gets the Order to apply given an ordering-string. This ordering-string matches a subset of the |
| * ordering-strings supported by <a href="https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query">Apache |
| * httpd</a>. |
| * |
| * @param order The ordering-string provided by the client. |
| * |
| * @return An Order specifying the column and ascending/descending to be applied to resources. |
| */ |
| public Order getOrder(String order) { |
| if (null == order || order.trim().isEmpty()) { |
| return Order.DEFAULT; |
| } |
| |
| String[] options = order.split(";"); |
| |
| if (0 == options.length) { |
| return Order.DEFAULT; |
| } |
| |
| char column = '\0'; |
| boolean ascending = false; |
| |
| for (String option : options) { |
| option = option.trim(); |
| |
| if (2 < option.length()) { |
| char opt = option.charAt(0); |
| if ('C' == opt) { |
| column = option.charAt(2); |
| } else if ('O' == opt) { |
| ascending = ('A' == option.charAt(2)); |
| } |
| } |
| } |
| |
| if ('N' == column) { |
| if (ascending) { |
| return Order.NAME_ASC; |
| } else { |
| return Order.NAME; |
| } |
| } |
| |
| if ('S' == column) { |
| if (ascending) { |
| return Order.SIZE_ASC; |
| } else { |
| return Order.SIZE; |
| } |
| } |
| |
| if ('M' == column) { |
| if (ascending) { |
| return Order.LAST_MODIFIED_ASC; |
| } else { |
| return Order.LAST_MODIFIED; |
| } |
| } |
| |
| return Order.DEFAULT; |
| } |
| |
| public static class Order { |
| final char column; |
| final boolean ascending; |
| |
| Order(char column, boolean ascending) { |
| this.column = column; |
| this.ascending = ascending; |
| } |
| |
| public static final Order NAME = new Order('N', false); |
| public static final Order NAME_ASC = new Order('N', true); |
| public static final Order SIZE = new Order('S', false); |
| public static final Order SIZE_ASC = new Order('S', true); |
| public static final Order LAST_MODIFIED = new Order('M', false); |
| public static final Order LAST_MODIFIED_ASC = new Order('M', true); |
| |
| public static final Order DEFAULT = NAME; |
| } |
| } |
| |
| |
| private static Comparator<WebResource> comparingTrueFirst(Function<WebResource,Boolean> keyExtractor) { |
| return (s1, s2) -> { |
| Boolean r1 = keyExtractor.apply(s1); |
| Boolean r2 = keyExtractor.apply(s2); |
| if (r1.booleanValue()) { |
| if (r2.booleanValue()) { |
| return 0; |
| } else { |
| return -1; // r1 (property is true) first |
| } |
| } else if (r2.booleanValue()) { |
| return 1; // r2 (property is true) first |
| } else { |
| return 0; |
| } |
| }; |
| } |
| |
| |
| enum BomConfig { |
| /** |
| * BoM is stripped if present and any BoM found used to determine the encoding used to read the resource. |
| */ |
| TRUE("true", true, true), |
| /** |
| * BoM is stripped if present but the configured file encoding is used to read the resource. |
| */ |
| FALSE("false", true, false), |
| /** |
| * BoM is not stripped and the configured file encoding is used to read the resource. |
| */ |
| PASS_THROUGH("pass-through", false, false); |
| |
| final String configurationValue; |
| final boolean stripBom; |
| final boolean useBomEncoding; |
| |
| BomConfig(String configurationValue, boolean stripBom, boolean useBomEncoding) { |
| this.configurationValue = configurationValue; |
| this.stripBom = stripBom; |
| this.useBomEncoding = useBomEncoding; |
| } |
| } |
| } |