1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.catalina.servlets;
18
19 import java.io.BufferedInputStream;
20 import java.io.ByteArrayInputStream;
21 import java.io.ByteArrayOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.io.OutputStreamWriter;
29 import java.io.PrintWriter;
30 import java.io.RandomAccessFile;
31 import java.io.Reader;
32 import java.io.Serializable;
33 import java.io.StringReader;
34 import java.io.StringWriter;
35 import java.io.UnsupportedEncodingException;
36 import java.nio.charset.Charset;
37 import java.nio.charset.StandardCharsets;
38 import java.security.AccessController;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.Enumeration;
44 import java.util.Iterator;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.StringTokenizer;
48
49 import javax.servlet.DispatcherType;
50 import javax.servlet.RequestDispatcher;
51 import javax.servlet.ServletContext;
52 import javax.servlet.ServletException;
53 import javax.servlet.ServletOutputStream;
54 import javax.servlet.ServletResponse;
55 import javax.servlet.ServletResponseWrapper;
56 import javax.servlet.UnavailableException;
57 import javax.servlet.http.HttpServlet;
58 import javax.servlet.http.HttpServletRequest;
59 import javax.servlet.http.HttpServletResponse;
60 import javax.xml.parsers.DocumentBuilder;
61 import javax.xml.parsers.DocumentBuilderFactory;
62 import javax.xml.parsers.ParserConfigurationException;
63 import javax.xml.transform.Source;
64 import javax.xml.transform.Transformer;
65 import javax.xml.transform.TransformerException;
66 import javax.xml.transform.TransformerFactory;
67 import javax.xml.transform.dom.DOMSource;
68 import javax.xml.transform.stream.StreamResult;
69 import javax.xml.transform.stream.StreamSource;
70
71 import org.apache.catalina.Context;
72 import org.apache.catalina.Globals;
73 import org.apache.catalina.WebResource;
74 import org.apache.catalina.WebResourceRoot;
75 import org.apache.catalina.connector.RequestFacade;
76 import org.apache.catalina.connector.ResponseFacade;
77 import org.apache.catalina.util.IOTools;
78 import org.apache.catalina.util.ServerInfo;
79 import org.apache.catalina.util.URLEncoder;
80 import org.apache.catalina.webresources.CachedResource;
81 import org.apache.tomcat.util.buf.B2CConverter;
82 import org.apache.tomcat.util.http.ResponseUtil;
83 import org.apache.tomcat.util.http.parser.ContentRange;
84 import org.apache.tomcat.util.http.parser.Ranges;
85 import org.apache.tomcat.util.res.StringManager;
86 import org.apache.tomcat.util.security.Escape;
87 import org.apache.tomcat.util.security.PrivilegedGetTccl;
88 import org.apache.tomcat.util.security.PrivilegedSetTccl;
89 import org.w3c.dom.Document;
90 import org.xml.sax.InputSource;
91 import org.xml.sax.SAXException;
92 import org.xml.sax.ext.EntityResolver2;
93
94
95 /**
96  * <p>The default resource-serving servlet for most web applications,
97  * used to serve static resources such as HTML pages and images.
98  * </p>
99  * <p>
100  * This servlet is intended to be mapped to <em>/</em> e.g.:
101  * </p>
102  * <pre>
103  *   &lt;servlet-mapping&gt;
104  *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
105  *       &lt;url-pattern&gt;/&lt;/url-pattern&gt;
106  *   &lt;/servlet-mapping&gt;
107  * </pre>
108  * <p>It can be mapped to sub-paths, however in all cases resources are served
109  * from the web application resource root using the full path from the root
110  * of the web application context.
111  * <br>e.g. given a web application structure:
112  *</p>
113  * <pre>
114  * /context
115  *   /images
116  *     tomcat2.jpg
117  *   /static
118  *     /images
119  *       tomcat.jpg
120  * </pre>
121  * <p>
122  * ... and a servlet mapping that maps only <code>/static/*</code> to the default servlet:
123  * </p>
124  * <pre>
125  *   &lt;servlet-mapping&gt;
126  *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
127  *       &lt;url-pattern&gt;/static/*&lt;/url-pattern&gt;
128  *   &lt;/servlet-mapping&gt;
129  * </pre>
130  * <p>
131  * Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed
132  * while a request to <code>/context/images/tomcat2.jpg</code> will fail.
133  * </p>
134  * @author Craig R. McClanahan
135  * @author Remy Maucherat
136  */

137 public class DefaultServlet extends HttpServlet {
138
139     private static final long serialVersionUID = 1L;
140
141     /**
142      * The string manager for this package.
143      */

144     protected static final StringManager sm = StringManager.getManager(Constants.Package);
145
146     private static final DocumentBuilderFactory factory;
147
148     private static final SecureEntityResolver secureEntityResolver;
149
150     /**
151      * Full range marker.
152      */

153     protected static final ArrayList<Range> FULL = new ArrayList<>();
154
155     private static final Range IGNORE = new Range();
156
157     /**
158      * MIME multipart separation string
159      */

160     protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";
161
162     /**
163      * Size of file transfer buffer in bytes.
164      */

165     protected static final int BUFFER_SIZE = 4096;
166
167
168     // ----------------------------------------------------- Static Initializer
169
170     static {
171         if (Globals.IS_SECURITY_ENABLED) {
172             factory = DocumentBuilderFactory.newInstance();
173             factory.setNamespaceAware(true);
174             factory.setValidating(false);
175             secureEntityResolver = new SecureEntityResolver();
176         } else {
177             factory = null;
178             secureEntityResolver = null;
179         }
180     }
181
182
183     // ----------------------------------------------------- Instance Variables
184
185     /**
186      * The debugging detail level for this servlet.
187      */

188     protected int debug = 0;
189
190     /**
191      * The input buffer size to use when serving resources.
192      */

193     protected int input = 2048;
194
195     /**
196      * Should we generate directory listings?
197      */

198     protected boolean listings = false;
199
200     /**
201      * Read only flag. By default, it's set to true.
202      */

203     protected boolean readOnly = true;
204
205     /**
206      * List of compression formats to serve and their preference order.
207      */

208     protected CompressionFormat[] compressionFormats;
209
210     /**
211      * The output buffer size to use when serving resources.
212      */

213     protected int output = 2048;
214
215     /**
216      * Allow customized directory listing per directory.
217      */

218     protected String localXsltFile = null;
219
220     /**
221      * Allow customized directory listing per context.
222      */

223     protected String contextXsltFile = null;
224
225     /**
226      * Allow customized directory listing per instance.
227      */

228     protected String globalXsltFile = null;
229
230     /**
231      * Allow a readme file to be included.
232      */

233     protected String readmeFile = null;
234
235     /**
236      * The complete set of web application resources
237      */

238     protected transient WebResourceRoot resources = null;
239
240     /**
241      * File encoding to be used when reading static files. If none is specified
242      * the platform default is used.
243      */

244     protected String fileEncoding = null;
245     private transient Charset fileEncodingCharset = null;
246
247     /**
248      * If a file has a BOM, should that be used in preference to fileEncoding?
249      */

250     private boolean useBomIfPresent = true;
251
252     /**
253      * Minimum size for sendfile usage in bytes.
254      */

255     protected int sendfileSize = 48 * 1024;
256
257     /**
258      * Should the Accept-Ranges: bytes header be send with static resources?
259      */

260     protected boolean useAcceptRanges = true;
261
262     /**
263      * Flag to determine if server information is presented.
264      */

265     protected boolean showServerInfo = true;
266
267     /**
268      * Flag to determine if resources should be sorted.
269      */

270     protected boolean sortListings = false;
271
272     /**
273      * The sorting manager for sorting files and directories.
274      */

275     protected transient SortManager sortManager;
276
277     /**
278      * Flag that indicates whether partial PUTs are permitted.
279      */

280     private boolean allowPartialPut = true;
281
282
283     // --------------------------------------------------------- Public Methods
284
285     /**
286      * Finalize this servlet.
287      */

288     @Override
289     public void destroy() {
290         // NOOP
291     }
292
293
294     /**
295      * Initialize this servlet.
296      */

297     @Override
298     public void init() throws ServletException {
299
300         if (getServletConfig().getInitParameter("debug") != null)
301             debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
302
303         if (getServletConfig().getInitParameter("input") != null)
304             input = Integer.parseInt(getServletConfig().getInitParameter("input"));
305
306         if (getServletConfig().getInitParameter("output") != null)
307             output = Integer.parseInt(getServletConfig().getInitParameter("output"));
308
309         listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings"));
310
311         if (getServletConfig().getInitParameter("readonly") != null)
312             readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
313
314         compressionFormats = parseCompressionFormats(
315                 getServletConfig().getInitParameter("precompressed"),
316                 getServletConfig().getInitParameter("gzip"));
317
318         if (getServletConfig().getInitParameter("sendfileSize") != null)
319             sendfileSize =
320                 Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024;
321
322         fileEncoding = getServletConfig().getInitParameter("fileEncoding");
323         if (fileEncoding == null) {
324             fileEncodingCharset = Charset.defaultCharset();
325             fileEncoding = fileEncodingCharset.name();
326         } else {
327             try {
328                 fileEncodingCharset = B2CConverter.getCharset(fileEncoding);
329             } catch (UnsupportedEncodingException e) {
330                 throw new ServletException(e);
331             }
332         }
333
334         if (getServletConfig().getInitParameter("useBomIfPresent") != null)
335             useBomIfPresent = Boolean.parseBoolean(
336                     getServletConfig().getInitParameter("useBomIfPresent"));
337
338         globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
339         contextXsltFile = getServletConfig().getInitParameter("contextXsltFile");
340         localXsltFile = getServletConfig().getInitParameter("localXsltFile");
341         readmeFile = getServletConfig().getInitParameter("readmeFile");
342
343         if (getServletConfig().getInitParameter("useAcceptRanges") != null)
344             useAcceptRanges = Boolean.parseBoolean(getServletConfig().getInitParameter("useAcceptRanges"));
345
346         // Sanity check on the specified buffer sizes
347         if (input < 256)
348             input = 256;
349         if (output < 256)
350             output = 256;
351
352         if (debug > 0) {
353             log("DefaultServlet.init:  input buffer size=" + input +
354                 ", output buffer size=" + output);
355         }
356
357         // Load the web resources
358         resources = (WebResourceRoot) getServletContext().getAttribute(
359                 Globals.RESOURCES_ATTR);
360
361         if (resources == null) {
362             throw new UnavailableException(sm.getString("defaultServlet.noResources"));
363         }
364
365         if (getServletConfig().getInitParameter("showServerInfo") != null) {
366             showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo"));
367         }
368
369         if (getServletConfig().getInitParameter("sortListings") != null) {
370             sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings"));
371
372             if(sortListings) {
373                 boolean sortDirectoriesFirst;
374                 if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) {
375                     sortDirectoriesFirst = Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst"));
376                 } else {
377                     sortDirectoriesFirst = false;
378                 }
379
380                 sortManager = new SortManager(sortDirectoriesFirst);
381             }
382         }
383
384         if (getServletConfig().getInitParameter("allowPartialPut") != null) {
385             allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut"));
386         }
387     }
388
389     private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) {
390         List<CompressionFormat> ret = new ArrayList<>();
391         if (precompressed != null && precompressed.indexOf('=') > 0) {
392             for (String pair : precompressed.split(",")) {
393                 String[] setting = pair.split("=");
394                 String encoding = setting[0];
395                 String extension = setting[1];
396                 ret.add(new CompressionFormat(extension, encoding));
397             }
398         } else if (precompressed != null) {
399             if (Boolean.parseBoolean(precompressed)) {
400                 ret.add(new CompressionFormat(".br""br"));
401                 ret.add(new CompressionFormat(".gz""gzip"));
402             }
403         } else if (Boolean.parseBoolean(gzip)) {
404             // gzip handling is for backwards compatibility with Tomcat 8.x
405             ret.add(new CompressionFormat(".gz""gzip"));
406         }
407         return ret.toArray(new CompressionFormat[ret.size()]);
408     }
409
410
411     // ------------------------------------------------------ Protected Methods
412
413
414     /**
415      * Return the relative path associated with this servlet.
416      *
417      * @param request The servlet request we are processing
418      * @return the relative path
419      */

