/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.olat.core.commons.services.webdav.servlets;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.servlets.URLEncoder;
/**
* <p>The default resource-serving servlet for most web applications,
* used to serve static resources such as HTML pages and images.
* </p>
* <p>
* This servlet is intended to be mapped to <em>/</em> e.g.:
* </p>
* <pre>
* <servlet-mapping>
* <servlet-name>default</servlet-name>
* <url-pattern>/</url-pattern>
* </servlet-mapping>
* </pre>
* <p>It can be mapped to sub-paths, however in all cases resources are served
* from the web appplication resource root using the full path from the root
* of the web application context.
* <br/>e.g. given a web application structure:
*</p>
* <pre>
* /context
* /images
* tomcat2.jpg
* /static
* /images
* tomcat.jpg
* </pre>
* <p>
* ... and a servlet mapping that maps only <code>/static/*</code> to the default servlet:
* </p>
* <pre>
* <servlet-mapping>
* <servlet-name>default</servlet-name>
* <url-pattern>/static/*</url-pattern>
* </servlet-mapping>
* </pre>
* <p>
* Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed
* while a request to <code>/context/images/tomcat2.jpg</code> will fail.
* </p>
* @author Craig R. McClanahan
* @author Remy Maucherat
* @version $Id$
*/
public abstract class DefaultDispatcher implements Serializable {
private static final long serialVersionUID = 1L;
private static final OLog log = Tracing.createLoggerFor(DefaultDispatcher.class);
// ----------------------------------------------------- Instance Variables
/**
* The input buffer size to use when serving resources.
*/
private int input = 2048;
/**
* Should be serve gzip versions of files. By default, it's set to false.
*/
private boolean gzip = false;
/**
* The output buffer size to use when serving resources.
*/
private int output = 2048;
/**
* Array containing the safe characters set.
*/
private static final URLEncoder urlEncoder;
/**
* File encoding to be used when reading static files. If none is specified
* the platform default is used.
*/
protected String fileEncoding = null;
/**
* Should the Accept-Ranges: bytes header be send with static resources?
*/
private boolean useAcceptRanges = true;
/**
* Full range marker.
*/
private static final ArrayList<Range> FULL = new ArrayList<Range>();
// ----------------------------------------------------- Static Initializer
/**
* GMT timezone - all HTTP dates are on GMT
*/
static {
urlEncoder = new URLEncoder();
urlEncoder.addSafeCharacter('-');
urlEncoder.addSafeCharacter('_');
urlEncoder.addSafeCharacter('.');
urlEncoder.addSafeCharacter('*');
urlEncoder.addSafeCharacter('/');
}
/**
* MIME multipart separation string
*/
protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";
/**
* Size of file transfer buffer in bytes.
*/
protected static final int BUFFER_SIZE = 4096;
// --------------------------------------------------------- Public Methods
protected abstract WebResourceRoot getResources(HttpServletRequest req);
// ------------------------------------------------------ Protected Methods
/**
* Return the relative path associated with this servlet.
*
* @param request The servlet request we are processing
*/
protected String getRelativePath(HttpServletRequest request) {
// IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
// serves resources from the web app root with context rooted paths.
// i.e. it cannot be used to mount the web app root under a sub-path
// This method must construct a complete context rooted path, although
// subclasses can change this behaviour.
// Are we being processed by a RequestDispatcher.include()?
if (request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
String result = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (result == null) {
result = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
} else {
result = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH) + result;
}
if ((result == null) || (result.equals(""))) {
result = "/";
}
return (result);
}
// No, extract the desired path directly from the request
String result = request.getPathInfo();
if (result == null) {
result = request.getServletPath();
} else {
result = request.getServletPath() + result;
}
if ((result == null) || (result.equals(""))) {
result = "/";
}
return (result);
}
/**
* Determines the appropriate path to prepend resources with
* when generating directory listings. Depending on the behaviour of
* {@link #getRelativePath(HttpServletRequest)} this will change.
* @param request the request to determine the path for
* @return the prefix to apply to all resources in the listing.
*/
protected String getPathPrefix(final HttpServletRequest request) {
return request.getContextPath();
}
/**
* Check if the conditions specified in the optional If headers are
* satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource
* @return 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
*/
protected boolean checkIfHeaders(HttpServletRequest request,
HttpServletResponse response,
WebResource resource)
throws IOException {
return checkIfMatch(request, response, resource)
&& checkIfModifiedSince(request, response, resource)
&& checkIfNoneMatch(request, response, resource)
&& checkIfUnmodifiedSince(request, response, resource);
}
/**
* URL rewriter.
*
* @param path Path which has to be rewritten
*/
protected String rewriteUrl(String path) {
return urlEncoder.encode( path );
}
/**
* 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 encoding The encoding to use if it is necessary to access the
* source as characters rather than as bytes
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet-specified error occurs
*/
protected void serveResource(HttpServletRequest request,
HttpServletResponse response,
boolean content,
String encoding)
throws IOException, ServletException {
boolean serveContent = content;
boolean debug = log.isDebug();
// Identify the requested resource path
String path = getRelativePath(request);
if (debug) {
if (serveContent)
log.debug("DefaultServlet.serveResource: Serving resource '" +
path + "' headers and data");
else
log.debug("DefaultServlet.serveResource: Serving resource '" +
path + "' headers only");
}
WebResourceRoot resources = getResources(request);
WebResource resource = resources.getResource(path);
if (!resource.exists()) {
// Check if we're included so we can return the appropriate
// missing resource name in the error
String requestUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestUri == null) {
requestUri = request.getRequestURI();
} else {
// We're included
// SRV.9.3 says we must throw a FNFE
throw new FileNotFoundException(
"defaultServlet.missingResource");
}
response.sendError(HttpServletResponse.SC_NOT_FOUND,
requestUri);
return;
}
// If the resource is not a collection, and the resource path
// ends with "/" or "\", return NOT FOUND
if (resource.isFile() && path.endsWith("/") || path.endsWith("\\")) {
// Check if we're included so we can return the appropriate
// missing resource name in the error
String requestUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestUri == null) {
requestUri = request.getRequestURI();
}
response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
return;
}
boolean isError = response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;
boolean included = false;
// Check if the conditions specified in the optional If headers are
// satisfied.
if (resource.isFile()) {
// Checking If headers
included = (request.getAttribute(
RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
if (!included && !isError && !checkIfHeaders(request, response, resource)) {
return;
}
}
// Find content type.
String contentType = resource.getMimeType();
if (contentType == null) {
contentType = request.getServletContext().getMimeType(resource.getName());
resource.setMimeType(contentType);
}
// These need to reflect the original resource, not the potentially
// gzip'd version of the resource so get them now if they are going to
// be needed later
String eTag = null;
String lastModifiedHttp = null;
if (resource.isFile() && !isError) {
eTag = resource.getETag();
lastModifiedHttp = resource.getLastModifiedHttp();
}
// Serve a gzipped version of the file if present
boolean usingGzippedVersion = false;
if (gzip && !included && resource.isFile() && !path.endsWith(".gz")) {
WebResource gzipResource = resources.getResource(path + ".gz");
if (gzipResource.exists() && gzipResource.isFile()) {
Collection<String> varyHeaders = response.getHeaders("Vary");
boolean addRequired = true;
for (String varyHeader : varyHeaders) {
if ("*".equals(varyHeader) ||
"accept-encoding".equalsIgnoreCase(varyHeader)) {
addRequired = false;
break;
}
}
if (addRequired) {
response.addHeader("Vary", "accept-encoding");
}
if (checkIfGzip(request)) {
response.addHeader("Content-Encoding", "gzip");
resource = gzipResource;
usingGzippedVersion = true;
}
}
}
ArrayList<Range> ranges = null;
long contentLength = -1L;
if (resource.isDirectory()) {
contentType = "text/html;charset=UTF-8";
} else {
if (!isError) {
if (useAcceptRanges) {
// Accept ranges header
response.setHeader("Accept-Ranges", "bytes");
}
// Parse range specifier
ranges = parseRange(request, response, resource);
// ETag header
response.setHeader("ETag", eTag);
// Last-Modified header
response.setHeader("Last-Modified", lastModifiedHttp);
}
// Get content length
contentLength = resource.getContentLength();
// Special case for zero length files, which would cause a
// (silent) ISE when setting the output buffer size
if (contentLength == 0L) {
serveContent = false;
}
}
ServletOutputStream ostream = null;
PrintWriter writer = null;
if (serveContent) {
// Trying to retrieve the servlet output stream
try {
ostream = response.getOutputStream();
} catch (IllegalStateException e) {
// If it fails, we try to get a Writer instead if we're
// trying to serve a text file
if (!usingGzippedVersion &&
((contentType == null) ||
(contentType.startsWith("text")) ||
(contentType.endsWith("xml")) ||
(contentType.contains("/javascript")))
) {
writer = response.getWriter();
// Cannot reliably serve partial content with a Writer
ranges = FULL;
} else {
throw e;
}
}
}
// Check to see if a Filter, Valve of wrapper has written some content.
// If it has, disable range requests and setting of a content length
// since neither can be done reliably.
ServletResponse r = response;
long contentWritten = 0;
while (r instanceof ServletResponseWrapper) {
r = ((ServletResponseWrapper) r).getResponse();
}
if (contentWritten > 0) {
ranges = FULL;
}
if (resource.isDirectory() ||
isError ||
( (ranges == null || ranges.isEmpty())
&& request.getHeader("Range") == null ) ||
ranges == FULL ) {
// Set the appropriate output headers
if (contentType != null) {
if (debug)
log.debug("DefaultServlet.serveFile: contentType='" +
contentType + "'");
response.setContentType(contentType);
}
if (resource.isFile() && contentLength >= 0 &&
(!serveContent || ostream != null)) {
if (debug)
log.debug("DefaultServlet.serveFile: contentLength=" +
contentLength);
// Don't set a content length if something else has already
// written to the response.
if (contentWritten == 0) {
response.setContentLengthLong(contentLength);
}
}
InputStream renderResult = null;
if (resource.isDirectory()) {
if (serveContent) {
// Serve the directory browser
renderResult = null;//TODO tomcat render(getPathPrefix(request), resource);
}
}
// Copy the input stream to our output stream (if requested)
if (serveContent) {
resource.increaseDownloadCount();
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream == null) {
// Output via a writer so can't use sendfile or write
// content directly.
if (resource.isDirectory()) {
renderResult = null;//render(getPathPrefix(request), resource);
} else {
renderResult = resource.getInputStream();
}
copy(resource, renderResult, writer, encoding);
} else {
// Output is via an InputStream
if (resource.isDirectory()) {
renderResult = null;//render(getPathPrefix(request), resource);
} else {
renderResult = resource.getInputStream();
}
// If a stream was configured, it needs to be copied to
// the output (this method closes the stream)
if (renderResult != null) {
copy(renderResult, ostream);
}
}
}
} else {
//download counter
resource.increaseDownloadCount();
if ((ranges == null) || (ranges.isEmpty()))
return;
// Partial content response.
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;
response.setContentLengthLong(length);
if (contentType != null) {
if (debug)
log.debug("DefaultServlet.serveFile: contentType='" +
contentType + "'");
response.setContentType(contentType);
}
if (serveContent) {
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
copy(resource, ostream, range);
} else {
// we should not get here
throw new IllegalStateException();
}
}
} else {
response.setContentType("multipart/byteranges; boundary="
+ mimeSeparation);
if (serveContent) {
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
copy(resource, ostream, ranges.iterator(), contentType);
} else {
// we should not get here
throw new IllegalStateException();
}
}
}
}
}
/**
* Parse the content-range header.
*
* @param request The servlet request we a)re processing
* @param response The servlet response we are creating
* @return Range
*/
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
* @return Vector of ranges
*/
protected ArrayList<Range> parseRange(HttpServletRequest request,
HttpServletResponse response,
WebResource 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) {
// Ignore
}
String eTag = resource.getETag();
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<>();
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;
}
// -------------------------------------------------------- protected Methods
/**
* Check if the if-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfMatch(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
String eTag = resource.getETag();
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);
return false;
}
}
}
return true;
}
/**
* Check if the if-modified-since condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfModifiedSince(HttpServletRequest request,
HttpServletResponse response, WebResource resource) {
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", resource.getETag());
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
return true;
}
return true;
}
/**
* Check if the if-none-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfNoneMatch(HttpServletRequest request,
HttpServletResponse response, WebResource resource)
throws IOException {
String eTag = resource.getETag();
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", eTag);
return false;
}
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
return true;
}
/**
* Check if the user agent supports gzip encoding.
*
* @param request The servlet request we are processing
* @return boolean true if the user agent supports gzip encoding,
* and false if the user agent does not support gzip encoding
*/
protected boolean checkIfGzip(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders("Accept-Encoding");
while (headers.hasMoreElements()) {
String header = headers.nextElement();
if (header.indexOf("gzip") != -1) {
return true;
}
}
return false;
}
/**
* Check if the if-unmodified-since condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resource The resource
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
HttpServletResponse response, WebResource 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);
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 is The input stream to read the source resource from
* @param ostream The output stream to write to
*
* @exception IOException if an input/output error occurs
*/
private void copy(InputStream is,
ServletOutputStream ostream)
throws IOException {
IOException exception = null;
InputStream istream = new BufferedInputStream(is, input);
// Copy the input stream to the output stream
exception = copyRange(istream, ostream);
// Clean up the input stream
istream.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream, and ensure that both streams are closed before returning
* (even in the face of an exception).
*
* @param resource The source resource
* @param is The input stream to read the source resource from
* @param writer The writer to write to
* @param encoding The encoding to use when reading the source input stream
*
* @exception IOException if an input/output error occurs
*/
protected void copy(WebResource resource, InputStream is, PrintWriter writer,
String encoding) throws IOException {
IOException exception = null;
InputStream resourceInputStream = null;
if (resource.isFile()) {
resourceInputStream = resource.getInputStream();
} else {
resourceInputStream = is;
}
Reader reader;
if (encoding == null) {
reader = new InputStreamReader(resourceInputStream);
} else {
reader = new InputStreamReader(resourceInputStream, encoding);
}
// Copy the input stream to the output stream
exception = copyRange(reader, writer);
// Clean up the reader
reader.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream, and ensure that both streams are closed before returning
* (even in the face of an exception).
*
* @param resource The source resource
* @param ostream The output stream to write to
* @param range Range the client wanted to retrieve
* @exception IOException if an input/output error occurs
*/
protected void copy(WebResource resource, ServletOutputStream ostream,
Range range)
throws IOException {
IOException exception = null;
InputStream resourceInputStream = resource.getInputStream();
InputStream istream =
new BufferedInputStream(resourceInputStream, input);
exception = copyRange(istream, ostream, range.start, range.end);
// Clean up the input stream
istream.close();
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream, and ensure that both streams are closed before returning
* (even in the face of an exception).
*
* @param resource The source resource
* @param ostream The output stream to write to
* @param ranges Enumeration of the ranges the client wanted to
* retrieve
* @param contentType Content type of the resource
* @exception IOException if an input/output error occurs
*/
protected void copy(WebResource resource, ServletOutputStream ostream,
Iterator<Range> ranges, String contentType)
throws IOException {
IOException exception = null;
while ( (exception == null) && (ranges.hasNext()) ) {
InputStream resourceInputStream = resource.getInputStream();
try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {
Range currentRange = ranges.next();
// Writing MIME header.
ostream.println();
ostream.println("--" + mimeSeparation);
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(istream, ostream, currentRange.start,
currentRange.end);
}
}
ostream.println();
ostream.print("--" + mimeSeparation + "--");
// Rethrow any exception that has occurred
if (exception != null)
throw exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream, and ensure that both streams are closed before returning
* (even in the face of an exception).
*
* @param istream The input stream to read from
* @param ostream The output stream to write to
* @return Exception which occurred during processing
*/
protected IOException copyRange(InputStream istream,
ServletOutputStream ostream) {
// Copy the input stream to the output stream
IOException exception = null;
byte buffer[] = new byte[input];
int len = buffer.length;
while (true) {
try {
len = istream.read(buffer);
if (len == -1)
break;
ostream.write(buffer, 0, len);
} catch (IOException e) {
exception = e;
len = -1;
break;
}
}
return exception;
}
/**
* Copy the contents of the specified input stream to the specified
* output stream, and ensure that both streams are closed before returning
* (even in the face of an exception).
*
* @param reader The reader to read from
* @param writer The writer to write to
* @return Exception which occurred during processing
*/
protected IOException copyRange(Reader reader, PrintWriter writer) {
// Copy the input stream to the output stream
IOException exception = null;
char buffer[] = new char[input];
int len = buffer.length;
while (true) {
try {
len = reader.read(buffer);
if (len == -1)
break;
writer.write(buffer, 0, len);
} catch (IOException e) {
exception = e;
len = -1;
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.isDebug()) {
log.debug("Serving bytes:" + start + "-" + end);
}
long skipped = 0;
try {
skipped = istream.skip(start);
} catch (IOException e) {
return e;
}
if (skipped < start) {
return new IOException("defaultservlet.skipfail" +
Long.valueOf(skipped) + Long.valueOf(start));
}
IOException exception = null;
long bytesToRead = end - start + 1;
byte buffer[] = new byte[input];
int len = buffer.length;
while ( (bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length)
break;
}
return exception;
}
protected static class Range {
public long start;
public long end;
public long length;
/**
* Validate range.
*
* @return true if the range is valid, otherwise false
*/
public boolean validate() {
if (end >= length)
end = length - 1;
return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
}
}
}