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
18
19 package org.apache.catalina.servlets;
20
21
22 import java.io.BufferedInputStream;
23 import java.io.ByteArrayInputStream;
24 import java.io.ByteArrayOutputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.io.OutputStreamWriter;
31 import java.io.PrintWriter;
32 import java.io.RandomAccessFile;
33 import java.io.Reader;
34 import java.io.StringReader;
35 import java.io.StringWriter;
36 import java.util.ArrayList;
37 import java.util.Iterator;
38 import java.util.StringTokenizer;
39
40 import javax.naming.InitialContext;
41 import javax.naming.NameClassPair;
42 import javax.naming.NamingEnumeration;
43 import javax.naming.NamingException;
44 import javax.naming.directory.DirContext;
45 import javax.servlet.ServletException;
46 import javax.servlet.ServletOutputStream;
47 import javax.servlet.UnavailableException;
48 import javax.servlet.http.HttpServlet;
49 import javax.servlet.http.HttpServletRequest;
50 import javax.servlet.http.HttpServletResponse;
51 import javax.xml.transform.Source;
52 import javax.xml.transform.Transformer;
53 import javax.xml.transform.TransformerException;
54 import javax.xml.transform.TransformerFactory;
55 import javax.xml.transform.stream.StreamResult;
56 import javax.xml.transform.stream.StreamSource;
57
58 import org.apache.catalina.Globals;
59 import org.apache.catalina.util.RequestUtil;
60 import org.apache.catalina.util.ServerInfo;
61 import org.apache.catalina.util.StringManager;
62 import org.apache.catalina.util.URLEncoder;
63 import org.apache.naming.resources.CacheEntry;
64 import org.apache.naming.resources.ProxyDirContext;
65 import org.apache.naming.resources.Resource;
66 import org.apache.naming.resources.ResourceAttributes;
67
68
69 /**
70 * The default resource-serving servlet for most web applications,
71 * used to serve static resources such as HTML pages and images.
72 *
73 * @author Craig R. McClanahan
74 * @author Remy Maucherat
75 * @version $Revision: 543680 $ $Date: 2007-06-02 02:42:36 +0200 (sam., 02 juin 2007) $
76 */
77
78 public class DefaultServlet
79 extends HttpServlet {
80
81
82 // ----------------------------------------------------- Instance Variables
83
84
85 /**
86 * The debugging detail level for this servlet.
87 */
88 protected int debug = 0;
89
90
91 /**
92 * The input buffer size to use when serving resources.
93 */
94 protected int input = 2048;
95
96
97 /**
98 * Should we generate directory listings?
99 */
100 protected boolean listings = false;
101
102
103 /**
104 * Read only flag. By default, it's set to true.
105 */
106 protected boolean readOnly = true;
107
108
109 /**
110 * The output buffer size to use when serving resources.
111 */
112 protected int output = 2048;
113
114
115 /**
116 * Array containing the safe characters set.
117 */
118 protected static URLEncoder urlEncoder;
119
120
121 /**
122 * Allow customized directory listing per directory.
123 */
124 protected String localXsltFile = null;
125
126
127 /**
128 * Allow customized directory listing per instance.
129 */
130 protected String globalXsltFile = null;
131
132
133 /**
134 * Allow a readme file to be included.
135 */
136 protected String readmeFile = null;
137
138
139 /**
140 * Proxy directory context.
141 */
142 protected ProxyDirContext resources = null;
143
144
145 /**
146 * File encoding to be used when reading static files. If none is specified
147 * the platform default is used.
148 */
149 protected String fileEncoding = null;
150
151
152 /**
153 * Minimum size for sendfile usage in bytes.
154 */
155 protected int sendfileSize = 48 * 1024;
156
157
158 /**
159 * Full range marker.
160 */
161 protected static ArrayList FULL = new ArrayList();
162
163
164 // ----------------------------------------------------- Static Initializer
165
166
167 /**
168 * GMT timezone - all HTTP dates are on GMT
169 */
170 static {
171 urlEncoder = new URLEncoder();
172 urlEncoder.addSafeCharacter('-');
173 urlEncoder.addSafeCharacter('_');
174 urlEncoder.addSafeCharacter('.');
175 urlEncoder.addSafeCharacter('*');
176 urlEncoder.addSafeCharacter('/');
177 }
178
179
180 /**
181 * MIME multipart separation string
182 */
183 protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";
184
185
186 /**
187 * JNDI resources name.
188 */
189 protected static final String RESOURCES_JNDI_NAME = "java:/comp/Resources";
190
191
192 /**
193 * The string manager for this package.
194 */
195 protected static StringManager sm =
196 StringManager.getManager(Constants.Package);
197
198
199 /**
200 * Size of file transfer buffer in bytes.
201 */
202 protected static final int BUFFER_SIZE = 4096;
203
204
205 // --------------------------------------------------------- Public Methods
206
207
208 /**
209 * Finalize this servlet.
210 */
211 public void destroy() {
212 }
213
214
215 /**
216 * Initialize this servlet.
217 */
218 public void init() throws ServletException {
219
220 if (getServletConfig().getInitParameter("debug") != null)
221 debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
222
223 if (getServletConfig().getInitParameter("input") != null)
224 input = Integer.parseInt(getServletConfig().getInitParameter("input"));
225
226 if (getServletConfig().getInitParameter("output") != null)
227 output = Integer.parseInt(getServletConfig().getInitParameter("output"));
228
229 listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings"));
230
231 if (getServletConfig().getInitParameter("readonly") != null)
232 readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
233
234 if (getServletConfig().getInitParameter("sendfileSize") != null)
235 sendfileSize =
236 Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024;
237
238 fileEncoding = getServletConfig().getInitParameter("fileEncoding");
239
240 globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
241 localXsltFile = getServletConfig().getInitParameter("localXsltFile");
242 readmeFile = getServletConfig().getInitParameter("readmeFile");
243
244 // Sanity check on the specified buffer sizes
245 if (input < 256)
246 input = 256;
247 if (output < 256)
248 output = 256;
249
250 if (debug > 0) {
251 log("DefaultServlet.init: input buffer size=" + input +
252 ", output buffer size=" + output);
253 }
254
255 // Load the proxy dir context.
256 resources = (ProxyDirContext) getServletContext()
257 .getAttribute(Globals.RESOURCES_ATTR);
258 if (resources == null) {
259 try {
260 resources =
261 (ProxyDirContext) new InitialContext()
262 .lookup(RESOURCES_JNDI_NAME);
263 } catch (NamingException e) {
264 // Failed
265 throw new ServletException("No resources", e);
266 }
267 }
268
269 if (resources == null) {
270 throw new UnavailableException("No resources");
271 }
272
273 }
274
275
276 // ------------------------------------------------------ Protected Methods
277
278
279 /**
280 * Return the relative path associated with this servlet.
281 *
282 * @param request The servlet request we are processing
283 */
284 protected String getRelativePath(HttpServletRequest request) {
285
286 // Are we being processed by a RequestDispatcher.include()?
287 if (request.getAttribute(Globals.INCLUDE_REQUEST_URI_ATTR) != null) {
288 String result = (String) request.getAttribute(
289 Globals.INCLUDE_PATH_INFO_ATTR);
290 if (result == null)
291 result = (String) request.getAttribute(
292 Globals.INCLUDE_SERVLET_PATH_ATTR);
293 if ((result == null) || (result.equals("")))
294 result = "/";
295 return (result);
296 }
297
298 // No, extract the desired path directly from the request
299 String result = request.getPathInfo();
300 if (result == null) {
301 result = request.getServletPath();
302 }
303 if ((result == null) || (result.equals(""))) {
304 result = "/";
305 }
306 return (result);
307
308 }
309
310
311 /**
312 * Process a GET request for the specified resource.
313 *
314 * @param request The servlet request we are processing
315 * @param response The servlet response we are creating
316 *
317 * @exception IOException if an input/output error occurs
318 * @exception ServletException if a servlet-specified error occurs
319 */
320 protected void doGet(HttpServletRequest request,
321 HttpServletResponse response)
322 throws IOException, ServletException {
323
324 // Serve the requested resource, including the data content
325 serveResource(request, response, true);
326
327 }
328
329
330 /**
331 * Process a HEAD request for the specified resource.
332 *
333 * @param request The servlet request we are processing
334 * @param response The servlet response we are creating
335 *
336 * @exception IOException if an input/output error occurs
337 * @exception ServletException if a servlet-specified error occurs
338 */
339 protected void doHead(HttpServletRequest request,
340 HttpServletResponse response)
341 throws IOException, ServletException {
342
343 // Serve the requested resource, without the data content
344 serveResource(request, response, false);
345
346 }
347
348
349 /**
350 * Process a POST request for the specified resource.
351 *
352 * @param request The servlet request we are processing
353 * @param response The servlet response we are creating
354 *
355 * @exception IOException if an input/output error occurs
356 * @exception ServletException if a servlet-specified error occurs
357 */
358 protected void doPost(HttpServletRequest request,
359 HttpServletResponse response)
360 throws IOException, ServletException {
361 doGet(request, response);
362 }
363
364
365 /**
366 * Process a POST request for the specified resource.
367 *
368 * @param req The servlet request we are processing
369 * @param resp The servlet response we are creating
370 *
371 * @exception IOException if an input/output error occurs
372 * @exception ServletException if a servlet-specified error occurs
373 */
374 protected void doPut(HttpServletRequest req, HttpServletResponse resp)
375 throws ServletException, IOException {
376
377 if (readOnly) {
378 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
379 return;
380 }
381
382 String path = getRelativePath(req);
383
384 boolean exists = true;
385 try {
386 resources.lookup(path);
387 } catch (NamingException e) {
388 exists = false;
389 }
390
391 boolean result = true;
392
393 // Temp. content file used to support partial PUT
394 File contentFile = null;
395
396 Range range = parseContentRange(req, resp);
397
398 InputStream resourceInputStream = null;
399
400 // Append data specified in ranges to existing content for this
401 // resource - create a temp. file on the local filesystem to
402 // perform this operation
403 // Assume just one range is specified for now
404 if (range != null) {
405 contentFile = executePartialPut(req, range, path);
406 resourceInputStream = new FileInputStream(contentFile);
407 } else {
408 resourceInputStream = req.getInputStream();
409 }
410
411 try {
412 Resource newResource = new Resource(resourceInputStream);
413 // FIXME: Add attributes
414 if (exists) {
415 resources.rebind(path, newResource);
416 } else {
417 resources.bind(path, newResource);
418 }
419 } catch(NamingException e) {
420 result = false;
421 }
422
423 if (result) {
424 if (exists) {
425 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
426 } else {
427 resp.setStatus(HttpServletResponse.SC_CREATED);
428 }
429 } else {
430 resp.sendError(HttpServletResponse.SC_CONFLICT);
431 }
432
433 }
434
435
436 /**
437 * Handle a partial PUT. New content specified in request is appended to
438 * existing content in oldRevisionContent (if present). This code does
439 * not support simultaneous partial updates to the same resource.
440 */
441 protected File executePartialPut(HttpServletRequest req, Range range,
442 String path)
443 throws IOException {
444
445 // Append data specified in ranges to existing content for this
446 // resource - create a temp. file on the local filesystem to
447 // perform this operation
448 File tempDir = (File) getServletContext().getAttribute
449 ("javax.servlet.context.tempdir");
450 // Convert all '/' characters to '.' in resourcePath
451 String convertedResourcePath = path.replace('/', '.');
452 File contentFile = new File(tempDir, convertedResourcePath);
453 if (contentFile.createNewFile()) {
454 // Clean up contentFile when Tomcat is terminated
455 contentFile.deleteOnExit();
456 }
457
458 RandomAccessFile randAccessContentFile =
459 new RandomAccessFile(contentFile, "rw");
460
461 Resource oldResource = null;
462 try {
463 Object obj = resources.lookup(path);
464 if (obj instanceof Resource)
465 oldResource = (Resource) obj;
466 } catch (NamingException e) {
467 ;
468 }
469
470 // Copy data in oldRevisionContent to contentFile
471 if (oldResource != null) {
472 BufferedInputStream bufOldRevStream =
473 new BufferedInputStream(oldResource.streamContent(),
474 BUFFER_SIZE);
475
476 int numBytesRead;
477 byte[] copyBuffer = new byte[BUFFER_SIZE];
478 while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
479 randAccessContentFile.write(copyBuffer, 0, numBytesRead);
480 }
481
482 bufOldRevStream.close();
483 }
484
485 randAccessContentFile.setLength(range.length);
486
487 // Append data in request input stream to contentFile
488 randAccessContentFile.seek(range.start);
489 int numBytesRead;
490 byte[] transferBuffer = new byte[BUFFER_SIZE];
491 BufferedInputStream requestBufInStream =
492 new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
493 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
494 randAccessContentFile.write(transferBuffer, 0, numBytesRead);
495 }
496 randAccessContentFile.close();
497 requestBufInStream.close();
498
499 return contentFile;
500
501 }
502
503
504 /**
505 * Process a POST request for the specified resource.
506 *
507 * @param req The servlet request we are processing
508 * @param resp The servlet response we are creating
509 *
510 * @exception IOException if an input/output error occurs
511 * @exception ServletException if a servlet-specified error occurs
512 */
513 protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
514 throws ServletException, IOException {
515
516 if (readOnly) {
517 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
518 return;
519 }
520
521 String path = getRelativePath(req);
522
523 boolean exists = true;
524 try {
525 resources.lookup(path);
526 } catch (NamingException e) {
527 exists = false;
528 }
529
530 if (exists) {
531 boolean result = true;
532 try {
533 resources.unbind(path);
534 } catch (NamingException e) {
535 result = false;
536 }
537 if (result) {
538 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
539 } else {
540 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
541 }
542 } else {
543 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
544 }
545
546 }
547
548
549 /**
550 * Check if the conditions specified in the optional If headers are
551 * satisfied.
552 *
553 * @param request The servlet request we are processing
554 * @param response The servlet response we are creating
555 * @param resourceAttributes The resource information
556 * @return boolean true if the resource meets all the specified conditions,
557 * and false if any of the conditions is not satisfied, in which case
558 * request processing is stopped
559 */
560 protected boolean checkIfHeaders(HttpServletRequest request,
561 HttpServletResponse response,
562 ResourceAttributes resourceAttributes)
563 throws IOException {
564
565 return checkIfMatch(request, response, resourceAttributes)
566 && checkIfModifiedSince(request, response, resourceAttributes)
567 && checkIfNoneMatch(request, response, resourceAttributes)
568 && checkIfUnmodifiedSince(request, response, resourceAttributes);
569
570 }
571
572
573 /**
574 * Get the ETag associated with a file.
575 *
576 * @param resourceAttributes The resource information
577 */
578 protected String getETag(ResourceAttributes resourceAttributes) {
579 String result = null;
580 if ((result = resourceAttributes.getETag(true)) != null) {
581 return result;
582 } else if ((result = resourceAttributes.getETag()) != null) {
583 return result;
584 } else {
585 return "W/\"" + resourceAttributes.getContentLength() + "-"
586 + resourceAttributes.getLastModified() + "\"";
587 }
588 }
589
590
591 /**
592 * URL rewriter.
593 *
594 * @param path Path which has to be rewiten
595 */
596 protected String rewriteUrl(String path) {
597 return urlEncoder.encode( path );
598 }
599
600
601 /**
602 * Display the size of a file.
603 */
604 protected void displaySize(StringBuffer buf, int filesize) {
605
606 int leftside = filesize / 1024;
607 int rightside = (filesize % 1024) / 103; // makes 1 digit
608 // To avoid 0.0 for non-zero file, we bump to 0.1
609 if (leftside == 0 && rightside == 0 && filesize != 0)
610 rightside = 1;
611 buf.append(leftside).append(".").append(rightside);
612 buf.append(" KB");
613
614 }
615
616
617 /**
618 * Serve the specified resource, optionally including the data content.
619 *
620 * @param request The servlet request we are processing
621 * @param response The servlet response we are creating
622 * @param content Should the content be included?
623 *
624 * @exception IOException if an input/output error occurs
625 * @exception ServletException if a servlet-specified error occurs
626 */
627 protected void serveResource(HttpServletRequest request,
628 HttpServletResponse response,
629 boolean content)
630 throws IOException, ServletException {
631
632 // Identify the requested resource path
633 String path = getRelativePath(request);
634 if (debug > 0) {
635 if (content)
636 log("DefaultServlet.serveResource: Serving resource '" +
637 path + "' headers and data");
638 else
639 log("DefaultServlet.serveResource: Serving resource '" +
640 path + "' headers only");
641 }
642
643 CacheEntry cacheEntry = resources.lookupCache(path);
644
645 if (!cacheEntry.exists) {
646 // Check if we're included so we can return the appropriate
647 // missing resource name in the error
648 String requestUri = (String) request.getAttribute(
649 Globals.INCLUDE_REQUEST_URI_ATTR);
650 if (requestUri == null) {
651 requestUri = request.getRequestURI();
652 } else {
653 // We're included, and the response.sendError() below is going
654 // to be ignored by the resource that is including us.
655 // Therefore, the only way we can let the including resource
656 // know is by including warning message in response
657 response.getWriter().write(
658 sm.getString("defaultServlet.missingResource",
659 requestUri));
660 }
661
662 response.sendError(HttpServletResponse.SC_NOT_FOUND,
663 requestUri);
664 return;
665 }
666
667 // If the resource is not a collection, and the resource path
668 // ends with "/" or "\", return NOT FOUND
669 if (cacheEntry.context == null) {
670 if (path.endsWith("/") || (path.endsWith("\\"))) {
671 // Check if we're included so we can return the appropriate
672 // missing resource name in the error
673 String requestUri = (String) request.getAttribute(
674 Globals.INCLUDE_REQUEST_URI_ATTR);
675 if (requestUri == null) {
676 requestUri = request.getRequestURI();
677 }
678 response.sendError(HttpServletResponse.SC_NOT_FOUND,
679 requestUri);
680 return;
681 }
682 }
683
684 // Check if the conditions specified in the optional If headers are
685 // satisfied.
686 if (cacheEntry.context == null) {
687
688 // Checking If headers
689 boolean included =
690 (request.getAttribute(Globals.INCLUDE_CONTEXT_PATH_ATTR) != null);
691 if (!included
692 && !checkIfHeaders(request, response, cacheEntry.attributes)) {
693 return;
694 }
695
696 }
697
698 // Find content type.
699 String contentType = cacheEntry.attributes.getMimeType();
700 if (contentType == null) {
701 contentType = getServletContext().getMimeType(cacheEntry.name);
702 cacheEntry.attributes.setMimeType(contentType);
703 }
704
705 ArrayList ranges = null;
706 long contentLength = -1L;
707
708 if (cacheEntry.context != null) {
709
710 // Skip directory listings if we have been configured to
711 // suppress them
712 if (!listings) {
713 response.sendError(HttpServletResponse.SC_NOT_FOUND,
714 request.getRequestURI());
715 return;
716 }
717 contentType = "text/html;charset=UTF-8";
718
719 } else {
720
721 // Parse range specifier
722
723 ranges = parseRange(request, response, cacheEntry.attributes);
724
725 // ETag header
726 response.setHeader("ETag", getETag(cacheEntry.attributes));
727
728 // Last-Modified header
729 response.setHeader("Last-Modified",
730 cacheEntry.attributes.getLastModifiedHttp());
731
732 // Get content length
733 contentLength = cacheEntry.attributes.getContentLength();
734 // Special case for zero length files, which would cause a
735 // (silent) ISE when setting the output buffer size
736 if (contentLength == 0L) {
737 content = false;
738 }
739
740 }
741
742 ServletOutputStream ostream = null;
743 PrintWriter writer = null;
744
745 if (content) {
746
747 // Trying to retrieve the servlet output stream
748
749 try {
750 ostream = response.getOutputStream();
751 } catch (IllegalStateException e) {
752 // If it fails, we try to get a Writer instead if we're
753 // trying to serve a text file
754 if ( (contentType == null)
755 || (contentType.startsWith("text"))
756 || (contentType.endsWith("xml")) ) {
757 writer = response.getWriter();
758 } else {
759 throw e;
760 }
761 }
762
763 }
764
765 if ( (cacheEntry.context != null)
766 || ( ((ranges == null) || (ranges.isEmpty()))
767 && (request.getHeader("Range") == null) )
768 || (ranges == FULL) ) {
769
770 // Set the appropriate output headers
771 if (contentType != null) {
772 if (debug > 0)
773 log("DefaultServlet.serveFile: contentType='" +
774 contentType + "'");
775 response.setContentType(contentType);
776 }
777 if ((cacheEntry.resource != null) && (contentLength >= 0)) {
778 if (debug > 0)
779 log("DefaultServlet.serveFile: contentLength=" +
780 contentLength);
781 if (contentLength < Integer.MAX_VALUE) {
782 response.setContentLength((int) contentLength);
783 } else {
784 // Set the content-length as String to be able to use a long
785 response.setHeader("content-length", "" + contentLength);
786 }
787 }
788
789 InputStream renderResult = null;
790 if (cacheEntry.context != null) {
791
792 if (content) {
793 // Serve the directory browser
794 renderResult =
795 render(request.getContextPath(), cacheEntry);
796 }
797
798 }
799
800 // Copy the input stream to our output stream (if requested)
801 if (content) {
802 try {
803 response.setBufferSize(output);
804 } catch (IllegalStateException e) {
805 // Silent catch
806 }
807 if (ostream != null) {
808 if (!checkSendfile(request, response, cacheEntry, contentLength, null))
809 copy(cacheEntry, renderResult, ostream);
810 } else {
811 copy(cacheEntry, renderResult, writer);
812 }
813 }
814
815 } else {
816
817 if ((ranges == null) || (ranges.isEmpty()))
818 return;
819
820 // Partial content response.
821
822 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
823
824 if (ranges.size() == 1) {
825
826 Range range = (Range) ranges.get(0);
827 response.addHeader("Content-Range", "bytes "
828 + range.start
829 + "-" + range.end + "/"
830 + range.length);
831 long length = range.end - range.start + 1;
832 if (length < Integer.MAX_VALUE) {
833 response.setContentLength((int) length);
834 } else {
835 // Set the content-length as String to be able to use a long
836 response.setHeader("content-length", "" + length);
837 }
838
839 if (contentType != null) {
840 if (debug > 0)
841 log("DefaultServlet.serveFile: contentType='" +
842 contentType + "'");
843 response.setContentType(contentType);
844 }
845
846 if (content) {
847 try {
848 response.setBufferSize(output);
849 } catch (IllegalStateException e) {
850 // Silent catch
851 }
852 if (ostream != null) {
853 if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
854 copy(cacheEntry, ostream, range);
855 } else {
856 copy(cacheEntry, writer, range);
857 }
858 }
859
860 } else {
861
862 response.setContentType("multipart/byteranges; boundary="
863 + mimeSeparation);
864
865 if (content) {
866 try {
867 response.setBufferSize(output);
868 } catch (IllegalStateException e) {
869 // Silent catch
870 }
871 if (ostream != null) {
872 copy(cacheEntry, ostream, ranges.iterator(),
873 contentType);
874 } else {
875 copy(cacheEntry, writer, ranges.iterator(),
876 contentType);
877 }
878 }
879
880 }
881
882 }
883
884 }
885
886
887 /**
888 * Parse the content-range header.
889 *
890 * @param request The servlet request we are processing
891 * @param response The servlet response we are creating
892 * @return Range
893 */
894 protected Range parseContentRange(HttpServletRequest request,
895 HttpServletResponse response)
896 throws IOException {
897
898 // Retrieving the content-range header (if any is specified
899 String rangeHeader = request.getHeader("Content-Range");
900
901 if (rangeHeader == null)
902 return null;
903
904 // bytes is the only range unit supported
905 if (!rangeHeader.startsWith("bytes")) {
906 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
907 return null;
908 }
909
910 rangeHeader = rangeHeader.substring(6).trim();
911
912 int dashPos = rangeHeader.indexOf('-');
913 int slashPos = rangeHeader.indexOf('/');
914
915 if (dashPos == -1) {
916 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
917 return null;
918 }
919
920 if (slashPos == -1) {
921 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
922 return null;
923 }
924
925 Range range = new Range();
926
927 try {
928 range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
929 range.end =
930 Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
931 range.length = Long.parseLong
932 (rangeHeader.substring(slashPos + 1, rangeHeader.length()));
933 } catch (NumberFormatException e) {
934 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
935 return null;
936 }
937
938 if (!range.validate()) {
939 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
940 return null;
941 }
942
943 return range;
944
945 }
946
947
948 /**
949 * Parse the range header.
950 *
951 * @param request The servlet request we are processing
952 * @param response The servlet response we are creating
953 * @return Vector of ranges
954 */
955 protected ArrayList parseRange(HttpServletRequest request,
956 HttpServletResponse response,
957 ResourceAttributes resourceAttributes)
958 throws IOException {
959
960 // Checking If-Range
961 String headerValue = request.getHeader("If-Range");
962
963 if (headerValue != null) {
964
965 long headerValueTime = (-1L);
966 try {
967 headerValueTime = request.getDateHeader("If-Range");
968 } catch (IllegalArgumentException e) {
969 ;
970 }
971
972 String eTag = getETag(resourceAttributes);
973 long lastModified = resourceAttributes.getLastModified();
974
975 if (headerValueTime == (-1L)) {
976
977 // If the ETag the client gave does not match the entity
978 // etag, then the entire entity is returned.
979 if (!eTag.equals(headerValue.trim()))
980 return FULL;
981
982 } else {
983
984 // If the timestamp of the entity the client got is older than
985 // the last modification date of the entity, the entire entity
986 // is returned.
987 if (lastModified > (headerValueTime + 1000))
988 return FULL;
989
990 }
991
992 }
993
994 long fileLength = resourceAttributes.getContentLength();
995
996 if (fileLength == 0)
997 return null;
998
999 // Retrieving the range header (if any is specified
1000 String rangeHeader = request.getHeader("Range");
1001
1002 if (rangeHeader == null)
1003 return null;
1004 // bytes is the only range unit supported (and I don't see the point
1005 // of adding new ones).
1006 if (!rangeHeader.startsWith("bytes")) {
1007 response.addHeader("Content-Range", "bytes */" + fileLength);
1008 response.sendError
1009 (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1010 return null;
1011 }
1012
1013 rangeHeader = rangeHeader.substring(6);
1014
1015 // Vector which will contain all the ranges which are successfully
1016 // parsed.
1017 ArrayList<Range> result = new ArrayList<Range>();
1018 StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
1019
1020 // Parsing the range list
1021 while (commaTokenizer.hasMoreTokens()) {
1022 String rangeDefinition = commaTokenizer.nextToken().trim();
1023
1024 Range currentRange = new Range();
1025 currentRange.length = fileLength;
1026
1027 int dashPos = rangeDefinition.indexOf('-');
1028
1029 if (dashPos == -1) {
1030 response.addHeader("Content-Range", "bytes */" + fileLength);
1031 response.sendError
1032 (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1033 return null;
1034 }
1035
1036 if (dashPos == 0) {
1037
1038 try {
1039 long offset = Long.parseLong(rangeDefinition);
1040 currentRange.start = fileLength + offset;
1041 currentRange.end = fileLength - 1;
1042 } catch (NumberFormatException e) {
1043 response.addHeader("Content-Range",
1044 "bytes */" + fileLength);
1045 response.sendError
1046 (HttpServletResponse
1047 .SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1048 return null;
1049 }
1050
1051 } else {
1052
1053 try {
1054 currentRange.start = Long.parseLong
1055 (rangeDefinition.substring(0, dashPos));
1056 if (dashPos < rangeDefinition.length() - 1)
1057 currentRange.end = Long.parseLong
1058 (rangeDefinition.substring
1059 (dashPos + 1, rangeDefinition.length()));
1060 else
1061 currentRange.end = fileLength - 1;
1062 } catch (NumberFormatException e) {
1063 response.addHeader("Content-Range",
1064 "bytes */" + fileLength);
1065 response.sendError
1066 (HttpServletResponse
1067 .SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1068 return null;
1069 }
1070
1071 }
1072
1073 if (!currentRange.validate()) {
1074 response.addHeader("Content-Range", "bytes */" + fileLength);
1075 response.sendError
1076 (HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1077 return null;
1078 }
1079
1080 result.add(currentRange);
1081 }
1082
1083 return result;
1084 }
1085
1086
1087
1088 /**
1089 * Decide which way to render. HTML or XML.
1090 */
1091 protected InputStream render(String contextPath, CacheEntry cacheEntry)
1092 throws IOException, ServletException {
1093
1094 InputStream xsltInputStream =
1095 findXsltInputStream(cacheEntry.context);
1096
1097 if (xsltInputStream==null) {
1098 return renderHtml(contextPath, cacheEntry);
1099 } else {
1100 return renderXml(contextPath, cacheEntry, xsltInputStream);
1101 }
1102
1103 }
1104
1105 /**
1106 * Return an InputStream to an HTML representation of the contents
1107 * of this directory.
1108 *
1109 * @param contextPath Context path to which our internal paths are
1110 * relative
1111 */
1112 protected InputStream renderXml(String contextPath,
1113 CacheEntry cacheEntry,
1114 InputStream xsltInputStream)
1115 throws IOException, ServletException {
1116
1117 StringBuffer sb = new StringBuffer();
1118
1119 sb.append("<?xml version=\"1.0\"?>");
1120 sb.append("<listing ");
1121 sb.append(" contextPath='");
1122 sb.append(contextPath);
1123 sb.append("'");
1124 sb.append(" directory='");
1125 sb.append(cacheEntry.name);
1126 sb.append("' ");
1127 sb.append(" hasParent='").append(!cacheEntry.name.equals("/"));
1128 sb.append("'>");
1129
1130 sb.append("<entries>");
1131
1132 try {
1133
1134 // Render the directory entries within this directory
1135 NamingEnumeration enumeration = resources.list(cacheEntry.name);
1136
1137 // rewriteUrl(contextPath) is expensive. cache result for later reuse
1138 String rewrittenContextPath = rewriteUrl(contextPath);
1139
1140 while (enumeration.hasMoreElements()) {
1141
1142 NameClassPair ncPair = (NameClassPair) enumeration.nextElement();
1143 String resourceName = ncPair.getName();
1144 String trimmed = resourceName/*.substring(trim)*/;
1145 if (trimmed.equalsIgnoreCase("WEB-INF") ||
1146 trimmed.equalsIgnoreCase("META-INF") ||
1147 trimmed.equalsIgnoreCase(localXsltFile))
1148 continue;
1149
1150 CacheEntry childCacheEntry =
1151 resources.lookupCache(cacheEntry.name + resourceName);
1152 if (!childCacheEntry.exists) {
1153 continue;
1154 }
1155
1156 sb.append("<entry");
1157 sb.append(" type='")
1158 .append((childCacheEntry.context != null)?"dir":"file")
1159 .append("'");
1160 sb.append(" urlPath='")
1161 .append(rewrittenContextPath)
1162 .append(rewriteUrl(cacheEntry.name + resourceName))
1163 .append((childCacheEntry.context != null)?"/":"")
1164 .append("'");
1165 if (childCacheEntry.resource != null) {
1166 sb.append(" size='")
1167 .append(renderSize(childCacheEntry.attributes.getContentLength()))
1168 .append("'");
1169 }
1170 sb.append(" date='")
1171 .append(childCacheEntry.attributes.getLastModifiedHttp())
1172 .append("'");
1173
1174 sb.append(">");
1175 sb.append(RequestUtil.filter(trimmed));
1176 if (childCacheEntry.context != null)
1177 sb.append("/");
1178 sb.append("</entry>");
1179
1180 }
1181
1182 } catch (NamingException e) {
1183 // Something went wrong
1184 throw new ServletException("Error accessing resource", e);
1185 }
1186
1187 sb.append("</entries>");
1188
1189 String readme = getReadme(cacheEntry.context);
1190
1191 if (readme!=null) {
1192 sb.append("<readme><![CDATA[");
1193 sb.append(readme);
1194 sb.append("]]></readme>");
1195 }
1196
1197
1198 sb.append("</listing>");
1199
1200
1201 try {
1202 TransformerFactory tFactory = TransformerFactory.newInstance();
1203 Source xmlSource = new StreamSource(new StringReader(sb.toString()));
1204 Source xslSource = new StreamSource(xsltInputStream);
1205 Transformer transformer = tFactory.newTransformer(xslSource);
1206
1207 ByteArrayOutputStream stream = new ByteArrayOutputStream();
1208 OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
1209 StreamResult out = new StreamResult(osWriter);
1210 transformer.transform(xmlSource, out);
1211 osWriter.flush();
1212 return (new ByteArrayInputStream(stream.toByteArray()));
1213 } catch (TransformerException e) {
1214 throw new ServletException("XSL transformer error", e);
1215 }
1216 }
1217
1218 /**
1219 * Return an InputStream to an HTML representation of the contents
1220 * of this directory.
1221 *
1222 * @param contextPath Context path to which our internal paths are
1223 * relative
1224 */
1225 protected InputStream renderHtml(String contextPath, CacheEntry cacheEntry)
1226 throws IOException, ServletException {
1227
1228 String name = cacheEntry.name;
1229
1230 // Number of characters to trim from the beginnings of filenames
1231 int trim = name.length();
1232 if (!name.endsWith("/"))
1233 trim += 1;
1234 if (name.equals("/"))
1235 trim = 1;
1236
1237 // Prepare a writer to a buffered area
1238 ByteArrayOutputStream stream = new ByteArrayOutputStream();
1239 OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
1240 PrintWriter writer = new PrintWriter(osWriter);
1241
1242 StringBuffer sb = new StringBuffer();
1243
1244 // rewriteUrl(contextPath) is expensive. cache result for later reuse
1245 String rewrittenContextPath = rewriteUrl(contextPath);
1246
1247 // Render the page header
1248 sb.append("<html>\r\n");
1249 sb.append("<head>\r\n");
1250 sb.append("<title>");
1251 sb.append(sm.getString("directory.title", name));
1252 sb.append("</title>\r\n");
1253 sb.append("<STYLE><!--");
1254 sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
1255 sb.append("--></STYLE> ");
1256 sb.append("</head>\r\n");
1257 sb.append("<body>");
1258 sb.append("<h1>");
1259 sb.append(sm.getString("directory.title", name));
1260
1261 // Render the link to our parent (if required)
1262 String parentDirectory = name;
1263 if (parentDirectory.endsWith("/")) {
1264 parentDirectory =
1265 parentDirectory.substring(0, parentDirectory.length() - 1);
1266 }
1267 int slash = parentDirectory.lastIndexOf('/');
1268 if (slash >= 0) {
1269 String parent = name.substring(0, slash);
1270 sb.append(" - <a href=\"");
1271 sb.append(rewrittenContextPath);
1272 if (parent.equals(""))
1273 parent = "/";
1274 sb.append(rewriteUrl(parent));
1275 if (!parent.endsWith("/"))
1276 sb.append("/");
1277 sb.append("\">");
1278 sb.append("<b>");
1279 sb.append(sm.getString("directory.parent", parent));
1280 sb.append("</b>");
1281 sb.append("</a>");
1282 }
1283
1284 sb.append("</h1>");
1285 sb.append("<HR size=\"1\" noshade=\"noshade\">");
1286
1287 sb.append("<table width=\"100%\" cellspacing=\"0\"" +
1288 " cellpadding=\"5\" align=\"center\">\r\n");
1289
1290 // Render the column headings
1291 sb.append("<tr>\r\n");
1292 sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
1293 sb.append(sm.getString("directory.filename"));
1294 sb.append("</strong></font></td>\r\n");
1295 sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
1296 sb.append(sm.getString("directory.size"));
1297 sb.append("</strong></font></td>\r\n");
1298 sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
1299 sb.append(sm.getString("directory.lastModified"));
1300 sb.append("</strong></font></td>\r\n");
1301 sb.append("</tr>");
1302
1303 try {
1304
1305 // Render the directory entries within this directory
1306 NamingEnumeration enumeration = resources.list(cacheEntry.name);
1307 boolean shade = false;
1308 while (enumeration.hasMoreElements()) {
1309
1310 NameClassPair ncPair = (NameClassPair) enumeration.nextElement();
1311 String resourceName = ncPair.getName();
1312 String trimmed = resourceName/*.substring(trim)*/;
1313 if (trimmed.equalsIgnoreCase("WEB-INF") ||
1314 trimmed.equalsIgnoreCase("META-INF"))
1315 continue;
1316
1317 CacheEntry childCacheEntry =
1318 resources.lookupCache(cacheEntry.name + resourceName);
1319 if (!childCacheEntry.exists) {
1320 continue;
1321 }
1322
1323 sb.append("<tr");
1324 if (shade)
1325 sb.append(" bgcolor=\"#eeeeee\"");
1326 sb.append(">\r\n");
1327 shade = !shade;
1328
1329 sb.append("<td align=\"left\"> \r\n");
1330 sb.append("<a href=\"");
1331 sb.append(rewrittenContextPath);
1332 resourceName = rewriteUrl(name + resourceName);
1333 sb.append(resourceName);
1334 if (childCacheEntry.context != null)
1335 sb.append("/");
1336 sb.append("\"><tt>");
1337 sb.append(RequestUtil.filter(trimmed));
1338 if (childCacheEntry.context != null)
1339 sb.append("/");
1340 sb.append("</tt></a></td>\r\n");
1341
1342 sb.append("<td align=\"right\"><tt>");
1343 if (childCacheEntry.context != null)
1344 sb.append(" ");
1345 else
1346 sb.append(renderSize(childCacheEntry.attributes.getContentLength()));
1347 sb.append("</tt></td>\r\n");
1348
1349 sb.append("<td align=\"right\"><tt>");
1350 sb.append(childCacheEntry.attributes.getLastModifiedHttp());
1351 sb.append("</tt></td>\r\n");
1352
1353 sb.append("</tr>\r\n");
1354 }
1355
1356 } catch (NamingException e) {
1357 // Something went wrong
1358 throw new ServletException("Error accessing resource", e);
1359 }
1360
1361 // Render the page footer
1362 sb.append("</table>\r\n");
1363
1364 sb.append("<HR size=\"1\" noshade=\"noshade\">");
1365
1366 String readme = getReadme(cacheEntry.context);
1367 if (readme!=null) {
1368 sb.append(readme);
1369 sb.append("<HR size=\"1\" noshade=\"noshade\">");
1370 }
1371
1372 sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
1373 sb.append("</body>\r\n");
1374 sb.append("</html>\r\n");
1375
1376 // Return an input stream to the underlying bytes
1377 writer.write(sb.toString());
1378 writer.flush();
1379 return (new ByteArrayInputStream(stream.toByteArray()));
1380
1381 }
1382
1383
1384 /**
1385 * Render the specified file size (in bytes).
1386 *
1387 * @param size File size (in bytes)
1388 */
1389 protected String renderSize(long size) {
1390
1391 long leftSide = size / 1024;
1392 long rightSide = (size % 1024) / 103; // Makes 1 digit
1393 if ((leftSide == 0) && (rightSide == 0) && (size > 0))
1394 rightSide = 1;
1395
1396 return ("" + leftSide + "." + rightSide + " kb");
1397
1398 }
1399
1400
1401 /**
1402 * Get the readme file as a string.
1403 */
1404 protected String getReadme(DirContext directory)
1405 throws IOException, ServletException {
1406
1407 if (readmeFile != null) {
1408 try {
1409 Object obj = directory.lookup(readmeFile);
1410 if ((obj != null) && (obj instanceof Resource)) {
1411 StringWriter buffer = new StringWriter();
1412 InputStream is = ((Resource) obj).streamContent();
1413 copyRange(new InputStreamReader(is),
1414 new PrintWriter(buffer));
1415 return buffer.toString();
1416 }
1417 } catch (NamingException e) {
1418 throw new ServletException("Error opening readme resource", e);
1419 }
1420 }
1421
1422 return null;
1423 }
1424
1425
1426 /**
1427 * Return the xsl template inputstream (if possible)
1428 */
1429 protected InputStream findXsltInputStream(DirContext directory)
1430 throws IOException, ServletException {
1431
1432 if (localXsltFile != null) {
1433 try {
1434 Object obj = directory.lookup(localXsltFile);
1435 if ((obj != null) && (obj instanceof Resource)) {
1436 InputStream is = ((Resource) obj).streamContent();
1437 if (is != null)
1438 return is;
1439 }
1440 } catch (NamingException e) {
1441 throw new ServletException("Error opening XSLT resource", e);
1442 }
1443 }
1444
1445 /* Open and read in file in one fell swoop to reduce chance
1446 * chance of leaving handle open.
1447 */
1448 if (globalXsltFile!=null) {
1449 FileInputStream fis = null;
1450
1451 try {
1452 File f = new File(globalXsltFile);
1453 if (f.exists()){
1454 fis =new FileInputStream(f);
1455 byte b[] = new byte[(int)f.length()]; /* danger! */
1456 fis.read(b);
1457 return new ByteArrayInputStream(b);
1458 }
1459 } finally {
1460 if (fis!=null)
1461 fis.close();
1462 }
1463 }
1464
1465 return null;
1466
1467 }
1468
1469
1470 // -------------------------------------------------------- protected Methods
1471
1472
1473 /**
1474 * Check if sendfile can be used.
1475 */
1476 protected boolean checkSendfile(HttpServletRequest request,
1477 HttpServletResponse response,
1478 CacheEntry entry,
1479 long length, Range range) {
1480 if ((sendfileSize > 0)
1481 && (entry.resource != null)
1482 && ((length > sendfileSize) || (entry.resource.getContent() == null))
1483 && (entry.attributes.getCanonicalPath() != null)
1484 && (Boolean.TRUE == request.getAttribute("org.apache.tomcat.sendfile.support"))
1485 && (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade"))
1486 && (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade"))) {
1487 request.setAttribute("org.apache.tomcat.sendfile.filename", entry.attributes.getCanonicalPath());
1488 if (range == null) {
1489 request.setAttribute("org.apache.tomcat.sendfile.start", new Long(0L));
1490 request.setAttribute("org.apache.tomcat.sendfile.end", new Long(length));
1491 } else {
1492 request.setAttribute("org.apache.tomcat.sendfile.start", new Long(range.start));
1493 request.setAttribute("org.apache.tomcat.sendfile.end", new Long(range.end + 1));
1494 }
1495 request.setAttribute("org.apache.tomcat.sendfile.token", this);
1496 return true;
1497 } else {
1498 return false;
1499 }
1500 }
1501
1502
1503 /**
1504 * Check if the if-match condition is satisfied.
1505 *
1506 * @param request The servlet request we are processing
1507 * @param response The servlet response we are creating
1508 * @param resourceInfo File object
1509 * @return boolean true if the resource meets the specified condition,
1510 * and false if the condition is not satisfied, in which case request
1511 * processing is stopped
1512 */
1513 protected boolean checkIfMatch(HttpServletRequest request,
1514 HttpServletResponse response,
1515 ResourceAttributes resourceAttributes)
1516 throws IOException {
1517
1518 String eTag = getETag(resourceAttributes);
1519 String headerValue = request.getHeader("If-Match");
1520 if (headerValue != null) {
1521 if (headerValue.indexOf('*') == -1) {
1522
1523 StringTokenizer commaTokenizer = new StringTokenizer
1524 (headerValue, ",");
1525 boolean conditionSatisfied = false;
1526
1527 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
1528 String currentToken = commaTokenizer.nextToken();
1529 if (currentToken.trim().equals(eTag))
1530 conditionSatisfied = true;
1531 }
1532
1533 // If none of the given ETags match, 412 Precodition failed is
1534 // sent back
1535 if (!conditionSatisfied) {
1536 response.sendError
1537 (HttpServletResponse.SC_PRECONDITION_FAILED);
1538 return false;
1539 }
1540
1541 }
1542 }
1543 return true;
1544
1545 }
1546
1547
1548 /**
1549 * Check if the if-modified-since condition is satisfied.
1550 *
1551 * @param request The servlet request we are processing
1552 * @param response The servlet response we are creating
1553 * @param resourceInfo File object
1554 * @return boolean true if the resource meets the specified condition,
1555 * and false if the condition is not satisfied, in which case request
1556 * processing is stopped
1557 */
1558 protected boolean checkIfModifiedSince(HttpServletRequest request,
1559 HttpServletResponse response,
1560 ResourceAttributes resourceAttributes)
1561 throws IOException {
1562 try {
1563 long headerValue = request.getDateHeader("If-Modified-Since");
1564 long lastModified = resourceAttributes.getLastModified();
1565 if (headerValue != -1) {
1566
1567 // If an If-None-Match header has been specified, if modified since
1568 // is ignored.
1569 if ((request.getHeader("If-None-Match") == null)
1570 && (lastModified < headerValue + 1000)) {
1571 // The entity has not been modified since the date
1572 // specified by the client. This is not an error case.
1573 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
1574 response.setHeader("ETag", getETag(resourceAttributes));
1575
1576 return false;
1577 }
1578 }
1579 } catch (IllegalArgumentException illegalArgument) {
1580 return true;
1581 }
1582 return true;
1583
1584 }
1585
1586
1587 /**
1588 * Check if the if-none-match condition is satisfied.
1589 *
1590 * @param request The servlet request we are processing
1591 * @param response The servlet response we are creating
1592 * @param resourceInfo File object
1593 * @return boolean true if the resource meets the specified condition,
1594 * and false if the condition is not satisfied, in which case request
1595 * processing is stopped
1596 */
1597 protected boolean checkIfNoneMatch(HttpServletRequest request,
1598 HttpServletResponse response,
1599 ResourceAttributes resourceAttributes)
1600 throws IOException {
1601
1602 String eTag = getETag(resourceAttributes);
1603 String headerValue = request.getHeader("If-None-Match");
1604 if (headerValue != null) {
1605
1606 boolean conditionSatisfied = false;
1607
1608 if (!headerValue.equals("*")) {
1609
1610 StringTokenizer commaTokenizer =
1611 new StringTokenizer(headerValue, ",");
1612
1613 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
1614 String currentToken = commaTokenizer.nextToken();
1615 if (currentToken.trim().equals(eTag))
1616 conditionSatisfied = true;
1617 }
1618
1619 } else {
1620 conditionSatisfied = true;
1621 }
1622
1623 if (conditionSatisfied) {
1624
1625 // For GET and HEAD, we should respond with
1626 // 304 Not Modified.
1627 // For every other method, 412 Precondition Failed is sent
1628 // back.
1629 if ( ("GET".equals(request.getMethod()))
1630 || ("HEAD".equals(request.getMethod())) ) {
1631 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
1632 response.setHeader("ETag", getETag(resourceAttributes));
1633
1634 return false;
1635 } else {
1636 response.sendError
1637 (HttpServletResponse.SC_PRECONDITION_FAILED);
1638 return false;
1639 }
1640 }
1641 }
1642 return true;
1643
1644 }
1645
1646
1647 /**
1648 * Check if the if-unmodified-since condition is satisfied.
1649 *
1650 * @param request The servlet request we are processing
1651 * @param response The servlet response we are creating
1652 * @param resourceInfo File object
1653 * @return boolean true if the resource meets the specified condition,
1654 * and false if the condition is not satisfied, in which case request
1655 * processing is stopped
1656 */
1657 protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
1658 HttpServletResponse response,
1659 ResourceAttributes resourceAttributes)
1660 throws IOException {
1661 try {
1662 long lastModified = resourceAttributes.getLastModified();
1663 long headerValue = request.getDateHeader("If-Unmodified-Since");
1664 if (headerValue != -1) {
1665 if ( lastModified >= (headerValue + 1000)) {
1666 // The entity has not been modified since the date
1667 // specified by the client. This is not an error case.
1668 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
1669 return false;
1670 }
1671 }
1672 } catch(IllegalArgumentException illegalArgument) {
1673 return true;
1674 }
1675 return true;
1676
1677 }
1678
1679
1680 /**
1681 * Copy the contents of the specified input stream to the specified
1682 * output stream, and ensure that both streams are closed before returning
1683 * (even in the face of an exception).
1684 *
1685 * @param resourceInfo The resource information
1686 * @param ostream The output stream to write to
1687 *
1688 * @exception IOException if an input/output error occurs
1689 */
1690 protected void copy(CacheEntry cacheEntry, InputStream is,
1691 ServletOutputStream ostream)
1692 throws IOException {
1693
1694 IOException exception = null;
1695 InputStream resourceInputStream = null;
1696
1697 // Optimization: If the binary content has already been loaded, send
1698 // it directly
1699 if (cacheEntry.resource != null) {
1700 byte buffer[] = cacheEntry.resource.getContent();
1701 if (buffer != null) {
1702 ostream.write(buffer, 0, buffer.length);
1703 return;
1704 }
1705 resourceInputStream = cacheEntry.resource.streamContent();
1706 } else {
1707 resourceInputStream = is;
1708 }
1709
1710 InputStream istream = new BufferedInputStream
1711 (resourceInputStream, input);
1712
1713 // Copy the input stream to the output stream
1714 exception = copyRange(istream, ostream);
1715
1716 // Clean up the input stream
1717 istream.close();
1718
1719 // Rethrow any exception that has occurred
1720 if (exception != null)
1721 throw exception;
1722
1723 }
1724
1725
1726 /**
1727 * Copy the contents of the specified input stream to the specified
1728 * output stream, and ensure that both streams are closed before returning
1729 * (even in the face of an exception).
1730 *
1731 * @param resourceInfo The resource info
1732 * @param writer The writer to write to
1733 *
1734 * @exception IOException if an input/output error occurs
1735 */
1736 protected void copy(CacheEntry cacheEntry, InputStream is, PrintWriter writer)
1737 throws IOException {
1738
1739 IOException exception = null;
1740
1741 InputStream resourceInputStream = null;
1742 if (cacheEntry.resource != null) {
1743 resourceInputStream = cacheEntry.resource.streamContent();
1744 } else {
1745 resourceInputStream = is;
1746 }
1747
1748 Reader reader;
1749 if (fileEncoding == null) {
1750 reader = new InputStreamReader(resourceInputStream);
1751 } else {
1752 reader = new InputStreamReader(resourceInputStream,
1753 fileEncoding);
1754 }
1755
1756 // Copy the input stream to the output stream
1757 exception = copyRange(reader, writer);
1758
1759 // Clean up the reader
1760 reader.close();
1761
1762 // Rethrow any exception that has occurred
1763 if (exception != null)
1764 throw exception;
1765
1766 }
1767
1768
1769 /**
1770 * Copy the contents of the specified input stream to the specified
1771 * output stream, and ensure that both streams are closed before returning
1772 * (even in the face of an exception).
1773 *
1774 * @param resourceInfo The ResourceInfo object
1775 * @param ostream The output stream to write to
1776 * @param range Range the client wanted to retrieve
1777 * @exception IOException if an input/output error occurs
1778 */
1779 protected void copy(CacheEntry cacheEntry, ServletOutputStream ostream,
1780 Range range)
1781 throws IOException {
1782
1783 IOException exception = null;
1784
1785 InputStream resourceInputStream = cacheEntry.resource.streamContent();
1786 InputStream istream =
1787 new BufferedInputStream(resourceInputStream, input);
1788 exception = copyRange(istream, ostream, range.start, range.end);
1789
1790 // Clean up the input stream
1791 istream.close();
1792
1793 // Rethrow any exception that has occurred
1794 if (exception != null)
1795 throw exception;
1796
1797 }
1798
1799
1800 /**
1801 * Copy the contents of the specified input stream to the specified
1802 * output stream, and ensure that both streams are closed before returning
1803 * (even in the face of an exception).
1804 *
1805 * @param resourceInfo The ResourceInfo object
1806 * @param writer The writer to write to
1807 * @param range Range the client wanted to retrieve
1808 * @exception IOException if an input/output error occurs
1809 */
1810 protected void copy(CacheEntry cacheEntry, PrintWriter writer,
1811 Range range)
1812 throws IOException {
1813
1814 IOException exception = null;
1815
1816 InputStream resourceInputStream = cacheEntry.resource.streamContent();
1817
1818 Reader reader;
1819 if (fileEncoding == null) {
1820 reader = new InputStreamReader(resourceInputStream);
1821 } else {
1822 reader = new InputStreamReader(resourceInputStream,
1823 fileEncoding);
1824 }
1825
1826 exception = copyRange(reader, writer, range.start, range.end);
1827
1828 // Clean up the input stream
1829 reader.close();
1830
1831 // Rethrow any exception that has occurred
1832 if (exception != null)
1833 throw exception;
1834
1835 }
1836
1837
1838 /**
1839 * Copy the contents of the specified input stream to the specified
1840 * output stream, and ensure that both streams are closed before returning
1841 * (even in the face of an exception).
1842 *
1843 * @param resourceInfo The ResourceInfo object
1844 * @param ostream The output stream to write to
1845 * @param ranges Enumeration of the ranges the client wanted to retrieve
1846 * @param contentType Content type of the resource
1847 * @exception IOException if an input/output error occurs
1848 */
1849 protected void copy(CacheEntry cacheEntry, ServletOutputStream ostream,
1850 Iterator ranges, String contentType)
1851 throws IOException {
1852
1853 IOException exception = null;
1854
1855 while ( (exception == null) && (ranges.hasNext()) ) {
1856
1857 InputStream resourceInputStream = cacheEntry.resource.streamContent();
1858 InputStream istream =
1859 new BufferedInputStream(resourceInputStream, input);
1860
1861 Range currentRange = (Range) ranges.next();
1862
1863 // Writing MIME header.
1864 ostream.println();
1865 ostream.println("--" + mimeSeparation);
1866 if (contentType != null)
1867 ostream.println("Content-Type: " + contentType);
1868 ostream.println("Content-Range: bytes " + currentRange.start
1869 + "-" + currentRange.end + "/"
1870 + currentRange.length);
1871 ostream.println();
1872
1873 // Printing content
1874 exception = copyRange(istream, ostream, currentRange.start,
1875 currentRange.end);
1876
1877 istream.close();
1878
1879 }
1880
1881 ostream.println();
1882 ostream.print("--" + mimeSeparation + "--");
1883
1884 // Rethrow any exception that has occurred
1885 if (exception != null)
1886 throw exception;
1887
1888 }
1889
1890
1891 /**
1892 * Copy the contents of the specified input stream to the specified
1893 * output stream, and ensure that both streams are closed before returning
1894 * (even in the face of an exception).
1895 *
1896 * @param resourceInfo The ResourceInfo object
1897 * @param writer The writer to write to
1898 * @param ranges Enumeration of the ranges the client wanted to retrieve
1899 * @param contentType Content type of the resource
1900 * @exception IOException if an input/output error occurs
1901 */
1902 protected void copy(CacheEntry cacheEntry, PrintWriter writer,
1903 Iterator ranges, String contentType)
1904 throws IOException {
1905
1906 IOException exception = null;
1907
1908 while ( (exception == null) && (ranges.hasNext()) ) {
1909
1910 InputStream resourceInputStream = cacheEntry.resource.streamContent();
1911
1912 Reader reader;
1913 if (fileEncoding == null) {
1914 reader = new InputStreamReader(resourceInputStream);
1915 } else {
1916 reader = new InputStreamReader(resourceInputStream,
1917 fileEncoding);
1918 }
1919
1920 Range currentRange = (Range) ranges.next();
1921
1922 // Writing MIME header.
1923 writer.println();
1924 writer.println("--" + mimeSeparation);
1925 if (contentType != null)
1926 writer.println("Content-Type: " + contentType);
1927 writer.println("Content-Range: bytes " + currentRange.start
1928 + "-" + currentRange.end + "/"
1929 + currentRange.length);
1930 writer.println();
1931
1932 // Printing content
1933 exception = copyRange(reader, writer, currentRange.start,
1934 currentRange.end);
1935
1936 reader.close();
1937
1938 }
1939
1940 writer.println();
1941 writer.print("--" + mimeSeparation + "--");
1942
1943 // Rethrow any exception that has occurred
1944 if (exception != null)
1945 throw exception;
1946
1947 }
1948
1949
1950 /**
1951 * Copy the contents of the specified input stream to the specified
1952 * output stream, and ensure that both streams are closed before returning
1953 * (even in the face of an exception).
1954 *
1955 * @param istream The input stream to read from
1956 * @param ostream The output stream to write to
1957 * @return Exception which occurred during processing
1958 */
1959 protected IOException copyRange(InputStream istream,
1960 ServletOutputStream ostream) {
1961
1962 // Copy the input stream to the output stream
1963 IOException exception = null;
1964 byte buffer[] = new byte[input];
1965 int len = buffer.length;
1966 while (true) {
1967 try {
1968 len = istream.read(buffer);
1969 if (len == -1)
1970 break;
1971 ostream.write(buffer, 0, len);
1972 } catch (IOException e) {
1973 exception = e;
1974 len = -1;
1975 break;
1976 }
1977 }
1978 return exception;
1979
1980 }
1981
1982
1983 /**
1984 * Copy the contents of the specified input stream to the specified
1985 * output stream, and ensure that both streams are closed before returning
1986 * (even in the face of an exception).
1987 *
1988 * @param reader The reader to read from
1989 * @param writer The writer to write to
1990 * @return Exception which occurred during processing
1991 */
1992 protected IOException copyRange(Reader reader, PrintWriter writer) {
1993
1994 // Copy the input stream to the output stream
1995 IOException exception = null;
1996 char buffer[] = new char[input];
1997 int len = buffer.length;
1998 while (true) {
1999 try {
2000 len = reader.read(buffer);
2001 if (len == -1)
2002 break;
2003 writer.write(buffer, 0, len);
2004 } catch (IOException e) {
2005 exception = e;
2006 len = -1;
2007 break;
2008 }
2009 }
2010 return exception;
2011
2012 }
2013
2014
2015 /**
2016 * Copy the contents of the specified input stream to the specified
2017 * output stream, and ensure that both streams are closed before returning
2018 * (even in the face of an exception).
2019 *
2020 * @param istream The input stream to read from
2021 * @param ostream The output stream to write to
2022 * @param start Start of the range which will be copied
2023 * @param end End of the range which will be copied
2024 * @return Exception which occurred during processing
2025 */
2026 protected IOException copyRange(InputStream istream,
2027 ServletOutputStream ostream,
2028 long start, long end) {
2029
2030 if (debug > 10)
2031 log("Serving bytes:" + start + "-" + end);
2032
2033 try {
2034 istream.skip(start);
2035 } catch (IOException e) {
2036 return e;
2037 }
2038
2039 IOException exception = null;
2040 long bytesToRead = end - start + 1;
2041
2042 byte buffer[] = new byte[input];
2043 int len = buffer.length;
2044 while ( (bytesToRead > 0) && (len >= buffer.length)) {
2045 try {
2046 len = istream.read(buffer);
2047 if (bytesToRead >= len) {
2048 ostream.write(buffer, 0, len);
2049 bytesToRead -= len;
2050 } else {
2051 ostream.write(buffer, 0, (int) bytesToRead);
2052 bytesToRead = 0;
2053 }
2054 } catch (IOException e) {
2055 exception = e;
2056 len = -1;
2057 }
2058 if (len < buffer.length)
2059 break;
2060 }
2061
2062 return exception;
2063
2064 }
2065
2066
2067 /**
2068 * Copy the contents of the specified input stream to the specified
2069 * output stream, and ensure that both streams are closed before returning
2070 * (even in the face of an exception).
2071 *
2072 * @param reader The reader to read from
2073 * @param writer The writer to write to
2074 * @param start Start of the range which will be copied
2075 * @param end End of the range which will be copied
2076 * @return Exception which occurred during processing
2077 */
2078 protected IOException copyRange(Reader reader, PrintWriter writer,
2079 long start, long end) {
2080
2081 try {
2082 reader.skip(start);
2083 } catch (IOException e) {
2084 return e;
2085 }
2086
2087 IOException exception = null;
2088 long bytesToRead = end - start + 1;
2089
2090 char buffer[] = new char[input];
2091 int len = buffer.length;
2092 while ( (bytesToRead > 0) && (len >= buffer.length)) {
2093 try {
2094 len = reader.read(buffer);
2095 if (bytesToRead >= len) {
2096 writer.write(buffer, 0, len);
2097 bytesToRead -= len;
2098 } else {
2099 writer.write(buffer, 0, (int) bytesToRead);
2100 bytesToRead = 0;
2101 }
2102 } catch (IOException e) {
2103 exception = e;
2104 len = -1;
2105 }
2106 if (len < buffer.length)
2107 break;
2108 }
2109
2110 return exception;
2111
2112 }
2113
2114
2115
2116 // ------------------------------------------------------ Range Inner Class
2117
2118
2119 protected class Range {
2120
2121 public long start;
2122 public long end;
2123 public long length;
2124
2125 /**
2126 * Validate range.
2127 */
2128 public boolean validate() {
2129 if (end >= length)
2130 end = length - 1;
2131 return ( (start >= 0) && (end >= 0) && (start <= end)
2132 && (length > 0) );
2133 }
2134
2135 public void recycle() {
2136 start = 0;
2137 end = 0;
2138 length = 0;
2139 }
2140
2141 }
2142
2143
2144 }