420     protected String getRelativePath(HttpServletRequest request) {
421         return getRelativePath(request, false);
422     }
423
424     protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
425         // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
426         // serves resources from the web app root with context rooted paths.
427         // i.e. it cannot be used to mount the web app root under a sub-path
428         // This method must construct a complete context rooted path, although
429         // subclasses can change this behaviour.
430
431         String servletPath;
432         String pathInfo;
433
434         if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
435             // For includes, get the info from the attributes
436             pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
437             servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
438         } else {
439             pathInfo = request.getPathInfo();
440             servletPath = request.getServletPath();
441         }
442
443         StringBuilder result = new StringBuilder();
444         if (servletPath.length() > 0) {
445             result.append(servletPath);
446         }
447         if (pathInfo != null) {
448             result.append(pathInfo);
449         }
450         if (result.length() == 0 && !allowEmptyPath) {
451             result.append('/');
452         }
453
454         return result.toString();
455     }
456
457
458     /**
459      * Determines the appropriate path to prepend resources with
460      * when generating directory listings. Depending on the behaviour of
461      * {@link #getRelativePath(HttpServletRequest)} this will change.
462      * @param request the request to determine the path for
463      * @return the prefix to apply to all resources in the listing.
464      */

465     protected String getPathPrefix(final HttpServletRequest request) {
466         return request.getContextPath();
467     }
468
469
470     @Override
471     protected void service(HttpServletRequest req, HttpServletResponse resp)
472             throws ServletException, IOException {
473
474         if (req.getDispatcherType() == DispatcherType.ERROR) {
475             doGet(req, resp);
476         } else {
477             super.service(req, resp);
478         }
479     }
480
481
482     /**
483      * Process a GET request for the specified resource.
484      *
485      * @param request The servlet request we are processing
486      * @param response The servlet response we are creating
487      *
488      * @exception IOException if an input/output error occurs
489      * @exception ServletException if a servlet-specified error occurs
490      */

491     @Override
492     protected void doGet(HttpServletRequest request,
493                          HttpServletResponse response)
494         throws IOException, ServletException {
495
496         // Serve the requested resource, including the data content
497         serveResource(request, response, true, fileEncoding);
498
499     }
500
501
502     /**
503      * Process a HEAD request for the specified resource.
504      *
505      * @param request The servlet request we are processing
506      * @param response The servlet response we are creating
507      *
508      * @exception IOException if an input/output error occurs
509      * @exception ServletException if a servlet-specified error occurs
510      */

511     @Override
512     protected void doHead(HttpServletRequest request, HttpServletResponse response)
513             throws IOException, ServletException {
514         // Serve the requested resource, without the data content unless we are
515         // being included since in that case the content needs to be provided so
516         // the correct content length is reported for the including resource
517         boolean serveContent = DispatcherType.INCLUDE.equals(request.getDispatcherType());
518         serveResource(request, response, serveContent, fileEncoding);
519     }
520
521
522     /**
523      * Override default implementation to ensure that TRACE is correctly
524      * handled.
525      *
526      * @param req   the {@link HttpServletRequest} object that
527      *                  contains the request the client made of
528      *                  the servlet
529      *
530      * @param resp  the {@link HttpServletResponse} object that
531      *                  contains the response the servlet returns
532      *                  to the client
533      *
534      * @exception IOException   if an input or output error occurs
535      *                              while the servlet is handling the
536      *                              OPTIONS request
537      *
538      * @exception ServletException  if the request for the
539      *                                  OPTIONS cannot be handled
540      */

541     @Override
542     protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
543         throws ServletException, IOException {
544
545         resp.setHeader("Allow", determineMethodsAllowed(req));
546     }
547
548
549     protected String determineMethodsAllowed(HttpServletRequest req) {
550         StringBuilder allow = new StringBuilder();
551
552         // Start with methods that are always allowed
553         allow.append("OPTIONS, GET, HEAD, POST");
554
555         // PUT and DELETE depend on readonly
556         if (!readOnly) {
557             allow.append(", PUT, DELETE");
558         }
559
560         // Trace - assume disabled unless we can prove otherwise
561         if (req instanceof RequestFacade &&
562                 ((RequestFacade) req).getAllowTrace()) {
563             allow.append(", TRACE");
564         }
565
566         return allow.toString();
567     }
568
569
570     protected void sendNotAllowed(HttpServletRequest req, HttpServletResponse resp)
571             throws IOException {
572         resp.addHeader("Allow", determineMethodsAllowed(req));
573         resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
574     }
575
576
577     /**
578      * Process a POST request for the specified resource.
579      *
580      * @param request The servlet request we are processing
581      * @param response The servlet response we are creating
582      *
583      * @exception IOException if an input/output error occurs
584      * @exception ServletException if a servlet-specified error occurs
585      */

586     @Override
587     protected void doPost(HttpServletRequest request,
588                           HttpServletResponse response)
589         throws IOException, ServletException {
590         doGet(request, response);
591     }
592
593
594     /**
595      * Process a PUT request for the specified resource.
596      *
597      * @param req The servlet request we are processing
598      * @param resp The servlet response we are creating
599      *
600      * @exception IOException if an input/output error occurs
601      * @exception ServletException if a servlet-specified error occurs
602      */

603     @Override
604     protected void doPut(HttpServletRequest req, HttpServletResponse resp)
605         throws ServletException, IOException {
606
607         if (readOnly) {
608             sendNotAllowed(req, resp);
609             return;
610         }
611
612         String path = getRelativePath(req);
613
614         WebResource resource = resources.getResource(path);
615
616         Range range = parseContentRange(req, resp);
617
618         if (range == null) {
619             // Processing error. parseContentRange() set the error code
620             return;
621         }
622
623         InputStream resourceInputStream = null;
624
625         try {
626             // Append data specified in ranges to existing content for this
627             // resource - create a temp. file on the local filesystem to
628             // perform this operation
629             // Assume just one range is specified for now
630             if (range == IGNORE) {
631                 resourceInputStream = req.getInputStream();
632             } else {
633                 File contentFile = executePartialPut(req, range, path);
634                 resourceInputStream = new FileInputStream(contentFile);
635             }
636
637             if (resources.write(path, resourceInputStream, true)) {
638                 if (resource.exists()) {
639                     resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
640                 } else {
641                     resp.setStatus(HttpServletResponse.SC_CREATED);
642                 }
643             } else {
644                 resp.sendError(HttpServletResponse.SC_CONFLICT);
645             }
646         } finally {
647             if (resourceInputStream != null) {
648                 try {
649                     resourceInputStream.close();
650                 } catch (IOException ioe) {
651                     // Ignore
652                 }
653             }
654         }
655     }
656
657
658     /**
659      * Handle a partial PUT.  New content specified in request is appended to
660      * existing content in oldRevisionContent (if present). This code does
661      * not support simultaneous partial updates to the same resource.
662      * @param req The Servlet request
663      * @param range The range that will be written
664      * @param path The path
665      * @return the associated file object
666      * @throws IOException an IO error occurred
667      */

668     protected File executePartialPut(HttpServletRequest req, Range range,
669                                      String path)
670         throws IOException {
671
672         // Append data specified in ranges to existing content for this
673         // resource - create a temp. file on the local filesystem to
674         // perform this operation
675         File tempDir = (File) getServletContext().getAttribute
676             (ServletContext.TEMPDIR);
677         // Convert all '/' characters to '.' in resourcePath
678         String convertedResourcePath = path.replace('/', '.');
679         File contentFile = new File(tempDir, convertedResourcePath);
680         if (contentFile.createNewFile()) {
681             // Clean up contentFile when Tomcat is terminated
682             contentFile.deleteOnExit();
683         }
684
685         try (RandomAccessFile randAccessContentFile =
686             new RandomAccessFile(contentFile, "rw")) {
687
688             WebResource oldResource = resources.getResource(path);
689
690             // Copy data in oldRevisionContent to contentFile
691             if (oldResource.isFile()) {
692                 try (BufferedInputStream bufOldRevStream =
693                     new BufferedInputStream(oldResource.getInputStream(),
694                             BUFFER_SIZE)) {
695
696                     int numBytesRead;
697                     byte[] copyBuffer = new byte[BUFFER_SIZE];
698                     while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
699                         randAccessContentFile.write(copyBuffer, 0, numBytesRead);
700                     }
701
702                 }
703             }
704
705             randAccessContentFile.setLength(range.length);
706
707             // Append data in request input stream to contentFile
708             randAccessContentFile.seek(range.start);
709             int numBytesRead;
710             byte[] transferBuffer = new byte[BUFFER_SIZE];
711             try (BufferedInputStream requestBufInStream =
712                 new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) {
713                 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
714                     randAccessContentFile.write(transferBuffer, 0, numBytesRead);
715                 }
716             }
717         }
718
719         return contentFile;
720     }
721
722
723     /**
724      * Process a DELETE request for the specified resource.
725      *
726      * @param req The servlet request we are processing
727      * @param resp The servlet response we are creating
728      *
729      * @exception IOException if an input/output error occurs
730      * @exception ServletException if a servlet-specified error occurs
731      */

732     @Override
733     protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
734         throws ServletException, IOException {
735
736         if (readOnly) {
737             sendNotAllowed(req, resp);
738             return;
739         }
740
741         String path = getRelativePath(req);
742
743         WebResource resource = resources.getResource(path);
744
745         if (resource.exists()) {
746             if (resource.delete()) {
747                 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
748             } else {
749                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
750             }
751         } else {
752             resp.sendError(HttpServletResponse.SC_NOT_FOUND);
753         }
754
755     }
756
757
758     /**
759      * Check if the conditions specified in the optional If headers are
760      * satisfied.
761      *
762      * @param request   The servlet request we are processing
763      * @param response  The servlet response we are creating
764      * @param resource  The resource
765      * @return <code>true</code> if the resource meets all the specified
766      *  conditions, and <code>false</code> if any of the conditions is not
767      *  satisfied, in which case request processing is stopped
768      * @throws IOException an IO error occurred
769      */

770     protected boolean checkIfHeaders(HttpServletRequest request,
771                                      HttpServletResponse response,
772                                      WebResource resource)
773         throws IOException {
774
775         return checkIfMatch(request, response, resource)
776             && checkIfModifiedSince(request, response, resource)
777             && checkIfNoneMatch(request, response, resource)
778             && checkIfUnmodifiedSince(request, response, resource);
779
780     }
781
782
783     /**
784      * URL rewriter.
785      *
786      * @param path Path which has to be rewritten
787      * @return the rewritten path
788      */

789     protected String rewriteUrl(String path) {
790         return URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8);
791     }
792
793
794     /**
795      * Serve the specified resource, optionally including the data content.
796      *
797      * @param request       The servlet request we are processing
798      * @param response      The servlet response we are creating
799      * @param content       Should the content be included?
800      * @param inputEncoding The encoding to use if it is necessary to access the
801      *                      source as characters rather than as bytes
802      *
803      * @exception IOException if an input/output error occurs
804      * @exception ServletException if a servlet-specified error occurs
805      */

