/* $HeadURL:: $
* $Id$
*
* Copyright (c) 2006-2010 by Public Library of Science
* http://plos.org
* http://ambraproject.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ambraproject.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.TimeZone;
/**
* A utility class that can serve up static resources (images, css, javascript etc.) in
* response to HttpServletRequests. This can be included in any Servlet or Filter as needed. It
* is based-on the DefaultServlet in catalina. (Revision 657157 of
* http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java)
*/
public class HttpResourceServer {
private static final Logger log = LoggerFactory.getLogger(HttpResourceServer.class);
/**
* The input buffer size to use when serving resources.
*/
private static final int INPUT_BUFFER_SIZE = 16384;
/**
* The output buffer size to use when serving resources.
*/
private static final int OUTPUT_BUFFER_SIZE = 16384;
/**
* File encoding to be used when reading static files. If none is specified UTF-8 is used.
*/
private static final String FILE_ENCODING = "UTF-8";
/**
* Full range marker.
*/
private static final ArrayList<Range> FULL = new ArrayList<Range>();
/**
* MIME multipart separation string
*/
private static final String MIME_SEPARATION = "AMBRA_MIME_BOUNDARY";
/**
* Serve the specified resource, optionally including the data content.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource to send
*
* @exception IOException if an input/output error occurs
*/
public void serveResource(HttpServletRequest request, HttpServletResponse response,
Resource resource, String xReproxy) throws IOException {
boolean content = "HEAD".equals(request.getMethod()) || xReproxy != null;
serveResource(request, response, !content, resource);
}
/**
* Serve the specified resource, optionally including the data content.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param content Should the content be included?
* @param resource The resource to send
*
* @exception IOException if an input/output error occurs
*/
public void serveResource(HttpServletRequest request, HttpServletResponse response,
boolean content, Resource resource)
throws IOException {
InputStream resourceInputStream = null;
try {
resourceInputStream = resource.streamContent();
// Check if the conditions specified in the optional If headers are satisfied.
if (!checkIfHeaders(request, response, resource))
return;
// Parse range specifier
ArrayList<Range> ranges = parseRange(request, response, resource);
// ETag header
response.setHeader("ETag", getETag(resource));
// Last-Modified header
response.setHeader("Last-Modified", resource.getLastModifiedHttp());
// Special case for zero length files, which would cause a
// (silent) ISE when setting the output buffer size
if (resource.getContentLength() == 0L)
content = false;
ServletOutputStream ostream = null;
PrintWriter writer = null;
String contentType = resource.getContentType();
long contentLength = resource.getContentLength();
if (content) {
// Trying to retrieve the servlet output stream
try {
ostream = response.getOutputStream();
} catch (IllegalStateException e) {
// If it fails, we try to get a Writer instead if we're trying to serve a text file
if ((contentType == null) || (contentType.startsWith("text"))
|| (contentType.endsWith("xml"))) {
writer = response.getWriter();
} else {
throw e;
}
}
}
if ((((ranges == null) || (ranges.isEmpty())) && (request.getHeader("Range") == null))
|| (ranges == FULL)) {
if (log.isDebugEnabled())
log.debug("Full content response for " + resource);
setOutputHeaders(response, contentType, contentLength, content);
// Copy the input stream to our output stream (if requested)
if (content) {
if (ostream != null) {
copy(resource.getContent(), resourceInputStream, ostream);
} else {
copy(resourceInputStream, writer);
}
}
} else {
if ((ranges == null) || (ranges.isEmpty()))// {
//resource.streamContent().close();
return;
//}
if (log.isDebugEnabled())
log.debug("Partial content response for " + resource);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (ranges.size() == 1) {
Range range = ranges.get(0);
response.addHeader("Content-Range",
"bytes " + range.start + "-" + range.end + "/" + range.length);
long length = range.end - range.start + 1;
setOutputHeaders(response, contentType, length, content);
if (content) {
if (ostream != null) {
copy(resourceInputStream, ostream, range);
} else {
copy(resourceInputStream, writer, range);
}
}
} else {
response.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATION);
if (content) {
try {
response.setBufferSize(OUTPUT_BUFFER_SIZE);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
copy(resourceInputStream, ostream, ranges.iterator(), contentType);
} else {
copy(resourceInputStream, writer, ranges.iterator(), contentType);
}
}
}
}
} finally {
if (resourceInputStream != null)
resourceInputStream.close();
}
}
/**
* Set the headers before streaming out content.
*
* @param response the response we are working with
* @param contentType the contentType to set
* @param contentLength the content length to set
* @param content false for sending only the headers
*/
protected void setOutputHeaders(HttpServletResponse response, String contentType,
long contentLength, boolean content) {
// Set the appropriate output headers
if (contentType != null)
response.setContentType(contentType);
if (contentLength >= 0) {
if (contentLength < Integer.MAX_VALUE) {
response.setContentLength((int) contentLength);
} else {
// Set the content-length as String to be able to use a long
response.setHeader("content-length", "" + contentLength);
}
}
// Copy the input stream to our output stream (if requested)
if (content) {
try {
response.setBufferSize(OUTPUT_BUFFER_SIZE);
} catch (IllegalStateException e) {
// Silent catch
}
}
}
/**
* Parse the content-range header.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @return Range
*
* @throws IOException on an error
*/
protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// Retrieving the content-range header (if any is specified
String rangeHeader = request.getHeader("Content-Range");
if (rangeHeader == null)
return null;
// bytes is the only range unit supported
if (!rangeHeader.startsWith("bytes")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
rangeHeader = rangeHeader.substring(6).trim();
int dashPos = rangeHeader.indexOf('-');
int slashPos = rangeHeader.indexOf('/');
if (dashPos == -1) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
if (slashPos == -1) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
Range range = new Range();
try {
range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
} catch (NumberFormatException e) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
if (!range.validate()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
return range;
}
/**
* Parse the range header.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource we are serving
*
* @return Vector of ranges
*
* @throws IOException on an error
*/
protected ArrayList<Range> parseRange(HttpServletRequest request, HttpServletResponse response,
Resource resource) throws IOException {
// Checking If-Range
String headerValue = request.getHeader("If-Range");
if (headerValue != null) {
long headerValueTime = (-1L);
try {
headerValueTime = request.getDateHeader("If-Range");
} catch (IllegalArgumentException e) {
}
String eTag = getETag(resource);
long lastModified = resource.getLastModified();
if (headerValueTime == (-1L)) {
/*
* If the ETag the client gave does not match the entity etag, then the entire entity is
* returned.
*/
if (!eTag.equals(headerValue.trim()))
return FULL;
} else {
/*
* If the timestamp of the entity the client got is older than the last modification date of
* the entity, the entire entity is returned.
*/
if (lastModified > (headerValueTime + 1000))
return FULL;
}
}
long fileLength = resource.getContentLength();
if (fileLength == 0)
return null;
// Retrieving the range header (if any is specified
String rangeHeader = request.getHeader("Range");
if (rangeHeader == null)
return null;
// bytes is the only range unit supported (and I don't see the point
// of adding new ones).
if (!rangeHeader.startsWith("bytes")) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
rangeHeader = rangeHeader.substring(6);
// Vector which will contain all the ranges which are successfully
// parsed.
ArrayList<Range> result = new ArrayList<Range>();
StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
// Parsing the range list
while (commaTokenizer.hasMoreTokens()) {
String rangeDefinition = commaTokenizer.nextToken().trim();
Range currentRange = new Range();
currentRange.length = fileLength;
int dashPos = rangeDefinition.indexOf('-');
if (dashPos == -1) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
if (dashPos == 0) {
try {
long offset = Long.parseLong(rangeDefinition);
currentRange.start = fileLength + offset;
currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
} else {
try {
currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
if (dashPos < (rangeDefinition.length() - 1))
currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1,
rangeDefinition.length()));
else
currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
}
if (!currentRange.validate()) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
result.add(currentRange);
}
return result;
}
/**
* Check if the conditions specified in the optional If headers are satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource information
*
* @return boolean true if the resource meets all the specified conditions, and false if any of
* the conditions is not satisfied, in which case request processing is stopped
*
* @throws IOException on an error
*/
protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
Resource resource) throws IOException {
return checkIfMatch(request, response, resource)
&& checkIfModifiedSince(request, response, resource)
&& checkIfNoneMatch(request, response, resource)
&& checkIfUnmodifiedSince(request, response, resource);
}
/**
* Get the ETag associated with a file.
*
* @param resource The resource information
*
* @return the ETag
*/
protected String getETag(Resource resource) {
return "W/\"" + resource.getContentLength() + "-" + resource.getLastModified() + "\"";
}
/**
* Check if the if-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource File object
*
* @return boolean true if the resource meets the specified condition, and false if the condition
* is not satisfied, in which case request processing is stopped
*
* @throws IOException on an error
*/
protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
Resource resource) throws IOException {
String eTag = getETag(resource);
String headerValue = request.getHeader("If-Match");
if (headerValue != null) {
if (headerValue.indexOf('*') == -1) {
StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
boolean conditionSatisfied = false;
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag))
conditionSatisfied = true;
}
// If none of the given ETags match, 412 Precodition failed is
// sent back
if (!conditionSatisfied) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
if (log.isDebugEnabled())
log.debug("If-Match: " + headerValue + ": Sending 'Pre-condition Failed' for "
+ resource);
return false;
}
}
}
return true;
}
/**
* Check if the if-modified-since condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource File object
*
* @return boolean true if the resource meets the specified condition, and false if the condition
* is not satisfied, in which case request processing is stopped
*
* @throws IOException on an error
*/
protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response,
Resource resource)
throws IOException {
try {
long headerValue = request.getDateHeader("If-Modified-Since");
long lastModified = resource.getLastModified();
if (headerValue != -1) {
// If an If-None-Match header has been specified, if modified since is ignored.
if ((request.getHeader("If-None-Match") == null) && (lastModified < (headerValue + 1000))) {
/*
* The entity has not been modified since the date specified by the client. This is not an
* error case.
*/
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", getETag(resource));
if (log.isDebugEnabled())
log.debug("If-Modified-Since: " + headerValue + ": Sending 'Not Modified' for "
+ resource);
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
return true;
}
return true;
}
/**
* Check if the if-none-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource File object
*
* @return boolean true if the resource meets the specified condition, and false if the condition
* is not satisfied, in which case request processing is stopped
*
* @throws IOException on an error
*/
protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response,
Resource resource)
throws IOException {
String eTag = getETag(resource);
String headerValue = request.getHeader("If-None-Match");
if (headerValue != null) {
boolean conditionSatisfied = false;
if (!headerValue.equals("*")) {
StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag))
conditionSatisfied = true;
}
} else {
conditionSatisfied = true;
}
if (conditionSatisfied) {
/*
* For GET and HEAD, we should respond with 304 Not Modified.
* For every other method, 412 Precondition Failed is sent back.
*/
if (("GET".equals(request.getMethod())) || ("HEAD".equals(request.getMethod()))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", getETag(resource));
if (log.isDebugEnabled())
log.debug("If-None-Match: " + headerValue + ": Sending 'Not Modified' for " + resource);
return false;
} else {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
if (log.isDebugEnabled())
log.debug("If-None-Match: " + headerValue + ": Sending 'Pre-condition failed' for " +
resource);
return false;
}
}
}
return true;
}
/**
* Check if the if-unmodified-since condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource File object
*
* @return boolean true if the resource meets the specified condition, and false if the condition
* is not satisfied, in which case request processing is stopped
*
* @throws IOException on an error
*/
protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
HttpServletResponse response, Resource resource)
throws IOException {
try {
long lastModified = resource.getLastModified();
long headerValue = request.getDateHeader("If-Unmodified-Since");
if (headerValue != -1) {
if (lastModified >= (headerValue + 1000)) {
/*
* The entity has not been modified since the date specified by the client. This is not an
* error case.
*/
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
if (log.isDebugEnabled())
log.debug("If-Unmodified-Since: " + headerValue
+ ": Sending 'Pre-condition failed' for " + resource);
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
return true;
}
return true;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param buffer The binary content
* @param resourceInputStream The stream to the Resource object
* @param ostream The output stream to write to
*
* @exception IOException if an input/output error occurs
*/
protected void copy(byte[] buffer, InputStream resourceInputStream, ServletOutputStream ostream)
throws IOException {
// Optimization: If the binary content has already been loaded, send it directly
//byte[] buffer = resource.getContent();
if (buffer != null) {
ostream.write(buffer, 0, buffer.length);
return;
}
// Copy the input stream to the output stream
//InputStream istream = resource.streamContent();
IOException exception = copyRange(resourceInputStream, ostream);
// Clean up the input stream
//istream.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param resourceInputStream The stream to the Resource object
* @param writer The writer to write to
*
* @exception IOException if an input/output error occurs
*/
protected void copy(InputStream resourceInputStream, PrintWriter writer) throws IOException {
Reader reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
// Copy the input stream to the output stream
IOException exception = copyRange(reader, writer);
// Clean up the reader
reader.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param resourceInputStream The stream to the Resource object
* @param ostream The output stream to write to
* @param range Range the client wanted to retrieve
*
* @exception IOException if an input/output error occurs
*/
protected void copy(InputStream resourceInputStream, ServletOutputStream ostream, Range range)
throws IOException {
IOException exception = copyRange(resourceInputStream, ostream, range.start, range.end);
// Clean up the input stream
// resourceInputStream.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param resourceInputStream The stream to the Resource object
* @param writer The writer to write to
* @param range Range the client wanted to retrieve
*
* @exception IOException if an input/output error occurs
*/
protected void copy(InputStream resourceInputStream, PrintWriter writer, Range range)
throws IOException {
Reader reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
IOException exception = copyRange(reader, writer, range.start, range.end);
// Clean up the input stream
reader.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param resourceInputStream The stream to the Resource object
* @param ostream The output stream to write to
* @param ranges Enumeration of the ranges the client wanted to retrieve
* @param contentType Content type of the resource
*
* @exception IOException if an input/output error occurs
*/
protected void copy(InputStream resourceInputStream, ServletOutputStream ostream, Iterator ranges,
String contentType) throws IOException {
IOException exception = null;
while ((exception == null) && (ranges.hasNext())) {
Range currentRange = (Range) ranges.next();
// Writing MIME header.
ostream.println();
ostream.println("--" + MIME_SEPARATION);
if (contentType != null)
ostream.println("Content-Type: " + contentType);
ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/"
+ currentRange.length);
ostream.println();
// Printing content
exception = copyRange(resourceInputStream, ostream, currentRange.start, currentRange.end);
// resourceInputStream.close();
}
ostream.println();
ostream.print("--" + MIME_SEPARATION + "--");
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param resourceInputStream The stream to the Resource object
* @param writer The writer to write to
* @param ranges Enumeration of the ranges the client wanted to retrieve
* @param contentType Content type of the resource
*
* @exception IOException if an input/output error occurs
*/
protected void copy(InputStream resourceInputStream, PrintWriter writer, Iterator ranges, String contentType)
throws IOException {
IOException exception = null;
while ((exception == null) && (ranges.hasNext())) {
Reader reader = new InputStreamReader(resourceInputStream, FILE_ENCODING);
Range currentRange = (Range) ranges.next();
// Writing MIME header.
writer.println();
writer.println("--" + MIME_SEPARATION);
if (contentType != null)
writer.println("Content-Type: " + contentType);
writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" +
currentRange.length);
writer.println();
// Printing content
exception = copyRange(reader, writer, currentRange.start, currentRange.end);
reader.close();
}
writer.println();
writer.print("--" + MIME_SEPARATION + "--");
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
* <br/>
* If an IOException occurs, then log that exception but do not return it like the other
* <code>copyRange(...)</code> methods do.
*
* @param istream The input stream to read from
* @param ostream The output stream to write to
*
* @return <code>null</code> in all circumstances; returning IOException does nothing useful
*/
protected IOException copyRange(InputStream istream, ServletOutputStream ostream) {
// Copy the input stream to the output stream
byte[] buffer = new byte[INPUT_BUFFER_SIZE];
int len;
while (true) {
try {
len = istream.read(buffer);
if (len == -1)
break;
ostream.write(buffer, 0, len);
} catch (IOException e) {
// If there is an exception, then log it and ignore it. IOException here tends to happen
// because of a loss of connectivity with the client, usually due to a "broken pipe".
// Throwing does no good because there is no mechanism for re-sending the failed content.
log.warn("Failure while attempting to copy an Input Stream to an Output Stream.", e);
break;
}
}
return null;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param reader The reader to read from
* @param writer The writer to write to
*
* @return Exception which occurred during processing
*/
protected IOException copyRange(Reader reader, PrintWriter writer) {
// Copy the input stream to the output stream
IOException exception = null;
char[] buffer = new char[INPUT_BUFFER_SIZE];
int len;
while (true) {
try {
len = reader.read(buffer);
if (len == -1)
break;
writer.write(buffer, 0, len);
} catch (IOException e) {
exception = e;
break;
}
}
return exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param istream The input stream to read from
* @param ostream The output stream to write to
* @param start Start of the range which will be copied
* @param end End of the range which will be copied
*
* @return Exception which occurred during processing
*/
protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start,
long end) {
if (log.isTraceEnabled())
log.trace("Serving bytes:" + start + "-" + end);
try {
istream.skip(start);
} catch (IOException e) {
return e;
}
IOException exception = null;
long bytesToRead = end - start + 1;
byte[] buffer = new byte[INPUT_BUFFER_SIZE];
int len = buffer.length;
while ((bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length)
break;
}
return exception;
}
/**
* Copy the contents of the specified input stream to the specified output stream, and
* ensure that both streams are closed before returning (even in the face of an exception).
*
* @param reader The reader to read from
* @param writer The writer to write to
* @param start Start of the range which will be copied
* @param end End of the range which will be copied
*
* @return Exception which occurred during processing
*/
protected IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
try {
reader.skip(start);
} catch (IOException e) {
return e;
}
IOException exception = null;
long bytesToRead = end - start + 1;
char[] buffer = new char[INPUT_BUFFER_SIZE];
int len = buffer.length;
while ((bytesToRead > 0) && (len >= buffer.length)) {
try {
len = reader.read(buffer);
if (bytesToRead >= len) {
writer.write(buffer, 0, len);
bytesToRead -= len;
} else {
writer.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length)
break;
}
return exception;
}
public static abstract class Resource {
private final String name;
private final long contentLength;
private final long lastModified;
private final String contentType;
private final String lastModifiedHttp;
public Resource(String name, String contentType, long contentLength, long lastModified) {
this.name = name;
this.contentType = (contentType == null) ? guessContentType(name) : contentType;
this.contentLength = contentLength;
this.lastModified = lastModified;
//RFC 1123 date. eg. Tue, 20 May 2008 13:45:26 GMT and always in English
SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
lastModifiedHttp = fmt.format(new Date(lastModified));
}
// Copied from Struts FilterDispatcher
public static String guessContentType(String name) {
// NOT using the code provided activation.jar to avoid adding yet another dependency
// this is generally OK, since these are the main files we server up
if (name.endsWith(".js")) {
return "text/javascript";
} else if (name.endsWith(".css")) {
return "text/css";
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
} else if (name.endsWith(".png")) {
return "image/png";
} else if (name.endsWith(".gif")) {
return "image/gif";
} else if (name.endsWith(".html")) {
return "text/html";
} else if (name.endsWith(".txt")) {
return "text/plain";
} else {
return null;
}
}
public String getContentType() {
return contentType;
}
public long getContentLength() {
return contentLength;
}
public abstract InputStream streamContent() throws IOException;
public abstract byte[] getContent();
public long getLastModified() {
return lastModified;
}
public String getLastModifiedHttp() {
return lastModifiedHttp;
}
public String toString() {
return "Resource[name=" + name +
", contentType=" + contentType +
", contentLength=" + contentLength +
",lastModified=" + lastModified +
"(" + lastModifiedHttp + ")]";
}
}
public static class FileResource extends Resource {
private final File file;
public FileResource(File file) {
super(file.getName(), guessContentType(file.getName()), file.length(), file.lastModified());
this.file = file;
}
public InputStream streamContent() throws IOException {
return new FileInputStream(file);
}
public byte[] getContent() {
return null;
}
}
public static class URLResource extends Resource {
private final URL url;
public URLResource(URL url) throws IOException {
this(url, url.openConnection());
}
private URLResource(URL url, URLConnection con) {
super(url.toString(), urlContentType(url, con), con.getContentLength(), con.getLastModified());
this.url = url;
}
private static String urlContentType(URL url, URLConnection con) {
//XXX: guess first and then look in con
String contentType = guessContentType(url.toString());
if (contentType == null)
contentType = con.getContentType();
return contentType;
}
public InputStream streamContent() throws IOException {
return url.openStream();
}
public byte[] getContent() {
return null;
}
}
protected static class Range {
public long start;
public long end;
public long length;
/**
* Validate range.
*
* @return true if this is a valid range
*/
public boolean validate() {
if (end >= length)
end = length - 1;
return ((start >= 0) && (end >= 0) && (start <= end) && (length > 0));
}
public void recycle() {
start = 0;
end = 0;
length = 0;
}
}
}