/* * 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); } } }