806     protected void serveResource(HttpServletRequest request,
807                                  HttpServletResponse response,
808                                  boolean content,
809                                  String inputEncoding)
810         throws IOException, ServletException {
811
812         boolean serveContent = content;
813
814         // Identify the requested resource path
815         String path = getRelativePath(request, true);
816
817         if (debug > 0) {
818             if (serveContent)
819                 log("DefaultServlet.serveResource:  Serving resource '" +
820                     path + "' headers and data");
821             else
822                 log("DefaultServlet.serveResource:  Serving resource '" +
823                     path + "' headers only");
824         }
825
826         if (path.length() == 0) {
827             // Context root redirect
828             doDirectoryRedirect(request, response);
829             return;
830         }
831
832         WebResource resource = resources.getResource(path);
833         boolean isError = DispatcherType.ERROR == request.getDispatcherType();
834
835         if (!resource.exists()) {
836             // Check if we're included so we can return the appropriate
837             // missing resource name in the error
838             String requestUri = (String) request.getAttribute(
839                     RequestDispatcher.INCLUDE_REQUEST_URI);
840             if (requestUri == null) {
841                 requestUri = request.getRequestURI();
842             } else {
843                 // We're included
844                 // SRV.9.3 says we must throw a FNFE
845                 throw new FileNotFoundException(sm.getString(
846                         "defaultServlet.missingResource", requestUri));
847             }
848
849             if (isError) {
850                 response.sendError(((Integer) request.getAttribute(
851                         RequestDispatcher.ERROR_STATUS_CODE)).intValue());
852             } else {
853                 response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
854             }
855             return;
856         }
857
858         if (!resource.canRead()) {
859             // Check if we're included so we can return the appropriate
860             // missing resource name in the error
861             String requestUri = (String) request.getAttribute(
862                     RequestDispatcher.INCLUDE_REQUEST_URI);
863             if (requestUri == null) {
864                 requestUri = request.getRequestURI();
865             } else {
866                 // We're included
867                 // Spec doesn't say what to do in this case but a FNFE seems
868                 // reasonable
869                 throw new FileNotFoundException(sm.getString(
870                         "defaultServlet.missingResource", requestUri));
871             }
872
873             if (isError) {
874                 response.sendError(((Integer) request.getAttribute(
875                         RequestDispatcher.ERROR_STATUS_CODE)).intValue());
876             } else {
877                 response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri);
878             }
879             return;
880         }
881
882         boolean included = false;
883         // Check if the conditions specified in the optional If headers are
884         // satisfied.
885         if (resource.isFile()) {
886             // Checking If headers
887             included = (request.getAttribute(
888                     RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
889             if (!included && !isError && !checkIfHeaders(request, response, resource)) {
890                 return;
891             }
892         }
893
894         // Find content type.
895         String contentType = resource.getMimeType();
896         if (contentType == null) {
897             contentType = getServletContext().getMimeType(resource.getName());
898             resource.setMimeType(contentType);
899         }
900
901         // These need to reflect the original resource, not the potentially
902         // precompressed version of the resource so get them now if they are going to
903         // be needed later
904         String eTag = null;
905         String lastModifiedHttp = null;
906         if (resource.isFile() && !isError) {
907             eTag = resource.getETag();
908             lastModifiedHttp = resource.getLastModifiedHttp();
909         }
910
911
912         // Serve a precompressed version of the file if present
913         boolean usingPrecompressedVersion = false;
914         if (compressionFormats.length > 0 && !included && resource.isFile() &&
915                 !pathEndsWithCompressedExtension(path)) {
916             List<PrecompressedResource> precompressedResources =
917                     getAvailablePrecompressedResources(path);
918             if (!precompressedResources.isEmpty()) {
919                 ResponseUtil.addVaryFieldName(response, "accept-encoding");
920                 PrecompressedResource bestResource =
921                         getBestPrecompressedResource(request, precompressedResources);
922                 if (bestResource != null) {
923                     response.addHeader("Content-Encoding", bestResource.format.encoding);
924                     resource = bestResource.resource;
925                     usingPrecompressedVersion = true;
926                 }
927             }
928         }
929
930         ArrayList<Range> ranges = FULL;
931         long contentLength = -1L;
932
933         if (resource.isDirectory()) {
934             if (!path.endsWith("/")) {
935                 doDirectoryRedirect(request, response);
936                 return;
937             }
938
939             // Skip directory listings if we have been configured to
940             // suppress them
941             if (!listings) {
942                 response.sendError(HttpServletResponse.SC_NOT_FOUND,
943                                    request.getRequestURI());
944                 return;
945             }
946             contentType = "text/html;charset=UTF-8";
947         } else {
948             if (!isError) {
949                 if (useAcceptRanges) {
950                     // Accept ranges header
951                     response.setHeader("Accept-Ranges""bytes");
952                 }
953
954                 // Parse range specifier
955                 ranges = parseRange(request, response, resource);
956                 if (ranges == null) {
957                     return;
958                 }
959
960                 // ETag header
961                 response.setHeader("ETag", eTag);
962
963                 // Last-Modified header
964                 response.setHeader("Last-Modified", lastModifiedHttp);
965             }
966
967             // Get content length
968             contentLength = resource.getContentLength();
969             // Special case for zero length files, which would cause a
970             // (silent) ISE when setting the output buffer size
971             if (contentLength == 0L) {
972                 serveContent = false;
973             }
974         }
975
976         ServletOutputStream ostream = null;
977         PrintWriter writer = null;
978
979         if (serveContent) {
980             // Trying to retrieve the servlet output stream
981             try {
982                 ostream = response.getOutputStream();
983             } catch (IllegalStateException e) {
984                 // If it fails, we try to get a Writer instead if we're
985                 // trying to serve a text file
986                 if (!usingPrecompressedVersion && isText(contentType)) {
987                     writer = response.getWriter();
988                     // Cannot reliably serve partial content with a Writer
989                     ranges = FULL;
990                 } else {
991                     throw e;
992                 }
993             }
994         }
995
996         // Check to see if a Filter, Valve or wrapper has written some content.
997         // If it has, disable range requests and setting of a content length
998         // since neither can be done reliably.
999         ServletResponse r = response;
1000         long contentWritten = 0;
1001         while (r instanceof ServletResponseWrapper) {
1002             r = ((ServletResponseWrapper) r).getResponse();
1003         }
1004         if (r instanceof ResponseFacade) {
1005             contentWritten = ((ResponseFacade) r).getContentWritten();
1006         }
1007         if (contentWritten > 0) {
1008             ranges = FULL;
1009         }
1010
1011         String outputEncoding = response.getCharacterEncoding();
1012         Charset charset = B2CConverter.getCharset(outputEncoding);
1013         boolean conversionRequired;
1014         /*
1015          * The test below deliberately uses != to compare two Strings. This is
1016          * because the code is looking to see if the default character encoding
1017          * has been returned because no explicit character encoding has been
1018          * defined. There is no clean way of doing this via the Servlet API. It
1019          * would be possible to add a Tomcat specific API but that would require
1020          * quite a bit of code to get to the Tomcat specific request object that
1021          * may have been wrapped. The != test is a (slightly hacky) quick way of
1022          * doing this.
1023          */

1024         boolean outputEncodingSpecified =
1025                 outputEncoding != org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name() &&
1026                 outputEncoding != resources.getContext().getResponseCharacterEncoding();
1027         if (!usingPrecompressedVersion && isText(contentType) && outputEncodingSpecified &&
1028                 !charset.equals(fileEncodingCharset)) {
1029             conversionRequired = true;
1030             // Conversion often results fewer/more/different bytes.
1031             // That does not play nicely with range requests.
1032             ranges = FULL;
1033         } else {
1034             conversionRequired = false;
1035         }
1036
1037         if (resource.isDirectory() || isError || ranges == FULL ) {
1038             // Set the appropriate output headers
1039             if (contentType != null) {
1040                 if (debug > 0)
1041                     log("DefaultServlet.serveFile:  contentType='" +
1042                         contentType + "'");
1043                 // Don't override a previously set content type
1044                 if (response.getContentType() == null) {
1045                     response.setContentType(contentType);
1046                 }
1047             }
1048             if (resource.isFile() && contentLength >= 0 &&
1049                     (!serveContent || ostream != null)) {
1050                 if (debug > 0)
1051                     log("DefaultServlet.serveFile:  contentLength=" +
1052                         contentLength);
1053                 // Don't set a content length if something else has already
1054                 // written to the response or if conversion will be taking place
1055                 if (contentWritten == 0 && !conversionRequired) {
1056                     response.setContentLengthLong(contentLength);
1057                 }
1058             }
1059
1060             if (serveContent) {
1061                 try {
1062                     response.setBufferSize(output);
1063                 } catch (IllegalStateException e) {
1064                     // Silent catch
1065                 }
1066                 InputStream renderResult = null;
1067                 if (ostream == null) {
1068                     // Output via a writer so can't use sendfile or write
1069                     // content directly.
1070                     if (resource.isDirectory()) {
1071                         renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
1072                     } else {
1073                         renderResult = resource.getInputStream();
1074                         if (included) {
1075                             // Need to make sure any BOM is removed
1076                             if (!renderResult.markSupported()) {
1077                                 renderResult = new BufferedInputStream(renderResult);
1078                             }
1079                             Charset bomCharset = processBom(renderResult);
1080                             if (bomCharset != null && useBomIfPresent) {
1081                                 inputEncoding = bomCharset.name();
1082                             }
1083                         }
1084                     }
1085                     copy(renderResult, writer, inputEncoding);
1086                 } else {
1087                     // Output is via an OutputStream
1088                     if (resource.isDirectory()) {
1089                         renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
1090                     } else {
1091                         // Output is content of resource
1092                         // Check to see if conversion is required
1093                         if (conversionRequired || included) {
1094                             // When including a file, we need to check for a BOM
1095                             // to determine if a conversion is required, so we
1096                             // might as well always convert
1097                             InputStream source = resource.getInputStream();
1098                             if (!source.markSupported()) {
1099                                 source = new BufferedInputStream(source);
1100                             }
1101                             Charset bomCharset = processBom(source);
1102                             if (bomCharset != null && useBomIfPresent) {
1103                                 inputEncoding = bomCharset.name();
1104                             }
1105                             // Following test also ensures included resources
1106                             // are converted if an explicit output encoding was
1107                             // specified
1108                             if (outputEncodingSpecified) {
1109                                 OutputStreamWriter osw = new OutputStreamWriter(ostream, charset);
1110                                 PrintWriter pw = new PrintWriter(osw);
1111                                 copy(source, pw, inputEncoding);
1112                                 pw.flush();
1113                             } else {
1114                                 // Just included but no conversion
1115                                 renderResult = source;
1116                             }
1117                         } else {
1118                             if (!checkSendfile(request, response, resource, contentLength, null)) {
1119                                 // sendfile not possible so check if resource
1120                                 // content is available directly via
1121                                 // CachedResource. Do not want to call
1122                                 // getContent() on other resource
1123                                 // implementations as that could trigger loading
1124                                 // the contents of a very large file into memory
1125                                 byte[] resourceBody = null;
1126                                 if (resource instanceof CachedResource) {
1127                                     resourceBody = resource.getContent();
1128                                 }
1129                                 if (resourceBody == null) {
1130                                     // Resource content not directly available,
1131                                     // use InputStream
1132                                     renderResult = resource.getInputStream();
1133                                 } else {
1134                                     // Use the resource content directly
1135                                     ostream.write(resourceBody);
1136                                 }
1137                             }
1138                         }
1139                     }
1140                     // If a stream was configured, it needs to be copied to
1141                     // the output (this method closes the stream)
1142                     if (renderResult != null) {
1143                         copy(renderResult, ostream);
1144                     }
1145                 }
1146             }
1147
1148         } else {
1149
1150             if ((ranges == null) || (ranges.isEmpty()))
1151                 return;
1152
1153             // Partial content response.
1154
1155             response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
1156
1157             if (ranges.size() == 1) {
1158
1159                 Range range = ranges.get(0);
1160                 response.addHeader("Content-Range""bytes "
1161                                    + range.start
1162                                    + "-" + range.end + "/"
1163                                    + range.length);
1164                 long length = range.end - range.start + 1;
1165                 response.setContentLengthLong(length);
1166
1167                 if (contentType != null) {
1168                     if (debug > 0)
1169                         log("DefaultServlet.serveFile:  contentType='" +
1170                             contentType + "'");
1171                     response.setContentType(contentType);
1172                 }
1173
1174                 if (serveContent) {
1175                     try {
1176                         response.setBufferSize(output);
1177                     } catch (IllegalStateException e) {
1178                         // Silent catch
1179                     }
1180                     if (ostream != null) {
1181                         if (!checkSendfile(request, response, resource,
1182                                 range.end - range.start + 1, range))
1183                             copy(resource, ostream, range);
1184                     } else {
1185                         // we should not get here
1186                         throw new IllegalStateException();
1187                     }
1188                 }
1189             } else {
1190                 response.setContentType("multipart/byteranges; boundary="
1191                                         + mimeSeparation);
1192                 if (serveContent) {
1193                     try {
1194                         response.setBufferSize(output);
1195                     } catch (IllegalStateException e) {
1196                         // Silent catch
1197                     }
1198                     if (ostream != null) {
1199                         copy(resource, ostream, ranges.iterator(), contentType);
1200                     } else {
1201                         // we should not get here
1202                         throw new IllegalStateException();
1203                     }
1204                 }
1205             }
1206         }
1207     }
1208
1209
1210     /*
1211      * Code borrowed heavily from Jasper's EncodingDetector
1212      */

1213     private static Charset processBom(InputStream is) throws IOException {
1214         // Java supported character sets do not use BOMs longer than 4 bytes
1215         byte[] bom = new byte[4];
1216         is.mark(bom.length);
1217
1218         int count = is.read(bom);
1219
1220         // BOMs are at least 2 bytes
1221         if (count < 2) {
1222             skip(is, 0);
1223             return null;
1224         }
1225
1226         // Look for two byte BOMs
1227         int b0 = bom[0] & 0xFF;
1228         int b1 = bom[1] & 0xFF;
1229         if (b0 == 0xFE && b1 == 0xFF) {
1230             skip(is, 2);
1231             return StandardCharsets.UTF_16BE;
1232         }
1233         // Delay the UTF_16LE check if there are more that 2 bytes since it
1234         // overlaps with UTF-32LE.
1235         if (count == 2 && b0 == 0xFF && b1 == 0xFE) {
1236             skip(is, 2);
1237             return StandardCharsets.UTF_16LE;
1238         }
1239
1240         // Remaining BOMs are at least 3 bytes
1241         if (count < 3) {
1242             skip(is, 0);
1243             return null;
1244         }
1245
1246         // UTF-8 is only 3-byte BOM
1247         int b2 = bom[2] & 0xFF;
1248         if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
1249             skip(is, 3);
1250             return StandardCharsets.UTF_8;
1251         }
1252
1253         if (count < 4) {
1254             skip(is, 0);
1255             return null;
1256         }
1257
1258         // Look for 4-byte BOMs
1259         int b3 = bom[3] & 0xFF;
1260         if (b0 == 0x00 && b1 == 0x00 && b2 == 0xFE && b3 == 0xFF) {
1261             return Charset.forName("UTF-32BE");
1262         }
1263         if (b0 == 0xFF && b1 == 0xFE && b2 == 0x00 && b3 == 0x00) {
1264             return Charset.forName("UTF-32LE");
1265         }
1266
1267         // Now we can check for UTF16-LE. There is an assumption here that we
1268         // won't see a UTF16-LE file with a BOM where the first real data is
1269         // 0x00 0x00
1270         if (b0 == 0xFF && b1 == 0xFE) {
1271             skip(is, 2);
1272             return StandardCharsets.UTF_16LE;
1273         }
1274
1275         skip(is, 0);
1276         return null;
1277     }
1278
1279
1280     private static void skip(InputStream is, int skip) throws IOException {
1281         is.reset();
1282         while (skip-- > 0) {
1283             is.read();
1284         }
1285     }
1286
1287
1288     private static boolean isText(String contentType) {
1289         return  contentType == null || contentType.startsWith("text") ||
1290                 contentType.endsWith("xml") || contentType.contains("/javascript");
1291     }
1292
1293
1294     private boolean pathEndsWithCompressedExtension(String path) {
1295         for (CompressionFormat format : compressionFormats) {
1296             if (path.endsWith(format.extension)) {
1297                 return true;
1298             }
1299         }
1300         return false;
1301     }
1302
1303     private List<PrecompressedResource> getAvailablePrecompressedResources(String path) {
1304         List<PrecompressedResource> ret = new ArrayList<>(compressionFormats.length);
1305         for (CompressionFormat format : compressionFormats) {
1306             WebResource precompressedResource = resources.getResource(path + format.extension);
1307             if (precompressedResource.exists() && precompressedResource.isFile()) {
1308                 ret.add(new PrecompressedResource(precompressedResource, format));
1309             }
1310         }
1311         return ret;
1312     }
1313
1314     /**
1315      * Match the client preferred encoding formats to the available precompressed resources.
1316      *
1317      * @param request   The servlet request we are processing
1318      * @param precompressedResources   List of available precompressed resources.
1319      * @return The best matching precompressed resource or null if no match was found.
1320      */

