root/head/ambra/webapp/src/main/java/org/topazproject/ambra/web/HttpResourceServer.java @ 7771

Revision 7771, 36.0 KB (checked in by dragisak, 14 months ago)

Remove the need to build journal template jar files. Instead, templates are placed under directory specified in ambra.virtualJournals.templateDir configuration paramater.

  • Each journal is placed under journals/journal-name
  • Journal configuration file is placed in journals/journal-name/configuration/journal.xml
  • Freemarker templates and static resources (images, css, javascript, etc) are placed under journals/journal-name/webapp

Deployment can be done directly from version control system if ambra.virtualJournals.templateDir is set to point to source head directory.

Addresses #919

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id HeadURL Revision
Line 
1/* $HeadURL::                                                                            $
2 * $Id$
3 *
4 * Copyright (c) 2006-2009 by Topaz, Inc.
5 * http://topazproject.org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *     http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.topazproject.ambra.web;
20
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.InputStreamReader;
24import java.io.PrintWriter;
25import java.io.Reader;
26import java.io.File;
27import java.io.FileInputStream;
28
29import java.net.URL;
30import java.net.URLConnection;
31
32import java.text.SimpleDateFormat;
33
34import java.util.ArrayList;
35import java.util.Date;
36import java.util.Iterator;
37import java.util.Locale;
38import java.util.StringTokenizer;
39import java.util.TimeZone;
40
41import javax.servlet.ServletOutputStream;
42import javax.servlet.http.HttpServletRequest;
43import javax.servlet.http.HttpServletResponse;
44
45import org.apache.commons.logging.Log;
46import org.apache.commons.logging.LogFactory;
47
48/**
49 * A utility class that can serve up static resources (images, css, javascript etc.)  in
50 * response to HttpServletRequests. This can be included in any Servlet or Filter as needed.  It
51 * is based-on the DefaultServlet in catalina. (Revision 657157 of
52 * http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java)
53 */
54public class HttpResourceServer {
55  private static final Log log = LogFactory.getLog(HttpResourceServer.class);
56
57  /**
58   * The input buffer size to use when serving resources.
59   */
60  private static final int INPUT_BUFFER_SIZE = 16384;
61
62  /**
63   * The output buffer size to use when serving resources.
64   */
65  private static final int OUTPUT_BUFFER_SIZE = 16384;
66
67  /**
68   * File encoding to be used when reading static files. If none is specified  UTF-8 is used.
69   */
70  private static final String FILE_ENCODING = "UTF-8";
71
72  /**
73   * Full range marker.
74   */
75  private static final ArrayList<Range> FULL = new ArrayList<Range>();
76
77  /**
78   * MIME multipart separation string
79   */
80  private static final String MIME_SEPARATION = "AMBRA_MIME_BOUNDARY";
81
82  /**
83   * Serve the specified resource, optionally including the data content.
84   *
85   * @param request The servlet request we are processing
86   * @param response The servlet response we are creating
87   * @param resource The resource to send
88   *
89   * @exception IOException if an input/output error occurs
90   */
91  public void serveResource(HttpServletRequest request, HttpServletResponse response,
92                            Resource resource) throws IOException {
93    boolean head = "HEAD".equals(request.getMethod());
94    serveResource(request, response, !head, resource);
95  }
96
97  /**
98   * Serve the specified resource, optionally including the data content.
99   *
100   * @param request The servlet request we are processing
101   * @param response The servlet response we are creating
102   * @param content Should the content be included?
103   * @param resource The resource to send
104   *
105   * @exception IOException if an input/output error occurs
106   */
107  public void serveResource(HttpServletRequest request, HttpServletResponse response,
108                            boolean content, Resource resource)
109                     throws IOException {
110    // Check if the conditions specified in the optional If headers are satisfied.
111    if (!checkIfHeaders(request, response, resource))
112      return;
113
114    // Parse range specifier
115    ArrayList<Range> ranges = parseRange(request, response, resource);
116    // ETag header
117    response.setHeader("ETag", getETag(resource));
118    // Last-Modified header
119    response.setHeader("Last-Modified", resource.getLastModifiedHttp());
120
121    // Special case for zero length files, which would cause a
122    // (silent) ISE when setting the output buffer size
123    if (resource.getContentLength() == 0L)
124      content = false;
125
126    ServletOutputStream ostream       = null;
127    PrintWriter         writer        = null;
128    String              contentType   = resource.getContentType();
129    long                contentLength = resource.getContentLength();
130
131    if (content) {
132      // Trying to retrieve the servlet output stream
133      try {
134        ostream = response.getOutputStream();
135      } catch (IllegalStateException e) {
136        // If it fails, we try to get a Writer instead if we're trying to serve a text file
137        if ((contentType == null) || (contentType.startsWith("text"))
138             || (contentType.endsWith("xml"))) {
139          writer = response.getWriter();
140        } else {
141          throw e;
142        }
143      }
144    }
145
146    if ((((ranges == null) || (ranges.isEmpty())) && (request.getHeader("Range") == null))
147         || (ranges == FULL)) {
148      if (log.isDebugEnabled())
149        log.debug("Full content response for " + resource);
150
151      setOutputHeaders(response, contentType, contentLength, content);
152
153      // Copy the input stream to our output stream (if requested)
154      if (content) {
155        if (ostream != null) {
156          copy(resource, ostream);
157        } else {
158          copy(resource, writer);
159        }
160      }
161    } else {
162      if ((ranges == null) || (ranges.isEmpty()))
163        return;
164
165      if (log.isDebugEnabled())
166        log.debug("Partial content response for " + resource);
167
168      response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
169
170      if (ranges.size() == 1) {
171        Range range = ranges.get(0);
172        response.addHeader("Content-Range",
173                           "bytes " + range.start + "-" + range.end + "/" + range.length);
174
175        long length = range.end - range.start + 1;
176
177        setOutputHeaders(response, contentType, length, content);
178
179        if (content) {
180          if (ostream != null) {
181            copy(resource, ostream, range);
182          } else {
183            copy(resource, writer, range);
184          }
185        }
186      } else {
187        response.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATION);
188
189        if (content) {
190          try {
191            response.setBufferSize(OUTPUT_BUFFER_SIZE);
192          } catch (IllegalStateException e) {
193            // Silent catch
194          }
195
196          if (ostream != null) {
197            copy(resource, ostream, ranges.iterator(), contentType);
198          } else {
199            copy(resource, writer, ranges.iterator(), contentType);
200          }
201        }
202      }
203    }
204  }
205
206  /**
207   * Set the headers before streaming out content.
208   *
209   * @param response the response we are working with
210   * @param contentType the contentType to set
211   * @param contentLength the content length to set
212   * @param content false for sending only the headers
213   */
214  protected void setOutputHeaders(HttpServletResponse response, String contentType,
215                                  long contentLength, boolean content) {
216    // Set the appropriate output headers
217    if (contentType != null)
218      response.setContentType(contentType);
219
220    if (contentLength >= 0) {
221      if (contentLength < Integer.MAX_VALUE) {
222        response.setContentLength((int) contentLength);
223      } else {
224        // Set the content-length as String to be able to use a long
225        response.setHeader("content-length", "" + contentLength);
226      }
227    }
228
229    // Copy the input stream to our output stream (if requested)
230    if (content) {
231      try {
232        response.setBufferSize(OUTPUT_BUFFER_SIZE);
233      } catch (IllegalStateException e) {
234        // Silent catch
235      }
236    }
237  }
238
239  /**
240   * Parse the content-range header.
241   *
242   * @param request The servlet request we are processing
243   * @param response The servlet response we are creating
244   *
245   * @return Range
246   *
247   * @throws IOException on an error
248   */
249  protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response)
250                             throws IOException {
251    // Retrieving the content-range header (if any is specified
252    String rangeHeader = request.getHeader("Content-Range");
253
254    if (rangeHeader == null)
255      return null;
256
257    // bytes is the only range unit supported
258    if (!rangeHeader.startsWith("bytes")) {
259      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
260
261      return null;
262    }
263
264    rangeHeader = rangeHeader.substring(6).trim();
265
266    int dashPos  = rangeHeader.indexOf('-');
267    int slashPos = rangeHeader.indexOf('/');
268
269    if (dashPos == -1) {
270      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
271
272      return null;
273    }
274
275    if (slashPos == -1) {
276      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
277
278      return null;
279    }
280
281    Range range = new Range();
282
283    try {
284      range.start    = Long.parseLong(rangeHeader.substring(0, dashPos));
285      range.end      = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
286      range.length   = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
287    } catch (NumberFormatException e) {
288      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
289
290      return null;
291    }
292
293    if (!range.validate()) {
294      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
295
296      return null;
297    }
298
299    return range;
300  }
301
302  /**
303   * Parse the range header.
304   *
305   * @param request The servlet request we are processing
306   * @param response The servlet response we are creating
307   * @param resource The resource we are serving
308   *
309   * @return Vector of ranges
310   *
311   * @throws IOException on an error
312   */
313  protected ArrayList<Range> parseRange(HttpServletRequest request, HttpServletResponse response,
314                                 Resource resource) throws IOException {
315    // Checking If-Range
316    String headerValue = request.getHeader("If-Range");
317
318    if (headerValue != null) {
319      long headerValueTime = (-1L);
320
321      try {
322        headerValueTime = request.getDateHeader("If-Range");
323      } catch (IllegalArgumentException e) {
324      }
325
326      String eTag         = getETag(resource);
327      long   lastModified = resource.getLastModified();
328
329      if (headerValueTime == (-1L)) {
330        /*
331         * If the ETag the client gave does not match the entity etag, then the entire entity is
332         * returned.
333         */
334        if (!eTag.equals(headerValue.trim()))
335          return FULL;
336      } else {
337        /*
338         * If the timestamp of the entity the client got is older than the last modification date of
339         * the entity, the entire entity is returned.
340         */
341        if (lastModified > (headerValueTime + 1000))
342          return FULL;
343      }
344    }
345
346    long fileLength = resource.getContentLength();
347
348    if (fileLength == 0)
349      return null;
350
351    // Retrieving the range header (if any is specified
352    String rangeHeader = request.getHeader("Range");
353
354    if (rangeHeader == null)
355      return null;
356
357    // bytes is the only range unit supported (and I don't see the point
358    // of adding new ones).
359    if (!rangeHeader.startsWith("bytes")) {
360      response.addHeader("Content-Range", "bytes */" + fileLength);
361      response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
362
363      return null;
364    }
365
366    rangeHeader = rangeHeader.substring(6);
367
368    // Vector which will contain all the ranges which are successfully
369    // parsed.
370    ArrayList<Range> result         = new ArrayList<Range>();
371    StringTokenizer  commaTokenizer = new StringTokenizer(rangeHeader, ",");
372
373    // Parsing the range list
374    while (commaTokenizer.hasMoreTokens()) {
375      String rangeDefinition = commaTokenizer.nextToken().trim();
376
377      Range  currentRange    = new Range();
378      currentRange.length    = fileLength;
379
380      int dashPos            = rangeDefinition.indexOf('-');
381
382      if (dashPos == -1) {
383        response.addHeader("Content-Range", "bytes */" + fileLength);
384        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
385
386        return null;
387      }
388
389      if (dashPos == 0) {
390        try {
391          long offset = Long.parseLong(rangeDefinition);
392          currentRange.start   = fileLength + offset;
393          currentRange.end     = fileLength - 1;
394        } catch (NumberFormatException e) {
395          response.addHeader("Content-Range", "bytes */" + fileLength);
396          response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
397
398          return null;
399        }
400      } else {
401        try {
402          currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
403
404          if (dashPos < (rangeDefinition.length() - 1))
405            currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1,
406                                                                        rangeDefinition.length()));
407          else
408            currentRange.end = fileLength - 1;
409        } catch (NumberFormatException e) {
410          response.addHeader("Content-Range", "bytes */" + fileLength);
411          response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
412
413          return null;
414        }
415      }
416
417      if (!currentRange.validate()) {
418        response.addHeader("Content-Range", "bytes */" + fileLength);
419        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
420
421        return null;
422      }
423
424      result.add(currentRange);
425    }
426
427    return result;
428  }
429
430  /**
431   * Check if the conditions specified in the optional If headers are satisfied.
432   *
433   * @param request The servlet request we are processing
434   * @param response The servlet response we are creating
435   * @param resource The resource information
436   *
437   * @return boolean true if the resource meets all the specified conditions, and false if any of
438   *         the conditions is not satisfied, in which case request processing is stopped
439   *
440   * @throws IOException on an error
441   */
442  protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
443                                   Resource resource) throws IOException {
444    return checkIfMatch(request, response, resource)
445            && checkIfModifiedSince(request, response, resource)
446            && checkIfNoneMatch(request, response, resource)
447            && checkIfUnmodifiedSince(request, response, resource);
448  }
449
450  /**
451   * Get the ETag associated with a file.
452   *
453   * @param resource The resource information
454   *
455   * @return the ETag
456   */
457  protected String getETag(Resource resource) {
458    return "W/\"" + resource.getContentLength() + "-" + resource.getLastModified() + "\"";
459  }
460
461  /**
462   * Check if the if-match condition is satisfied.
463   *
464   * @param request The servlet request we are processing
465   * @param response The servlet response we are creating
466   * @param resource File object
467   *
468   * @return boolean true if the resource meets the specified condition, and false if the condition
469   *         is not satisfied, in which case request processing is stopped
470   *
471   * @throws IOException on an error
472   */
473  protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
474                                 Resource resource) throws IOException {
475    String eTag        = getETag(resource);
476    String headerValue = request.getHeader("If-Match");
477
478    if (headerValue != null) {
479      if (headerValue.indexOf('*') == -1) {
480        StringTokenizer commaTokenizer     = new StringTokenizer(headerValue, ",");
481        boolean         conditionSatisfied = false;
482
483        while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
484          String currentToken = commaTokenizer.nextToken();
485
486          if (currentToken.trim().equals(eTag))
487            conditionSatisfied = true;
488        }
489
490        // If none of the given ETags match, 412 Precodition failed is
491        // sent back
492        if (!conditionSatisfied) {
493          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
494
495          if (log.isDebugEnabled())
496            log.debug("If-Match: " + headerValue + ": Sending 'Pre-condition Failed' for "
497                      + resource);
498
499          return false;
500        }
501      }
502    }
503
504    return true;
505  }
506
507  /**
508   * Check if the if-modified-since condition is satisfied.
509   *
510   * @param request The servlet request we are processing
511   * @param response The servlet response we are creating
512   * @param resource File object
513   *
514   * @return boolean true if the resource meets the specified condition, and false if the condition
515   *         is not satisfied, in which case request processing is stopped
516   *
517   * @throws IOException on an error
518   */
519  protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response,
520                                         Resource resource)
521                                  throws IOException {
522    try {
523      long headerValue  = request.getDateHeader("If-Modified-Since");
524      long lastModified = resource.getLastModified();
525
526      if (headerValue != -1) {
527        // If an If-None-Match header has been specified, if modified since is ignored.
528        if ((request.getHeader("If-None-Match") == null) && (lastModified < (headerValue + 1000))) {
529          /*
530           * The entity has not been modified since the date specified by the client. This is not an
531           * error case.
532           */
533          response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
534          response.setHeader("ETag", getETag(resource));
535
536          if (log.isDebugEnabled())
537            log.debug("If-Modified-Since: " + headerValue + ": Sending 'Not Modified' for "
538                      + resource);
539
540          return false;
541        }
542      }
543    } catch (IllegalArgumentException illegalArgument) {
544      return true;
545    }
546
547    return true;
548  }
549
550  /**
551   * Check if the if-none-match condition is satisfied.
552   *
553   * @param request The servlet request we are processing
554   * @param response The servlet response we are creating
555   * @param resource File object
556   *
557   * @return boolean true if the resource meets the specified condition, and false if the condition
558   *         is not satisfied, in which case request processing is stopped
559   *
560   * @throws IOException on an error
561   */
562  protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response,
563                                     Resource resource)
564                              throws IOException {
565    String eTag        = getETag(resource);
566    String headerValue = request.getHeader("If-None-Match");
567
568    if (headerValue != null) {
569      boolean conditionSatisfied = false;
570
571      if (!headerValue.equals("*")) {
572        StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
573
574        while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
575          String currentToken = commaTokenizer.nextToken();
576
577          if (currentToken.trim().equals(eTag))
578            conditionSatisfied = true;
579        }
580      } else {
581        conditionSatisfied = true;
582      }
583
584      if (conditionSatisfied) {
585        /*
586         * For GET and HEAD, we should respond with 304 Not Modified.
587         * For every other method, 412 Precondition Failed is sent back.
588         */
589        if (("GET".equals(request.getMethod())) || ("HEAD".equals(request.getMethod()))) {
590          response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
591          response.setHeader("ETag", getETag(resource));
592
593          if (log.isDebugEnabled())
594            log.debug("If-None-Match: " + headerValue + ": Sending 'Not Modified' for " + resource);
595
596          return false;
597        } else {
598          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
599
600          if (log.isDebugEnabled())
601            log.debug("If-None-Match: " + headerValue + ": Sending 'Pre-condition failed' for " +
602                      resource);
603
604          return false;
605        }
606      }
607    }
608
609    return true;
610  }
611
612  /**
613   * Check if the if-unmodified-since condition is satisfied.
614   *
615   * @param request The servlet request we are processing
616   * @param response The servlet response we are creating
617   * @param resource File object
618   *
619   * @return boolean true if the resource meets the specified condition, and false if the condition
620   *         is not satisfied, in which case request processing is stopped
621   *
622   * @throws IOException on an error
623   */
624  protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
625                                           HttpServletResponse response, Resource resource)
626                                    throws IOException {
627    try {
628      long lastModified = resource.getLastModified();
629      long headerValue  = request.getDateHeader("If-Unmodified-Since");
630
631      if (headerValue != -1) {
632        if (lastModified >= (headerValue + 1000)) {
633          /*
634           * The entity has not been modified since the date specified by the client. This is not an
635           * error case.
636           */
637          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
638
639          if (log.isDebugEnabled())
640            log.debug("If-Unmodified-Since: " + headerValue
641                      + ": Sending 'Pre-condition failed' for " + resource);
642
643          return false;
644        }
645      }
646    } catch (IllegalArgumentException illegalArgument) {
647      return true;
648    }
649
650    return true;
651  }
652
653  /**
654   * Copy the contents of the specified input stream to the specified output stream, and
655   * ensure that both streams are closed before returning (even in the face of an exception).
656   *
657   * @param resource The resource information
658   * @param ostream The output stream to write to
659   *
660   * @exception IOException if an input/output error occurs
661   */
662  protected void copy(Resource resource, ServletOutputStream ostream)
663               throws IOException {
664    // Optimization: If the binary content has already been loaded, send it directly
665    byte[] buffer = resource.getContent();
666
667    if (buffer != null) {
668      ostream.write(buffer, 0, buffer.length);
669
670      return;
671    }
672
673    // Copy the input stream to the output stream
674    InputStream istream   = resource.streamContent();
675    IOException exception = copyRange(istream, ostream);
676
677    // Clean up the input stream
678    istream.close();
679
680    // Rethrow any exception that has occurred
681    if (exception != null)
682      throw exception;
683  }
684
685  /**
686   * Copy the contents of the specified input stream to the specified output stream, and
687   * ensure that both streams are closed before returning (even in the face of an exception).
688   *
689   * @param resource The resource info
690   * @param writer The writer to write to
691   *
692   * @exception IOException if an input/output error occurs
693   */
694  protected void copy(Resource resource, PrintWriter writer) throws IOException {
695    InputStream resourceInputStream = resource.streamContent();
696
697    Reader reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
698
699    // Copy the input stream to the output stream
700    IOException exception = copyRange(reader, writer);
701
702    // Clean up the reader
703    reader.close();
704
705    // Rethrow any exception that has occurred
706    if (exception != null)
707      throw exception;
708  }
709
710  /**
711   * Copy the contents of the specified input stream to the specified output stream, and
712   * ensure that both streams are closed before returning (even in the face of an exception).
713   *
714   * @param resource The ResourceInfo object
715   * @param ostream The output stream to write to
716   * @param range Range the client wanted to retrieve
717   *
718   * @exception IOException if an input/output error occurs
719   */
720  protected void copy(Resource resource, ServletOutputStream ostream, Range range)
721               throws IOException {
722    InputStream istream   = resource.streamContent();
723    IOException exception = copyRange(istream, ostream, range.start, range.end);
724
725    // Clean up the input stream
726    istream.close();
727
728    // Rethrow any exception that has occurred
729    if (exception != null)
730      throw exception;
731  }
732
733  /**
734   * Copy the contents of the specified input stream to the specified output stream, and
735   * ensure that both streams are closed before returning (even in the face of an exception).
736   *
737   * @param resource The ResourceInfo object
738   * @param writer The writer to write to
739   * @param range Range the client wanted to retrieve
740   *
741   * @exception IOException if an input/output error occurs
742   */
743  protected void copy(Resource resource, PrintWriter writer, Range range)
744               throws IOException {
745    InputStream resourceInputStream = resource.streamContent();
746
747    Reader      reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
748
749    IOException exception = copyRange(reader, writer, range.start, range.end);
750
751    // Clean up the input stream
752    reader.close();
753
754    // Rethrow any exception that has occurred
755    if (exception != null)
756      throw exception;
757  }
758
759  /**
760   * Copy the contents of the specified input stream to the specified output stream, and
761   * ensure that both streams are closed before returning (even in the face of an exception).
762   *
763   * @param resource The ResourceInfo object
764   * @param ostream The output stream to write to
765   * @param ranges Enumeration of the ranges the client wanted to retrieve
766   * @param contentType Content type of the resource
767   *
768   * @exception IOException if an input/output error occurs
769   */
770  protected void copy(Resource resource, ServletOutputStream ostream, Iterator ranges,
771                      String contentType) throws IOException {
772    IOException exception = null;
773
774    while ((exception == null) && (ranges.hasNext())) {
775      InputStream istream      = resource.streamContent();
776      Range       currentRange = (Range) ranges.next();
777
778      // Writing MIME header.
779      ostream.println();
780      ostream.println("--" + MIME_SEPARATION);
781
782      if (contentType != null)
783        ostream.println("Content-Type: " + contentType);
784
785      ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/"
786                      + currentRange.length);
787      ostream.println();
788
789      // Printing content
790      exception = copyRange(istream, ostream, currentRange.start, currentRange.end);
791
792      istream.close();
793    }
794
795    ostream.println();
796    ostream.print("--" + MIME_SEPARATION + "--");
797
798    // Rethrow any exception that has occurred
799    if (exception != null)
800      throw exception;
801  }
802
803  /**
804   * Copy the contents of the specified input stream to the specified output stream, and
805   * ensure that both streams are closed before returning (even in the face of an exception).
806   *
807   * @param resource The ResourceInfo object
808   * @param writer The writer to write to
809   * @param ranges Enumeration of the ranges the client wanted to retrieve
810   * @param contentType Content type of the resource
811   *
812   * @exception IOException if an input/output error occurs
813   */
814  protected void copy(Resource resource, PrintWriter writer, Iterator ranges, String contentType)
815               throws IOException {
816    IOException exception = null;
817
818    while ((exception == null) && (ranges.hasNext())) {
819      InputStream resourceInputStream = resource.streamContent();
820
821      Reader      reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
822
823      Range currentRange = (Range) ranges.next();
824
825      // Writing MIME header.
826      writer.println();
827      writer.println("--" + MIME_SEPARATION);
828
829      if (contentType != null)
830        writer.println("Content-Type: " + contentType);
831
832      writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" +
833                     currentRange.length);
834      writer.println();
835
836      // Printing content
837      exception = copyRange(reader, writer, currentRange.start, currentRange.end);
838
839      reader.close();
840    }
841
842    writer.println();
843    writer.print("--" + MIME_SEPARATION + "--");
844
845    // Rethrow any exception that has occurred
846    if (exception != null)
847      throw exception;
848  }
849
850  /**
851   * Copy the contents of the specified input stream to the specified output stream, and
852   * ensure that both streams are closed before returning (even in the face of an exception).
853   *
854   * @param istream The input stream to read from
855   * @param ostream The output stream to write to
856   *
857   * @return Exception which occurred during processing
858   */
859  protected IOException copyRange(InputStream istream, ServletOutputStream ostream) {
860    // Copy the input stream to the output stream
861    IOException exception = null;
862    byte[]      buffer    = new byte[INPUT_BUFFER_SIZE];
863    int         len;
864
865    while (true) {
866      try {
867        len = istream.read(buffer);
868
869        if (len == -1)
870          break;
871
872        ostream.write(buffer, 0, len);
873      } catch (IOException e) {
874        exception   = e;
875        break;
876      }
877    }
878
879    return exception;
880  }
881
882  /**
883   * Copy the contents of the specified input stream to the specified output stream, and
884   * ensure that both streams are closed before returning (even in the face of an exception).
885   *
886   * @param reader The reader to read from
887   * @param writer The writer to write to
888   *
889   * @return Exception which occurred during processing
890   */
891  protected IOException copyRange(Reader reader, PrintWriter writer) {
892    // Copy the input stream to the output stream
893    IOException exception = null;
894    char[]      buffer    = new char[INPUT_BUFFER_SIZE];
895    int         len;
896
897    while (true) {
898      try {
899        len = reader.read(buffer);
900
901        if (len == -1)
902          break;
903
904        writer.write(buffer, 0, len);
905      } catch (IOException e) {
906        exception   = e;
907        break;
908      }
909    }
910
911    return exception;
912  }
913
914  /**
915   * Copy the contents of the specified input stream to the specified output stream, and
916   * ensure that both streams are closed before returning (even in the face of an exception).
917   *
918   * @param istream The input stream to read from
919   * @param ostream The output stream to write to
920   * @param start Start of the range which will be copied
921   * @param end End of the range which will be copied
922   *
923   * @return Exception which occurred during processing
924   */
925  protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start,
926                                  long end) {
927    if (log.isTraceEnabled())
928      log.trace("Serving bytes:" + start + "-" + end);
929
930    try {
931      istream.skip(start);
932    } catch (IOException e) {
933      return e;
934    }
935
936    IOException exception   = null;
937    long        bytesToRead = end - start + 1;
938
939    byte[]      buffer      = new byte[INPUT_BUFFER_SIZE];
940    int         len         = buffer.length;
941
942    while ((bytesToRead > 0) && (len >= buffer.length)) {
943      try {
944        len = istream.read(buffer);
945
946        if (bytesToRead >= len) {
947          ostream.write(buffer, 0, len);
948          bytesToRead -= len;
949        } else {
950          ostream.write(buffer, 0, (int) bytesToRead);
951          bytesToRead = 0;
952        }
953      } catch (IOException e) {
954        exception   = e;
955        len         = -1;
956      }
957
958      if (len < buffer.length)
959        break;
960    }
961
962    return exception;
963  }
964
965  /**
966   * Copy the contents of the specified input stream to the specified output stream, and
967   * ensure that both streams are closed before returning (even in the face of an exception).
968   *
969   * @param reader The reader to read from
970   * @param writer The writer to write to
971   * @param start Start of the range which will be copied
972   * @param end End of the range which will be copied
973   *
974   * @return Exception which occurred during processing
975   */
976  protected IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
977    try {
978      reader.skip(start);
979    } catch (IOException e) {
980      return e;
981    }
982
983    IOException exception   = null;
984    long        bytesToRead = end - start + 1;
985
986    char[]      buffer      = new char[INPUT_BUFFER_SIZE];
987    int         len         = buffer.length;
988
989    while ((bytesToRead > 0) && (len >= buffer.length)) {
990      try {
991        len = reader.read(buffer);
992
993        if (bytesToRead >= len) {
994          writer.write(buffer, 0, len);
995          bytesToRead -= len;
996        } else {
997          writer.write(buffer, 0, (int) bytesToRead);
998          bytesToRead = 0;
999        }
1000      } catch (IOException e) {
1001        exception   = e;
1002        len         = -1;
1003      }
1004
1005      if (len < buffer.length)
1006        break;
1007    }
1008
1009    return exception;
1010  }
1011
1012  public static abstract class Resource {
1013    private final String name;
1014    private final long      contentLength;
1015    private final long      lastModified;
1016    private final String    contentType;
1017    private final String    lastModifiedHttp;
1018
1019    public Resource(String name, String contentType, long contentLength, long lastModified) {
1020      this.name = name;
1021      this.contentType = (contentType == null) ? guessContentType(name) : contentType;
1022      this.contentLength = contentLength;
1023      this.lastModified = lastModified;
1024
1025      //RFC 1123 date. eg. Tue, 20 May 2008 13:45:26 GMT and always in English
1026      SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
1027      fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1028      lastModifiedHttp = fmt.format(new Date(lastModified));
1029    }
1030
1031    // Copied from Struts FilterDispatcher
1032    public static String guessContentType(String name) {
1033      // NOT using the code provided activation.jar to avoid adding yet another dependency
1034      // this is generally OK, since these are the main files we server up
1035      if (name.endsWith(".js")) {
1036        return "text/javascript";
1037      } else if (name.endsWith(".css")) {
1038        return "text/css";
1039      } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
1040        return "image/jpeg";
1041      } else if (name.endsWith(".png")) {
1042        return "image/png";
1043      } else if (name.endsWith(".gif")) {
1044        return "image/gif";
1045      } else if (name.endsWith(".html")) {
1046        return "text/html";
1047      } else if (name.endsWith(".txt")) {
1048        return "text/plain";
1049      } else {
1050        return null;
1051      }
1052    }
1053
1054    public String getContentType() {
1055      return contentType;
1056    }
1057
1058    public long getContentLength() {
1059      return contentLength;
1060    }
1061
1062    public abstract InputStream streamContent() throws IOException;
1063
1064    public abstract byte[] getContent();
1065
1066    public long getLastModified() {
1067      return lastModified;
1068    }
1069
1070    public String getLastModifiedHttp() {
1071      return lastModifiedHttp;
1072    }
1073
1074    public String toString() {
1075      return "Resource[name=" + name +
1076             ", contentType=" + contentType +
1077             ", contentLength=" + contentLength +
1078             ",lastModified=" + lastModified +
1079             "(" + lastModifiedHttp + ")]";
1080    }
1081  }
1082
1083  public static class FileResource extends Resource {
1084
1085    private final File file;
1086
1087    public FileResource(File file) {
1088      super(file.getName(), guessContentType(file.getName()), file.length(), file.lastModified());
1089      this.file = file;
1090    }
1091
1092    public InputStream streamContent() throws IOException {
1093      return new FileInputStream(file);
1094    }
1095
1096    public byte[] getContent() {
1097      return null;
1098    }
1099  }
1100
1101
1102  public static class URLResource extends Resource {
1103    private final URL url;
1104
1105    public URLResource(URL url) throws IOException {
1106      this(url, url.openConnection());
1107    }
1108
1109    private URLResource(URL url, URLConnection con) {
1110      super(url.toString(), urlContentType(url, con), con.getContentLength(), con.getLastModified());
1111      this.url = url;
1112    }
1113
1114    private static String urlContentType(URL url, URLConnection con) {
1115      //XXX: guess first and then look in con
1116      String contentType = guessContentType(url.toString());
1117      if (contentType == null)
1118        contentType = con.getContentType();
1119      return contentType;
1120    }
1121
1122    public InputStream streamContent() throws IOException {
1123      return url.openStream();
1124    }
1125
1126    public byte[] getContent() {
1127      return null;
1128    }
1129  }
1130
1131  protected static class Range {
1132    public long start;
1133    public long end;
1134    public long length;
1135
1136    /**
1137     * Validate range.
1138     *
1139     * @return true if this is a valid range
1140     */
1141    public boolean validate() {
1142      if (end >= length)
1143        end = length - 1;
1144
1145      return ((start >= 0) && (end >= 0) && (start <= end) && (length > 0));
1146    }
1147
1148    public void recycle() {
1149      start    = 0;
1150      end      = 0;
1151      length   = 0;
1152    }
1153  }
1154}
Note: See TracBrowser for help on using the browser.