1321     private PrecompressedResource getBestPrecompressedResource(HttpServletRequest request,
1322             List<PrecompressedResource> precompressedResources) {
1323         Enumeration<String> headers = request.getHeaders("Accept-Encoding");
1324         PrecompressedResource bestResource = null;
1325         double bestResourceQuality = 0;
1326         int bestResourcePreference = Integer.MAX_VALUE;
1327         while (headers.hasMoreElements()) {
1328             String header = headers.nextElement();
1329             for (String preference : header.split(",")) {
1330                 double quality = 1;
1331                 int qualityIdx = preference.indexOf(';');
1332                 if (qualityIdx > 0) {
1333                     int equalsIdx = preference.indexOf('=', qualityIdx + 1);
1334                     if (equalsIdx == -1) {
1335                         continue;
1336                     }
1337                     quality = Double.parseDouble(preference.substring(equalsIdx + 1).trim());
1338                 }
1339                 if (quality >= bestResourceQuality) {
1340                     String encoding = preference;
1341                     if (qualityIdx > 0) {
1342                         encoding = encoding.substring(0, qualityIdx);
1343                     }
1344                     encoding = encoding.trim();
1345                     if ("identity".equals(encoding)) {
1346                         bestResource = null;
1347                         bestResourceQuality = quality;
1348                         bestResourcePreference = Integer.MAX_VALUE;
1349                         continue;
1350                     }
1351                     if ("*".equals(encoding)) {
1352                         bestResource = precompressedResources.get(0);
1353                         bestResourceQuality = quality;
1354                         bestResourcePreference = 0;
1355                         continue;
1356                     }
1357                     for (int i = 0; i < precompressedResources.size(); ++i) {
1358                         PrecompressedResource resource = precompressedResources.get(i);
1359                         if (encoding.equals(resource.format.encoding)) {
1360                             if (quality > bestResourceQuality || i < bestResourcePreference) {
1361                                 bestResource = resource;
1362                                 bestResourceQuality = quality;
1363                                 bestResourcePreference = i;
1364                             }
1365                             break;
1366                         }
1367                     }
1368                 }
1369             }
1370         }
1371         return bestResource;
1372     }
1373
1374     private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response)
1375             throws IOException {
1376         StringBuilder location = new StringBuilder(request.getRequestURI());
1377         location.append('/');
1378         if (request.getQueryString() != null) {
1379             location.append('?');
1380             location.append(request.getQueryString());
1381         }
1382         // Avoid protocol relative redirects
1383         while (location.length() > 1 && location.charAt(1) == '/') {
1384             location.deleteCharAt(0);
1385         }
1386         response.sendRedirect(response.encodeRedirectURL(location.toString()));
1387     }
1388
1389     /**
1390      * Parse the content-range header.
1391      *
1392      * @param request The servlet request we are processing
1393      * @param response The servlet response we are creating
1394      * @return the partial content-range, {@code nullif the content-range
1395      *         header was invalid or {@code #IGNORE} if there is no header to
1396      *         process
1397      * @throws IOException an IO error occurred
1398      */

1399     protected Range parseContentRange(HttpServletRequest request,
1400                                       HttpServletResponse response)
1401         throws IOException {
1402
1403         // Retrieving the content-range header (if any is specified
1404         String contentRangeHeader = request.getHeader("Content-Range");
1405
1406         if (contentRangeHeader == null) {
1407             return IGNORE;
1408         }
1409
1410         if (!allowPartialPut) {
1411             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1412             return null;
1413         }
1414
1415         ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader));
1416
1417         if (contentRange == null) {
1418             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1419             return null;
1420         }
1421
1422
1423         // bytes is the only range unit supported
1424         if (!contentRange.getUnits().equals("bytes")) {
1425             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1426             return null;
1427         }
1428
1429         // TODO: Remove the internal representation and use Ranges
1430         // Convert to internal representation
1431         Range range = new Range();
1432         range.start = contentRange.getStart();
1433         range.end = contentRange.getEnd();
1434         range.length = contentRange.getLength();
1435
1436         if (!range.validate()) {
1437             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1438             return null;
1439         }
1440
1441         return range;
1442     }
1443
1444
1445     /**
1446      * Parse the range header.
1447      *
1448      * @param request   The servlet request we are processing
1449      * @param response  The servlet response we are creating
1450      * @param resource  The resource
1451      * @return a list of ranges, {@code nullif the range header was invalid or
1452      *         {@code #FULL} if the Range header should be ignored.
1453      * @throws IOException an IO error occurred
1454      */

1455     protected ArrayList<Range> parseRange(HttpServletRequest request,
1456             HttpServletResponse response,
1457             WebResource resource) throws IOException {
1458
1459         // Range headers are only valid on GET requests. That implies they are
1460         // also valid on HEAD requests. This method is only called by doGet()
1461         // and doHead() so no need to check the request method.
1462
1463         // Checking If-Range
1464         String headerValue = request.getHeader("If-Range");
1465
1466         if (headerValue != null) {
1467
1468             long headerValueTime = (-1L);
1469             try {
1470                 headerValueTime = request.getDateHeader("If-Range");
1471             } catch (IllegalArgumentException e) {
1472                 // Ignore
1473             }
1474
1475             String eTag = resource.getETag();
1476             long lastModified = resource.getLastModified();
1477
1478             if (headerValueTime == (-1L)) {
1479                 // If the ETag the client gave does not match the entity
1480                 // etag, then the entire entity is returned.
1481                 if (!eTag.equals(headerValue.trim())) {
1482                     return FULL;
1483                 }
1484             } else {
1485                 // If the timestamp of the entity the client got differs from
1486                 // the last modification date of the entity, the entire entity
1487                 // is returned.
1488                 if (Math.abs(lastModified  -headerValueTime) > 1000) {
1489                     return FULL;
1490                 }
1491             }
1492         }
1493
1494         long fileLength = resource.getContentLength();
1495
1496         if (fileLength == 0) {
1497             // Range header makes no sense for a zero length resource. Tomcat
1498             // therefore opts to ignore it.
1499             return FULL;
1500         }
1501
1502         // Retrieving the range header (if any is specified
1503         String rangeHeader = request.getHeader("Range");
1504
1505         if (rangeHeader == null) {
1506             // No Range header is the same as ignoring any Range header
1507             return FULL;
1508         }
1509
1510         Ranges ranges = Ranges.parse(new StringReader(rangeHeader));
1511
1512         if (ranges == null) {
1513             // The Range header is present but not formatted correctly.
1514             // Could argue for a 400 response but 416 is more specific.
1515             // There is also the option to ignore the (invalid) Range header.
1516             // RFC7233#4.4 notes that many servers do ignore the Range header in
1517             // these circumstances but Tomcat has always returned a 416.
1518             response.addHeader("Content-Range""bytes */" + fileLength);
1519             response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1520             return null;
1521         }
1522
1523         // bytes is the only range unit supported (and I don't see the point
1524         // of adding new ones).
1525         if (!ranges.getUnits().equals("bytes")) {
1526             // RFC7233#3.1 Servers must ignore range units they don't understand
1527             return FULL;
1528         }
1529
1530         // TODO: Remove the internal representation and use Ranges
1531         // Convert to internal representation
1532         ArrayList<Range> result = new ArrayList<>();
1533
1534         for (Ranges.Entry entry : ranges.getEntries()) {
1535             Range currentRange = new Range();
1536             if (entry.getStart() == -1) {
1537                 currentRange.start = fileLength - entry.getEnd();
1538                 if (currentRange.start < 0) {
1539                     currentRange.start = 0;
1540                 }
1541                 currentRange.end = fileLength - 1;
1542             } else if (entry.getEnd() == -1) {
1543                 currentRange.start = entry.getStart();
1544                 currentRange.end = fileLength - 1;
1545             } else {
1546                 currentRange.start = entry.getStart();
1547                 currentRange.end = entry.getEnd();
1548             }
1549             currentRange.length = fileLength;
1550
1551             if (!currentRange.validate()) {
1552                 response.addHeader("Content-Range""bytes */" + fileLength);
1553                 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1554                 return null;
1555             }
1556
1557             result.add(currentRange);
1558         }
1559
1560         return result;
1561     }
1562
1563
1564     /**
1565      * Decide which way to render. HTML or XML.
1566      *
1567      * @param contextPath The path
1568      * @param resource    The resource
1569      * @param encoding    The encoding to use to process the readme (if any)
1570      *
1571      * @return the input stream with the rendered output
1572      *
1573      * @throws IOException an IO error occurred
1574      * @throws ServletException rendering error
1575      *
1576      * @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
1577      */

1578     @Deprecated
1579     protected InputStream render(String contextPath, WebResource resource, String encoding)
1580         throws IOException, ServletException {
1581
1582         return render(null, contextPath, resource, encoding);
1583     }
1584
1585     /**
1586      * Decide which way to render. HTML or XML.
1587      *
1588      * @param request     The HttpServletRequest being served
1589      * @param contextPath The path
1590      * @param resource    The resource
1591      * @param encoding    The encoding to use to process the readme (if any)
1592      *
1593      * @return the input stream with the rendered output
1594      *
1595      * @throws IOException an IO error occurred
1596      * @throws ServletException rendering error
1597      */

1598     protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
1599         throws IOException, ServletException {
1600
1601         Source xsltSource = findXsltSource(resource);
1602
1603         if (xsltSource == null) {
1604             return renderHtml(request, contextPath, resource, encoding);
1605         }
1606         return renderXml(request, contextPath, resource, xsltSource, encoding);
1607     }
1608
1609
1610     /**
1611      * Return an InputStream to an XML representation of the contents this
1612      * directory.
1613      *
1614      * @param contextPath Context path to which our internal paths are relative
1615      * @param resource    The associated resource
1616      * @param xsltSource  The XSL stylesheet
1617      * @param encoding    The encoding to use to process the readme (if any)
1618      *
1619      * @return the XML data
1620      *
1621      * @throws IOException an IO error occurred
1622      * @throws ServletException rendering error
1623      * @deprecated Unused. Will be removed in Tomcat 10
1624      * @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
1625      */

1626     @Deprecated
1627     protected InputStream renderXml(String contextPath, WebResource resource, Source xsltSource,
1628             String encoding)
1629         throws ServletException, IOException
1630     {
1631         return renderXml(null, contextPath, resource, xsltSource, encoding);
1632     }
1633
1634     /**
1635      * Return an InputStream to an XML representation of the contents this
1636      * directory.
1637      *
1638      * @param request     The HttpServletRequest being served
1639      * @param contextPath Context path to which our internal paths are relative
1640      * @param resource    The associated resource
1641      * @param xsltSource  The XSL stylesheet
1642      * @param encoding    The encoding to use to process the readme (if any)
1643      *
1644      * @return the XML data
1645      *
1646      * @throws IOException an IO error occurred
1647      * @throws ServletException rendering error
1648      */

1649     protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource, Source xsltSource,
1650             String encoding)
1651         throws IOException, ServletException {
1652
1653         StringBuilder sb = new StringBuilder();
1654
1655         sb.append("<?xml version=\"1.0\"?>");
1656         sb.append("<listing ");
1657         sb.append(" contextPath='");
1658         sb.append(contextPath);
1659         sb.append("'");
1660         sb.append(" directory='");
1661         sb.append(resource.getName());
1662         sb.append("' ");
1663         sb.append(" hasParent='").append(!resource.getName().equals("/"));
1664         sb.append("'>");
1665
1666         sb.append("<entries>");
1667
1668         String[] entries = resources.list(resource.getWebappPath());
1669
1670         // rewriteUrl(contextPath) is expensive. cache result for later reuse
1671         String rewrittenContextPath =  rewriteUrl(contextPath);
1672         String directoryWebappPath = resource.getWebappPath();
1673
1674         for (String entry : entries) {
1675
1676             if (entry.equalsIgnoreCase("WEB-INF") ||
1677                     entry.equalsIgnoreCase("META-INF") ||
1678                     entry.equalsIgnoreCase(localXsltFile))
1679                 continue;
1680
1681             if ((directoryWebappPath + entry).equals(contextXsltFile))
1682                 continue;
1683
1684             WebResource childResource =
1685                     resources.getResource(directoryWebappPath + entry);
1686             if (!childResource.exists()) {
1687                 continue;
1688             }
1689
1690             sb.append("<entry");
1691             sb.append(" type='")
1692               .append(childResource.isDirectory()?"dir":"file")
1693               .append("'");
1694             sb.append(" urlPath='")
1695               .append(rewrittenContextPath)
1696               .append(rewriteUrl(directoryWebappPath + entry))
1697               .append(childResource.isDirectory()?"/":"")
1698               .append("'");
1699             if (childResource.isFile()) {
1700                 sb.append(" size='")
1701                   .append(renderSize(childResource.getContentLength()))
1702                   .append("'");
1703             }
1704             sb.append(" date='")
1705               .append(childResource.getLastModifiedHttp())
1706               .append("'");
1707
1708             sb.append(">");
1709             sb.append(Escape.htmlElementContent(entry));
1710             if (childResource.isDirectory())
1711                 sb.append("/");
1712             sb.append("</entry>");
1713         }
1714         sb.append("</entries>");
1715
1716         String readme = getReadme(resource, encoding);
1717
1718         if (readme!=null) {
1719             sb.append("<readme><![CDATA[");
1720             sb.append(readme);
1721             sb.append("]]></readme>");
1722         }
1723
1724         sb.append("</listing>");
1725
1726         // Prevent possible memory leak. Ensure Transformer and
1727         // TransformerFactory are not loaded from the web application.
1728         ClassLoader original;
1729         if (Globals.IS_SECURITY_ENABLED) {
1730             PrivilegedGetTccl pa = new PrivilegedGetTccl();
1731             original = AccessController.doPrivileged(pa);
1732         } else {
1733             original = Thread.currentThread().getContextClassLoader();
1734         }
1735         try {
1736             if (Globals.IS_SECURITY_ENABLED) {
1737                 PrivilegedSetTccl pa =
1738                         new PrivilegedSetTccl(DefaultServlet.class.getClassLoader());
1739                 AccessController.doPrivileged(pa);
1740             } else {
1741                 Thread.currentThread().setContextClassLoader(
1742                         DefaultServlet.class.getClassLoader());
1743             }
1744
1745             TransformerFactory tFactory = TransformerFactory.newInstance();
1746             Source xmlSource = new StreamSource(new StringReader(sb.toString()));
1747             Transformer transformer = tFactory.newTransformer(xsltSource);
1748
1749             ByteArrayOutputStream stream = new ByteArrayOutputStream();
1750             OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
1751             StreamResult out = new StreamResult(osWriter);
1752             transformer.transform(xmlSource, out);
1753             osWriter.flush();
1754             return new ByteArrayInputStream(stream.toByteArray());
1755         } catch (TransformerException e) {
1756             throw new ServletException(sm.getString("defaultServlet.xslError"), e);
1757         } finally {
1758             if (Globals.IS_SECURITY_ENABLED) {
1759                 PrivilegedSetTccl pa = new PrivilegedSetTccl(original);
1760                 AccessController.doPrivileged(pa);
1761             } else {
1762                 Thread.currentThread().setContextClassLoader(original);
1763             }
1764         }
1765     }
1766
1767     /**
1768      * Return an InputStream to an HTML representation of the contents of this
1769      * directory.
1770      *
1771      * @param contextPath Context path to which our internal paths are relative
1772      * @param resource    The associated resource
1773      * @param encoding    The encoding to use to process the readme (if any)
1774      *
1775      * @return the HTML data
1776      *
1777      * @throws IOException an IO error occurred
1778      *
1779      * @deprecated Unused. Will be removed in Tomcat 10
1780      * @deprecated Use {@link #renderHtml(HttpServletRequest, String, WebResource, String)} instead
1781      */

1782     @Deprecated
1783     protected InputStream renderHtml(String contextPath, WebResource resource, String encoding)
1784         throws IOException {
1785         return renderHtml(null, contextPath, resource, encoding);
1786     }
1787
1788     /**
1789      * Return an InputStream to an HTML representation of the contents of this
1790      * directory.
1791      *
1792      * @param request     The HttpServletRequest being served
1793      * @param contextPath Context path to which our internal paths are relative
1794      * @param resource    The associated resource
1795      * @param encoding    The encoding to use to process the readme (if any)
1796      *
1797      * @return the HTML data
1798      *
1799      * @throws IOException an IO error occurred
1800      */

1801     protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
1802         throws IOException {
1803
1804         // Prepare a writer to a buffered area
1805         ByteArrayOutputStream stream = new ByteArrayOutputStream();
1806         OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
1807         PrintWriter writer = new PrintWriter(osWriter);
1808
1809         StringBuilder sb = new StringBuilder();
1810
1811         String directoryWebappPath = resource.getWebappPath();
1812         WebResource[] entries = resources.listResources(directoryWebappPath);
1813
1814         // rewriteUrl(contextPath) is expensive. cache result for later reuse
1815         String rewrittenContextPath =  rewriteUrl(contextPath);
1816
1817         // Render the page header
1818         sb.append("<!doctype html><html>\r\n");
1819         /* TODO Activate this as soon as we use smClient with the request locales
1820         sb.append("<!doctype html><html lang=\"");
1821         sb.append(smClient.getLocale().getLanguage()).append("\">\r\n");
1822         */

1823         sb.append("<head>\r\n");
1824         sb.append("<title>");
1825         sb.append(sm.getString("directory.title", directoryWebappPath));
1826         sb.append("</title>\r\n");
1827         sb.append("<style>");
1828         sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
1829         sb.append("</style> ");
1830         sb.append("</head>\r\n");
1831         sb.append("<body>");
1832         sb.append("<h1>");
1833         sb.append(sm.getString("directory.title", directoryWebappPath));
1834
1835         // Render the link to our parent (if required)
1836         String parentDirectory = directoryWebappPath;
1837         if (parentDirectory.endsWith("/")) {
1838             parentDirectory =
1839                 parentDirectory.substring(0, parentDirectory.length() - 1);
1840         }
1841         int slash = parentDirectory.lastIndexOf('/');
1842         if (slash >= 0) {
1843             String parent = directoryWebappPath.substring(0, slash);
1844             sb.append(" - <a href=\"");
1845             sb.append(rewrittenContextPath);
1846             if (parent.equals(""))
1847                 parent = "/";
1848             sb.append(rewriteUrl(parent));
1849             if (!parent.endsWith("/"))
1850                 sb.append("/");
1851             sb.append("\">");
1852             sb.append("<b>");
1853             sb.append(sm.getString("directory.parent", parent));
1854             sb.append("</b>");
1855             sb.append("</a>");
1856         }
1857
1858         sb.append("</h1>");
1859         sb.append("<hr class=\"line\">");
1860
1861         sb.append("<table width=\"100%\" cellspacing=\"0\"" +
1862                      " cellpadding=\"5\" align=\"center\">\r\n");
1863
1864         SortManager.Order order;
1865         if(sortListings && null != request)
1866             order = sortManager.getOrder(request.getQueryString());
1867         else
1868             order = null;
1869         // Render the column headings
1870         sb.append("<tr>\r\n");
1871         sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
1872         if(sortListings && null != request) {
1873             sb.append("<a href=\"?C=N;O=");
1874             sb.append(getOrderChar(order, 'N'));
1875             sb.append("\">");
1876             sb.append(sm.getString("directory.filename"));
1877             sb.append("</a>");
1878         } else {
1879             sb.append(sm.getString("directory.filename"));
1880         }
1881         sb.append("</strong></font></td>\r\n");
1882         sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
1883         if(sortListings && null != request) {
1884             sb.append("<a href=\"?C=S;O=");
1885             sb.append(getOrderChar(order, 'S'));
1886             sb.append("\">");
1887             sb.append(sm.getString("directory.size"));
1888             sb.append("</a>");
1889         } else {
1890             sb.append(sm.getString("directory.size"));
1891         }
1892         sb.append("</strong></font></td>\r\n");
1893         sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
1894         if(sortListings && null != request) {
1895             sb.append("<a href=\"?C=M;O=");
1896             sb.append(getOrderChar(order, 'M'));
1897             sb.append("\">");
1898             sb.append(sm.getString("directory.lastModified"));
1899             sb.append("</a>");
1900         } else {
1901             sb.append(sm.getString("directory.lastModified"));
1902         }
1903         sb.append("</strong></font></td>\r\n");
1904         sb.append("</tr>");
1905
1906         if(null != sortManager && null != request) {
1907             sortManager.sort(entries, request.getQueryString());
1908         }
1909
1910         boolean shade = false;
1911         for (WebResource childResource : entries) {
1912             String filename = childResource.getName();
1913             if (filename.equalsIgnoreCase("WEB-INF") ||
1914                 filename.equalsIgnoreCase("META-INF"))
1915                 continue;
1916
1917             if (!childResource.exists()) {
1918                 continue;
1919             }
1920
1921             sb.append("<tr");
1922             if (shade)
1923                 sb.append(" bgcolor=\"#eeeeee\"");
1924             sb.append(">\r\n");
1925             shade = !shade;
1926
1927             sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
1928             sb.append("<a href=\"");
1929             sb.append(rewrittenContextPath);
1930             sb.append(rewriteUrl(childResource.getWebappPath()));
1931             if (childResource.isDirectory())
1932                 sb.append("/");
1933             sb.append("\"><tt>");
1934             sb.append(Escape.htmlElementContent(filename));
1935             if (childResource.isDirectory())
1936                 sb.append("/");
1937             sb.append("</tt></a></td>\r\n");
1938
1939             sb.append("<td align=\"right\"><tt>");
1940             if (childResource.isDirectory())
1941                 sb.append("&nbsp;");
1942             else
1943                 sb.append(renderSize(childResource.getContentLength()));
1944             sb.append("</tt></td>\r\n");
1945
1946             sb.append("<td align=\"right\"><tt>");
1947             sb.append(childResource.getLastModifiedHttp());
1948             sb.append("</tt></td>\r\n");
1949
1950             sb.append("</tr>\r\n");
1951         }
1952
1953         // Render the page footer
1954         sb.append("</table>\r\n");
1955
1956         sb.append("<hr class=\"line\">");
1957
1958         String readme = getReadme(resource, encoding);
1959         if (readme!=null) {
1960             sb.append(readme);
1961             sb.append("<hr class=\"line\">");
1962         }
1963
1964         if (showServerInfo) {
1965             sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
1966         }
1967         sb.append("</body>\r\n");
1968         sb.append("</html>\r\n");
1969
1970         // Return an input stream to the underlying bytes
1971         writer.write(sb.toString());
1972         writer.flush();
1973         return new ByteArrayInputStream(stream.toByteArray());
1974
1975     }
1976
1977
1978     /**
1979      * Render the specified file size (in bytes).
1980      *
1981      * @param size File size (in bytes)
1982      * @return the formatted size
1983      */

1984     protected String renderSize(long size) {
1985
1986         long leftSide = size / 1024;
1987         long rightSide = (size % 1024) / 103;   // Makes 1 digit
1988         if ((leftSide == 0) && (rightSide == 0) && (size > 0))
1989             rightSide = 1;
1990
1991         return ("" + leftSide + "." + rightSide + " kb");
1992
1993     }
1994
1995
1996     /**
1997      * Get the readme file as a string.
1998      * @param directory The directory to search
1999      * @param encoding The readme encoding
2000      * @return the readme for the specified directory
2001      */

2002     protected String getReadme(WebResource directory, String encoding) {
2003
2004         if (readmeFile != null) {
2005             WebResource resource = resources.getResource(
2006                     directory.getWebappPath() + readmeFile);
2007             if (resource.isFile()) {
2008                 StringWriter buffer = new StringWriter();
2009                 InputStreamReader reader = null;
2010                 try (InputStream is = resource.getInputStream()){
2011                     if (encoding != null) {
2012                         reader = new InputStreamReader(is, encoding);
2013                     } else {
2014                         reader = new InputStreamReader(is);
2015                     }
2016                     copyRange(reader, new PrintWriter(buffer));
2017                 } catch (IOException e) {
2018                     log(sm.getString("defaultServlet.readerCloseFailed"), e);
2019                 } finally {
2020                     if (reader != null) {
2021                         try {
2022                             reader.close();
2023                         } catch (IOException e) {
2024                         }
2025                     }
2026                 }
2027                 return buffer.toString();
2028             } else {
2029                 if (debug > 10)
2030                     log("readme '" + readmeFile + "' not found");
2031
2032                 return null;
2033             }
2034         }
2035
2036         return null;
2037     }
2038
2039
2040     /**
2041      * Return a Source for the xsl template (if possible).
2042      * @param directory The directory to search
2043      * @return the source for the specified directory
2044      * @throws IOException an IO error occurred
2045      */

2046     protected Source findXsltSource(WebResource directory)
2047         throws IOException {
2048
2049         if (localXsltFile != null) {
2050             WebResource resource = resources.getResource(
2051                     directory.getWebappPath() + localXsltFile);
2052             if (resource.isFile()) {
2053                 InputStream is = resource.getInputStream();
2054                 if (is != null) {
2055                     if (Globals.IS_SECURITY_ENABLED) {
2056                         return secureXslt(is);
2057                     } else {
2058                         return new StreamSource(is);
2059                     }
2060                 }
2061             }
2062             if (debug > 10) {
2063                 log("localXsltFile '" + localXsltFile + "' not found");
2064             }
2065         }
2066
2067         if (contextXsltFile != null) {
2068             InputStream is =
2069                 getServletContext().getResourceAsStream(contextXsltFile);
2070             if (is != null) {
2071                 if (Globals.IS_SECURITY_ENABLED) {
2072                     return secureXslt(is);
2073                 } else {
2074                     return new StreamSource(is);
2075                 }
2076             }
2077
2078             if (debug > 10)
2079                 log("contextXsltFile '" + contextXsltFile + "' not found");
2080         }
2081
2082         /*  Open and read in file in one fell swoop to reduce chance
2083          *  chance of leaving handle open.
2084          */

2085         if (globalXsltFile != null) {
2086             File f = validateGlobalXsltFile();
2087             if (f != null) {
2088                 long globalXsltFileSize = f.length();
2089                 if (globalXsltFileSize > Integer.MAX_VALUE) {
2090                     log("globalXsltFile [" + f.getAbsolutePath() + "] is too big to buffer");
2091                 } else {
2092                     try (FileInputStream fis = new FileInputStream(f)){
2093                         byte b[] = new byte[(int)f.length()];
2094                         IOTools.readFully(fis, b);
2095                         return new StreamSource(new ByteArrayInputStream(b));
2096                     }
2097                 }
2098             }
2099         }
2100
2101         return null;
2102     }
2103
2104
2105     private File validateGlobalXsltFile() {
2106         Context context = resources.getContext();
2107
2108         File baseConf = new File(context.getCatalinaBase(), "conf");
2109         File result = validateGlobalXsltFile(baseConf);
2110         if (result == null) {
2111             File homeConf = new File(context.getCatalinaHome(), "conf");
2112             if (!baseConf.equals(homeConf)) {
2113                 result = validateGlobalXsltFile(homeConf);
2114             }
2115         }
2116
2117         return result;
2118     }
2119
2120
2121     private File validateGlobalXsltFile(File base) {
2122         File candidate = new File(globalXsltFile);
2123         if (!candidate.isAbsolute()) {
2124             candidate = new File(base, globalXsltFile);
2125         }
2126
2127         if (!candidate.isFile()) {
2128             return null;
2129         }
2130
2131         // First check that the resulting path is under the provided base
2132         try {
2133             if (!candidate.getCanonicalPath().startsWith(base.getCanonicalPath())) {
2134                 return null;
2135             }
2136         } catch (IOException ioe) {
2137             return null;
2138         }
2139
2140         // Next check that an .xsl or .xslt file has been specified
2141         String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH);
2142         if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) {
2143             return null;
2144         }
2145
2146         return candidate;
2147     }
2148
2149
2150     private Source secureXslt(InputStream is) {
2151         // Need to filter out any external entities
2152         Source result = null;
2153         try {
2154             DocumentBuilder builder = factory.newDocumentBuilder();
2155             builder.setEntityResolver(secureEntityResolver);
2156             Document document = builder.parse(is);
2157             result = new DOMSource(document);
2158         } catch (ParserConfigurationException | SAXException | IOException e) {
2159             if (debug > 0) {
2160                 log(e.getMessage(), e);
2161             }
2162         } finally {
2163             if (is != null) {
2164                 try {
2165                     is.close();
2166                 } catch (IOException e) {
2167                     // Ignore
2168                 }
2169             }
2170         }
2171         return result;
2172     }
2173
2174
2175     // -------------------------------------------------------- protected Methods
2176
2177     /**
2178      * Check if sendfile can be used.
2179      * @param request The Servlet request
2180      * @param response The Servlet response
2181      * @param resource The resource
2182      * @param length The length which will be written (will be used only if
2183      *  range is null)
2184      * @param range The range that will be written
2185      * @return <code>true</code> if sendfile should be used (writing is then
2186      *  delegated to the endpoint)
2187      */

2188     protected boolean checkSendfile(HttpServletRequest request,
2189                                   HttpServletResponse response,
2190                                   WebResource resource,
2191                                   long length, Range range) {
2192         String canonicalPath;
2193         if (sendfileSize > 0
2194             && length > sendfileSize
2195             && (Boolean.TRUE.equals(request.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR)))
2196             && (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade"))
2197             && (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade"))
2198             && resource.isFile()
2199             && ((canonicalPath = resource.getCanonicalPath()) != null)
2200             ) {
2201             request.setAttribute(Globals.SENDFILE_FILENAME_ATTR, canonicalPath);
2202             if (range == null) {
2203                 request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0L));
2204                 request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(length));
2205             } else {
2206                 request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(range.start));
2207                 request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(range.end + 1));
2208             }
2209             return true;
2210         }
2211         return false;
2212     }
2213
2214
2215     /**
2216      * Check if the if-match condition is satisfied.
2217      *
2218      * @param request   The servlet request we are processing
2219      * @param response  The servlet response we are creating
2220      * @param resource  The resource
2221      * @return <code>true</code> if the resource meets the specified condition,
2222      *  and <code>false</code> if the condition is not satisfied, in which case
2223      *  request processing is stopped
2224      * @throws IOException an IO error occurred
2225      */

2226     protected boolean checkIfMatch(HttpServletRequest request,
2227             HttpServletResponse response, WebResource resource)
2228             throws IOException {
2229
2230         String eTag = resource.getETag();
2231         String headerValue = request.getHeader("If-Match");
2232         if (headerValue != null) {
2233             if (headerValue.indexOf('*') == -1) {
2234
2235                 StringTokenizer commaTokenizer = new StringTokenizer
2236                     (headerValue, ",");
2237                 boolean conditionSatisfied = false;
2238
2239                 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2240                     String currentToken = commaTokenizer.nextToken();
2241                     if (currentToken.trim().equals(eTag))
2242                         conditionSatisfied = true;
2243                 }
2244
2245                 // If none of the given ETags match, 412 Precondition failed is
2246                 // sent back
2247                 if (!conditionSatisfied) {
2248                     response.sendError
2249                         (HttpServletResponse.SC_PRECONDITION_FAILED);
2250                     return false;
2251                 }
2252
2253             }
2254         }
2255         return true;
2256     }
2257
2258
2259     /**
2260      * Check if the if-modified-since condition is satisfied.
2261      *
2262      * @param request   The servlet request we are processing
2263      * @param response  The servlet response we are creating
2264      * @param resource  The resource
2265      * @return <code>true</code> if the resource meets the specified condition,
2266      *  and <code>false</code> if the condition is not satisfied, in which case
2267      *  request processing is stopped
2268      */

2269     protected boolean checkIfModifiedSince(HttpServletRequest request,
2270             HttpServletResponse response, WebResource resource) {
2271         try {
2272             long headerValue = request.getDateHeader("If-Modified-Since");
2273             long lastModified = resource.getLastModified();
2274             if (headerValue != -1) {
2275
2276                 // If an If-None-Match header has been specified, if modified since
2277                 // is ignored.
2278                 if ((request.getHeader("If-None-Match") == null)
2279                     && (lastModified < headerValue + 1000)) {
2280                     // The entity has not been modified since the date
2281                     // specified by the client. This is not an error case.
2282                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2283                     response.setHeader("ETag", resource.getETag());
2284
2285                     return false;
2286                 }
2287             }
2288         } catch (IllegalArgumentException illegalArgument) {
2289             return true;
2290         }
2291         return true;
2292     }
2293
2294
2295     /**
2296      * Check if the if-none-match condition is satisfied.
2297      *
2298      * @param request   The servlet request we are processing
2299      * @param response  The servlet response we are creating
2300      * @param resource  The resource
2301      * @return <code>true</code> if the resource meets the specified condition,
2302      *  and <code>false</code> if the condition is not satisfied, in which case
2303      *  request processing is stopped
2304      * @throws IOException an IO error occurred
2305      */

2306     protected boolean checkIfNoneMatch(HttpServletRequest request,
2307             HttpServletResponse response, WebResource resource)
2308             throws IOException {
2309
2310         String eTag = resource.getETag();
2311         String headerValue = request.getHeader("If-None-Match");
2312         if (headerValue != null) {
2313
2314             boolean conditionSatisfied = false;
2315
2316             if (!headerValue.equals("*")) {
2317
2318                 StringTokenizer commaTokenizer =
2319                     new StringTokenizer(headerValue, ",");
2320
2321                 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2322                     String currentToken = commaTokenizer.nextToken();
2323                     if (currentToken.trim().equals(eTag))
2324                         conditionSatisfied = true;
2325                 }
2326
2327             } else {
2328                 conditionSatisfied = true;
2329             }
2330
2331             if (conditionSatisfied) {
2332
2333                 // For GET and HEAD, we should respond with
2334                 // 304 Not Modified.
2335                 // For every other method, 412 Precondition Failed is sent
2336                 // back.
2337                 if ( ("GET".equals(request.getMethod()))
2338                      || ("HEAD".equals(request.getMethod())) ) {
2339                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2340                     response.setHeader("ETag", eTag);
2341
2342                     return false;
2343                 }
2344                 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2345                 return false;
2346             }
2347         }
2348         return true;
2349     }
2350
2351     /**
2352      * Check if the if-unmodified-since condition is satisfied.
2353      *
2354      * @param request   The servlet request we are processing
2355      * @param response  The servlet response we are creating
2356      * @param resource  The resource
2357      * @return <code>true</code> if the resource meets the specified condition,
2358      *  and <code>false</code> if the condition is not satisfied, in which case
2359      *  request processing is stopped
2360      * @throws IOException an IO error occurred
2361      */

2362     protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
2363             HttpServletResponse response, WebResource resource)
2364             throws IOException {
2365         try {
2366             long lastModified = resource.getLastModified();
2367             long headerValue = request.getDateHeader("If-Unmodified-Since");
2368             if (headerValue != -1) {
2369                 if ( lastModified >= (headerValue + 1000)) {
2370                     // The entity has not been modified since the date
2371                     // specified by the client. This is not an error case.
2372                     response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2373                     return false;
2374                 }
2375             }
2376         } catch(IllegalArgumentException illegalArgument) {
2377             return true;
2378         }
2379         return true;
2380     }
2381
2382
2383     /**
2384      * Copy the contents of the specified input stream to the specified
2385      * output stream, and ensure that both streams are closed before returning
2386      * (even in the face of an exception).
2387      *
2388      * @param is        The input stream to read the source resource from
2389      * @param ostream   The output stream to write to
2390      *
2391      * @exception IOException if an input/output error occurs
2392      */

2393     protected void copy(InputStream is, ServletOutputStream ostream) throws IOException {
2394
2395         IOException exception = null;
2396         InputStream istream = new BufferedInputStream(is, input);
2397
2398         // Copy the input stream to the output stream
2399         exception = copyRange(istream, ostream);
2400
2401         // Clean up the input stream
2402         istream.close();
2403
2404         // Rethrow any exception that has occurred
2405         if (exception != null)
2406             throw exception;
2407     }
2408
2409
2410     /**
2411      * Copy the contents of the specified input stream to the specified
2412      * output stream, and ensure that both streams are closed before returning
2413      * (even in the face of an exception).
2414      *
2415      * @param is        The input stream to read the source resource from
2416      * @param writer    The writer to write to
2417      * @param encoding  The encoding to use when reading the source input stream
2418      *
2419      * @exception IOException if an input/output error occurs
2420      */

2421     protected void copy(InputStream is, PrintWriter writer, String encoding) throws IOException {
2422         IOException exception = null;
2423
2424         Reader reader;
2425         if (encoding == null) {
2426             reader = new InputStreamReader(is);
2427         } else {
2428             reader = new InputStreamReader(is, encoding);
2429         }
2430
2431         // Copy the input stream to the output stream
2432         exception = copyRange(reader, writer);
2433
2434         // Clean up the reader
2435         reader.close();
2436
2437         // Rethrow any exception that has occurred
2438         if (exception != null) {
2439             throw exception;
2440         }
2441     }
2442
2443
2444     /**
2445      * Copy the contents of the specified input stream to the specified
2446      * output stream, and ensure that both streams are closed before returning
2447      * (even in the face of an exception).
2448      *
2449      * @param resource  The source resource
2450      * @param ostream   The output stream to write to
2451      * @param range     Range the client wanted to retrieve
2452      * @exception IOException if an input/output error occurs
2453      */

2454     protected void copy(WebResource resource, ServletOutputStream ostream,
2455                       Range range)
2456         throws IOException {
2457
2458         IOException exception = null;
2459
2460         InputStream resourceInputStream = resource.getInputStream();
2461         InputStream istream =
2462             new BufferedInputStream(resourceInputStream, input);
2463         exception = copyRange(istream, ostream, range.start, range.end);
2464
2465         // Clean up the input stream
2466         istream.close();
2467
2468         // Rethrow any exception that has occurred
2469         if (exception != null)
2470             throw exception;
2471
2472     }
2473
2474
2475     /**
2476      * Copy the contents of the specified input stream to the specified
2477      * output stream, and ensure that both streams are closed before returning
2478      * (even in the face of an exception).
2479      *
2480      * @param resource      The source resource
2481      * @param ostream       The output stream to write to
2482      * @param ranges        Enumeration of the ranges the client wanted to
2483      *                          retrieve
2484      * @param contentType   Content type of the resource
2485      * @exception IOException if an input/output error occurs
2486      */

2487     protected void copy(WebResource resource, ServletOutputStream ostream,
2488                       Iterator<Range> ranges, String contentType)
2489         throws IOException {
2490
2491         IOException exception = null;
2492
2493         while ( (exception == null) && (ranges.hasNext()) ) {
2494
2495             InputStream resourceInputStream = resource.getInputStream();
2496             try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {
2497
2498                 Range currentRange = ranges.next();
2499
2500                 // Writing MIME header.
2501                 ostream.println();
2502                 ostream.println("--" + mimeSeparation);
2503                 if (contentType != null)
2504                     ostream.println("Content-Type: " + contentType);
2505                 ostream.println("Content-Range: bytes " + currentRange.start
2506                                + "-" + currentRange.end + "/"
2507                                + currentRange.length);
2508                 ostream.println();
2509
2510                 // Printing content
2511                 exception = copyRange(istream, ostream, currentRange.start,
2512                                       currentRange.end);
2513             }
2514         }
2515
2516         ostream.println();
2517         ostream.print("--" + mimeSeparation + "--");
2518
2519         // Rethrow any exception that has occurred
2520         if (exception != null)
2521             throw exception;
2522
2523     }
2524
2525
2526     /**
2527      * Copy the contents of the specified input stream to the specified
2528      * output stream, and ensure that both streams are closed before returning
2529      * (even in the face of an exception).
2530      *
2531      * @param istream The input stream to read from
2532      * @param ostream The output stream to write to
2533      * @return Exception which occurred during processing
2534      */

2535     protected IOException copyRange(InputStream istream,
2536                                   ServletOutputStream ostream) {
2537
2538         // Copy the input stream to the output stream
2539         IOException exception = null;
2540         byte buffer[] = new byte[input];
2541         int len = buffer.length;
2542         while (true) {
2543             try {
2544                 len = istream.read(buffer);
2545                 if (len == -1)
2546                     break;
2547                 ostream.write(buffer, 0, len);
2548             } catch (IOException e) {
2549                 exception = e;
2550                 len = -1;
2551                 break;
2552             }
2553         }
2554         return exception;
2555
2556     }
2557
2558
2559     /**
2560      * Copy the contents of the specified input stream to the specified
2561      * output stream, and ensure that both streams are closed before returning
2562      * (even in the face of an exception).
2563      *
2564      * @param reader The reader to read from
2565      * @param writer The writer to write to
2566      * @return Exception which occurred during processing
2567      */

2568     protected IOException copyRange(Reader reader, PrintWriter writer) {
2569
2570         // Copy the input stream to the output stream
2571         IOException exception = null;
2572         char buffer[] = new char[input];
2573         int len = buffer.length;
2574         while (true) {
2575             try {
2576                 len = reader.read(buffer);
2577                 if (len == -1)
2578                     break;
2579                 writer.write(buffer, 0, len);
2580             } catch (IOException e) {
2581                 exception = e;
2582                 len = -1;
2583                 break;
2584             }
2585         }
2586         return exception;
2587
2588     }
2589
2590
2591     /**
2592      * Copy the contents of the specified input stream to the specified
2593      * output stream, and ensure that both streams are closed before returning
2594      * (even in the face of an exception).
2595      *
2596      * @param istream The input stream to read from
2597      * @param ostream The output stream to write to
2598      * @param start Start of the range which will be copied
2599      * @param end End of the range which will be copied
2600      * @return Exception which occurred during processing
2601      */

2602     protected IOException copyRange(InputStream istream,
2603                                   ServletOutputStream ostream,
2604                                   long start, long end) {
2605
2606         if (debug > 10)
2607             log("Serving bytes:" + start + "-" + end);
2608
2609         long skipped = 0;
2610         try {
2611             skipped = istream.skip(start);
2612         } catch (IOException e) {
2613             return e;
2614         }
2615         if (skipped < start) {
2616             return new IOException(sm.getString("defaultServlet.skipfail",
2617                     Long.valueOf(skipped), Long.valueOf(start)));
2618         }
2619
2620         IOException exception = null;
2621         long bytesToRead = end - start + 1;
2622
2623         byte buffer[] = new byte[input];
2624         int len = buffer.length;
2625         while ( (bytesToRead > 0) && (len >= buffer.length)) {
2626             try {
2627                 len = istream.read(buffer);
2628                 if (bytesToRead >= len) {
2629                     ostream.write(buffer, 0, len);
2630                     bytesToRead -= len;
2631                 } else {
2632                     ostream.write(buffer, 0, (int) bytesToRead);
2633                     bytesToRead = 0;
2634                 }
2635             } catch (IOException e) {
2636                 exception = e;
2637                 len = -1;
2638             }
2639             if (len < buffer.length)
2640                 break;
2641         }
2642
2643         return exception;
2644
2645     }
2646
2647
2648     protected static class Range {
2649
2650         public long start;
2651         public long end;
2652         public long length;
2653
2654         /**
2655          * Validate range.
2656          *
2657          * @return true if the range is valid, otherwise false
2658          */

2659         public boolean validate() {
2660             if (end >= length)
2661                 end = length - 1;
2662             return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
2663         }
2664     }
2665
2666     protected static class CompressionFormat implements Serializable {
2667         private static final long serialVersionUID = 1L;
2668         public final String extension;
2669         public final String encoding;
2670
2671         public CompressionFormat(String extension, String encoding) {
2672             this.extension = extension;
2673             this.encoding = encoding;
2674         }
2675     }
2676
2677     private static class PrecompressedResource {
2678         public final WebResource resource;
2679         public final CompressionFormat format;
2680
2681         private PrecompressedResource(WebResource resource, CompressionFormat format) {
2682             this.resource = resource;
2683             this.format = format;
2684         }
2685     }
2686
2687     /**
2688      * This is secure in the sense that any attempt to use an external entity
2689      * will trigger an exception.
2690      */

2691     private static class SecureEntityResolver implements EntityResolver2  {
2692
2693         @Override
2694         public InputSource resolveEntity(String publicId, String systemId)
2695                 throws SAXException, IOException {
2696             throw new SAXException(sm.getString("defaultServlet.blockExternalEntity",
2697                     publicId, systemId));
2698         }
2699
2700         @Override
2701         public InputSource getExternalSubset(String name, String baseURI)
2702                 throws SAXException, IOException {
2703             throw new SAXException(sm.getString("defaultServlet.blockExternalSubset",
2704                     name, baseURI));
2705         }
2706
2707         @Override
2708         public InputSource resolveEntity(String name, String publicId,
2709                 String baseURI, String systemId) throws SAXException,
2710                 IOException {
2711             throw new SAXException(sm.getString("defaultServlet.blockExternalEntity2",
2712                     name, publicId, baseURI, systemId));
2713         }
2714     }
2715
2716     /**
2717      * Gets the ordering character to be used for a particular column.
2718      *
2719      * @param order  The order that is currently being applied
2720      * @param column The column that will be rendered.
2721      *
2722      * @return Either 'A' or 'D', to indicate "ascending" or "descending" sort
2723      *         order.
2724      */

2725     private char getOrderChar(SortManager.Order order, char column) {
2726         if(column == order.column) {
2727             if(order.ascending) {
2728                 return 'D';
2729             } else {
2730                 return 'A';
2731             }
2732         } else {
2733             return 'D';
2734         }
2735     }
2736
2737     /**
2738      * A class encapsulating the sorting of resources.
2739      */

2740     private static class SortManager
2741     {
2742         /**
2743          * The default sort.
2744          */

2745         protected Comparator<WebResource> defaultResourceComparator;
2746
2747         /**
2748          * Comparator to use when sorting resources by name.
2749          */

2750         protected Comparator<WebResource> resourceNameComparator;
2751
2752         /**
2753          * Comparator to use when sorting files by name, ascending (reverse).
2754          */

2755         protected Comparator<WebResource> resourceNameComparatorAsc;
2756
2757         /**
2758          * Comparator to use when sorting resources by size.
2759          */

2760         protected Comparator<WebResource> resourceSizeComparator;
2761
2762         /**
2763          * Comparator to use when sorting files by size, ascending (reverse).
2764          */

2765         protected Comparator<WebResource> resourceSizeComparatorAsc;
2766
2767         /**
2768          * Comparator to use when sorting resources by last-modified date.
2769          */

2770         protected Comparator<WebResource> resourceLastModifiedComparator;
2771
2772         /**
2773          * Comparator to use when sorting files by last-modified date, ascending (reverse).
2774          */

2775         protected Comparator<WebResource> resourceLastModifiedComparatorAsc;
2776
2777         public SortManager(boolean directoriesFirst) {
2778             resourceNameComparator = new ResourceNameComparator();
2779             resourceNameComparatorAsc = Collections.reverseOrder(resourceNameComparator);
2780             resourceSizeComparator = new ResourceSizeComparator(resourceNameComparator);
2781             resourceSizeComparatorAsc = Collections.reverseOrder(resourceSizeComparator);
2782             resourceLastModifiedComparator = new ResourceLastModifiedDateComparator(resourceNameComparator);
2783             resourceLastModifiedComparatorAsc = Collections.reverseOrder(resourceLastModifiedComparator);
2784
2785             if(directoriesFirst) {
2786                 resourceNameComparator = new DirsFirstComparator(resourceNameComparator);
2787                 resourceNameComparatorAsc = new DirsFirstComparator(resourceNameComparatorAsc);
2788                 resourceSizeComparator = new DirsFirstComparator(resourceSizeComparator);
2789                 resourceSizeComparatorAsc = new DirsFirstComparator(resourceSizeComparatorAsc);
2790                 resourceLastModifiedComparator = new DirsFirstComparator(resourceLastModifiedComparator);
2791                 resourceLastModifiedComparatorAsc = new DirsFirstComparator(resourceLastModifiedComparatorAsc);
2792             }
2793
2794             defaultResourceComparator = resourceNameComparator;
2795         }
2796
2797         /**
2798          * Sorts an array of resources according to an ordering string.
2799          *
2800          * @param resources The array to sort.
2801          * @param order     The ordering string.
2802          *
2803          * @see #getOrder(String)
2804          */

2805         public void sort(WebResource[] resources, String order) {
2806             Comparator<WebResource> comparator = getComparator(order);
2807
2808             if(null != comparator)
2809                 Arrays.sort(resources, comparator);
2810         }
2811
2812         public Comparator<WebResource> getComparator(String order) {
2813             return getComparator(getOrder(order));
2814         }
2815
2816         public Comparator<WebResource> getComparator(Order order) {
2817             if(null == order)
2818                 return defaultResourceComparator;
2819
2820             if('N' == order.column) {
2821                 if(order.ascending) {
2822                     return resourceNameComparatorAsc;
2823                 } else {
2824                     return resourceNameComparator;
2825                 }
2826             }
2827
2828             if('S' == order.column) {
2829                 if(order.ascending) {
2830                     return resourceSizeComparatorAsc;
2831                 } else {
2832                     return resourceSizeComparator;
2833                 }
2834             }
2835
2836             if('M' == order.column) {
2837                 if(order.ascending) {
2838                     return resourceLastModifiedComparatorAsc;
2839                 } else {
2840                     return resourceLastModifiedComparator;
2841                 }
2842             }
2843
2844             return defaultResourceComparator;
2845         }
2846
2847         /**
2848          * Gets the Order to apply given an ordering-string. This
2849          * ordering-string matches a subset of the ordering-strings
2850          * supported by
2851          * <a href="https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query">Apache httpd</a>.
2852          *
2853          * @param order The ordering-string provided by the client.
2854          *
2855          * @return An Order specifying the column and ascending/descending to
2856          *         be applied to resources.
2857          */

2858         public Order getOrder(String order) {
2859             if(null == order || 0 == order.trim().length())
2860                 return Order.DEFAULT;
2861
2862             String[] options = order.split(";");
2863
2864             if(0 == options.length)
2865                 return Order.DEFAULT;
2866
2867             char column = '\0';
2868             boolean ascending = false;
2869
2870             for(String option : options) {
2871                 option = option.trim();
2872
2873                 if(2 < option.length()) {
2874                     char opt = option.charAt(0);
2875                     if('C' == opt)
2876                         column = option.charAt(2);
2877                     else if('O' == opt)
2878                         ascending = ('A' == option.charAt(2));
2879                 }
2880             }
2881
2882             if('N' == column) {
2883                 if(ascending) {
2884                     return Order.NAME_ASC;
2885                 } else {
2886                     return Order.NAME;
2887                 }
2888             }
2889
2890             if('S' == column) {
2891                 if(ascending) {
2892                     return Order.SIZE_ASC;
2893                 } else {
2894                     return Order.SIZE;
2895                 }
2896             }
2897
2898             if('M' == column) {
2899                 if(ascending) {
2900                     return Order.LAST_MODIFIED_ASC;
2901                 } else {
2902                     return Order.LAST_MODIFIED;
2903                 }
2904             }
2905
2906             return Order.DEFAULT;
2907         }
2908
2909         public static class Order {
2910             final char column;
2911             final boolean ascending;
2912
2913             public Order(char column, boolean ascending) {
2914                 this.column = column;
2915                 this.ascending = ascending;
2916             }
2917
2918             public static final Order NAME = new Order('N', false);
2919             public static final Order NAME_ASC = new Order('N', true);
2920             public static final Order SIZE = new Order('S', false);
2921             public static final Order SIZE_ASC = new Order('S', true);
2922             public static final Order LAST_MODIFIED = new Order('M', false);
2923             public static final Order LAST_MODIFIED_ASC = new Order('M', true);
2924
2925             public static final Order DEFAULT = NAME;
2926         }
2927     }
2928
2929     private static class DirsFirstComparator
2930         implements Comparator<WebResource>
2931     {
2932         private final Comparator<WebResource> base;
2933
2934         public DirsFirstComparator(Comparator<WebResource> core) {
2935             this.base = core;
2936         }
2937
2938         @Override
2939         public int compare(WebResource r1, WebResource r2) {
2940             if(r1.isDirectory()) {
2941                 if(r2.isDirectory()) {
2942                     return base.compare(r1, r2);
2943                 } else {
2944                     return -1; // r1, directory, first
2945                 }
2946             } else if(r2.isDirectory()) {
2947                 return 1; // r2, directory, first
2948             } else {
2949                 return base.compare(r1, r2);
2950             }
2951         }
2952     }
2953
2954     private static class ResourceNameComparator
2955         implements Comparator<WebResource>
2956     {
2957         @Override
2958         public int compare(WebResource r1, WebResource r2) {
2959             return r1.getName().compareTo(r2.getName());
2960         }
2961     }
2962
2963     private static class ResourceSizeComparator
2964         implements Comparator<WebResource>
2965     {
2966         private Comparator<WebResource> base;
2967
2968         public ResourceSizeComparator(Comparator<WebResource> base) {
2969             this.base = base;
2970         }
2971
2972         @Override
2973         public int compare(WebResource r1, WebResource r2) {
2974             int c = Long.compare(r1.getContentLength(), r2.getContentLength());
2975
2976             if(0 == c)
2977                 return base.compare(r1, r2);
2978             else
2979                 return c;
2980         }
2981     }
2982
2983     private static class ResourceLastModifiedDateComparator
2984         implements Comparator<WebResource>
2985     {
2986         private Comparator<WebResource> base;
2987
2988         public ResourceLastModifiedDateComparator(Comparator<WebResource> base) {
2989             this.base = base;
2990         }
2991
2992         @Override
2993         public int compare(WebResource r1, WebResource r2) {
2994             int c = Long.compare(r1.getLastModified(), r2.getLastModified());
2995
2996             if(0 == c)
2997                 return base.compare(r1, r2);
2998             else
2999                 return c;
3000         }
3001     }
3002 }
3003