/*
* (C) Copyright 2013 Kurento (http://kurento.org/)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package com.kurento.kmf.repository.internal.http;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static javax.servlet.http.HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.kurento.kmf.common.exception.KurentoException;
import com.kurento.kmf.repository.RepositoryApiConfiguration;
import com.kurento.kmf.repository.RepositoryItem;
import com.kurento.kmf.repository.RepositoryItemAttributes;
import com.kurento.kmf.repository.internal.RepositoryHttpEndpointImpl;
import com.kurento.kmf.spring.KurentoApplicationContextUtils;
@WebServlet(value = "/repository_servlet/*", loadOnStartup = 1)
public class RepositoryHttpServlet extends HttpServlet {
protected static class Range {
public long start;
public long end;
public long length;
/**
* Validate range.
*
*/
public boolean validate() {
if (length != -1 && end >= length) {
end = length - 1;
}
return (start >= 0) && (end >= 0) && (start <= end)
&& (length == -1 || length > 0);
}
}
private static Logger log = LoggerFactory
.getLogger(RepositoryHttpServlet.class);
private static final long serialVersionUID = 1L;
/**
* Full range constant.
*/
protected static final List<Range> FULL = new ArrayList<>();
/**
* MIME multipart separation string
*/
protected static final String MIME_SEPARATION = "KURENTO_MIME_BOUNDARY";
/**
* Size of file transfer buffer in bytes.
*/
protected static final int FILE_BUFFER_SIZE = 4096;
/**
* The input buffer size to use when serving resources.
*/
private static final int INPUT_BUFFER_SIZE = 2048;
/**
* The output buffer size to use when serving resources.
*/
private static final int OUTPUT_BUFFER_SIZE = 2048;
/**
* The debugging detail level for this servlet.
*/
protected int debug;
/**
* RepoItemHttpElems
*/
@Autowired
protected transient RepositoryHttpManager repoHttpManager;
@Autowired
private RepositoryApiConfiguration config;
/**
* Finalize this servlet.
*/
@Override
public void destroy() {
// NOOP
}
/**
* Initialize this servlet.
*/
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
configureKurentoAppContext(servletConfig);
configureServletMapping(servletConfig);
configureWebappPublicURL(servletConfig);
if (servletConfig.getInitParameter("debug") != null) {
debug = Integer.parseInt(getServletConfig().getInitParameter(
"debug"));
}
}
private String configureWebappPublicURL(ServletConfig servletConfig) {
String webappURL = config.getWebappPublicURL();
if (webappURL == null || webappURL.trim().isEmpty()) {
webappURL = servletConfig.getServletContext().getContextPath();
} else {
if (webappURL.endsWith("/")) {
webappURL = webappURL.substring(0, webappURL.length() - 1);
}
}
repoHttpManager.setWebappPublicURL(webappURL);
return webappURL;
}
private String configureServletMapping(ServletConfig servletConfig) {
Collection<String> mappings = servletConfig.getServletContext()
.getServletRegistration(servletConfig.getServletName())
.getMappings();
if (mappings.isEmpty()) {
throw new KurentoException("There is no mapping for servlet "
+ RepositoryHttpServlet.class.getName());
}
String mapping = mappings.iterator().next();
// TODO: Document this. We assume a mapping starting with / and ending
// with /*
mapping = mapping.substring(0, mapping.length() - 1);
repoHttpManager.setServletPath(mapping);
return mapping;
}
private void configureKurentoAppContext(ServletConfig servletConfig) {
if (KurentoApplicationContextUtils.getKurentoApplicationContext() == null) {
KurentoApplicationContextUtils
.createKurentoApplicationContext(servletConfig
.getServletContext());
}
KurentoApplicationContextUtils
.processInjectionBasedOnKurentoApplicationContext(this);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
logRequest(req);
super.service(req, resp);
logResponse(resp);
}
/**
* Override default implementation to ensure that TRACE is correctly
* handled.
*
* @param req
* the {@link HttpServletRequest} object that contains the
* request the client made of the servlet
*
* @param resp
* the {@link HttpServletResponse} object that contains the
* response the servlet returns to the client
*
* @exception IOException
* if an input or output error occurs while the servlet is
* handling the OPTIONS request
*
* @exception ServletException
* if the request for the OPTIONS cannot be handled
*/
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setHeader("Allow", "GET, HEAD, POST, PUT, OPTIONS");
}
/**
* Process a HEAD request for the specified resource.
*
* @param request
* The servlet request we are processing
* @param response
* The servlet response we are creating
*
* @exception IOException
* if an input/output error occurs
* @exception ServletException
* if a servlet-specified error occurs
*/
@Override
protected void doHead(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
// Serve the requested resource, without the data content
serveResource(request, response, false);
}
/**
* Process a POST request for the specified resource.
*
* @param request
* The servlet request we are processing
* @param response
* The servlet response we are creating
*
* @exception IOException
* if an input/output error occurs
* @exception ServletException
* if a servlet-specified error occurs
*/
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
doPut(request, response);
}
/**
* Process a PUT request for the specified resource.
*
* @param req
* The servlet request we are processing
* @param resp
* The servlet response we are creating
*
* @exception IOException
* if an input/output error occurs
* @exception ServletException
* if a servlet-specified error occurs
*/
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
uploadContent(req, resp);
}
/**
* Process a GET request for the specified resource.
*
* @param request
* The servlet request we are processing
* @param response
* The servlet response we are creating
*
* @exception IOException
* if an input/output error occurs
* @exception ServletException
* if a servlet-specified error occurs
*/
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
serveResource(request, response, true);
}
protected void uploadContent(HttpServletRequest req,
HttpServletResponse resp) throws IOException {
String sessionId = extractSessionId(req);
RepositoryHttpEndpointImpl elem = repoHttpManager
.getHttpRepoItemElem(sessionId);
if (elem == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
elem.stopCurrentTimer();
elem.fireStartedEventIfFirstTime();
try (InputStream requestInputStream = req.getInputStream()) {
try (OutputStream repoItemOutputStream = elem
.getRepoItemOutputStream()) {
Range range = parseContentRange(req, resp);
if (range != null) {
if (range.start > elem.getWrittenBytes()) {
resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
resp.getOutputStream().println(
"The server doesn't support writing ranges "
+ "ahead of previously written bytes");
} else if (range.end == elem.getWrittenBytes()) {
// TODO We assume that the put range is the same than
// the
// previous one. Do we need to check this?
resp.setStatus(SC_OK);
resp.getOutputStream()
.println(
"The server has detected that the submited range "
+ "has already submited in a previous request");
} else if (range.start < elem.getWrittenBytes()
&& range.end > elem.getWrittenBytes()) {
Range copyRange = new Range();
copyRange.start = elem.getWrittenBytes() - range.start;
copyRange.end = range.end - range.start;
copyStreamsRange(requestInputStream,
repoItemOutputStream, copyRange);
resp.setStatus(SC_OK);
} else if (range.start == elem.getWrittenBytes()) {
IOUtils.copy(requestInputStream, repoItemOutputStream);
resp.setStatus(SC_OK);
}
} else {
boolean isMultipart = ServletFileUpload
.isMultipartContent(req);
if (isMultipart) {
uploadMultipart(req, resp, repoItemOutputStream);
} else {
try {
log.info("Start to receive bytes (estimated "
+ req.getContentLength() + " bytes)");
int bytes = IOUtils.copy(requestInputStream,
repoItemOutputStream);
resp.setStatus(SC_OK);
log.info("Bytes received: " + bytes);
} catch (Exception e) {
log.warn("Exception when uploading content", e);
elem.fireSessionErrorEvent(e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
}
} finally {
elem.stopInTimeout();
}
}
private void uploadMultipart(HttpServletRequest req,
HttpServletResponse resp, OutputStream repoItemOutputStrem)
throws IOException {
log.info("Multipart detected");
ServletFileUpload upload = new ServletFileUpload();
try {
// Parse the request
FileItemIterator iter = upload.getItemIterator(req);
while (iter.hasNext()) {
FileItemStream item = iter.next();
String name = item.getFieldName();
try (InputStream stream = item.openStream()) {
if (item.isFormField()) {
// TODO What to do with this?
log.info("Form field {} with value {} detected.", name,
Streams.asString(stream));
} else {
// TODO Must we support multiple files uploading?
log.info("File field {} with file name detected.",
name, item.getName());
log.info("Start to receive bytes (estimated bytes)",
Integer.toString(req.getContentLength()));
int bytes = IOUtils.copy(stream, repoItemOutputStrem);
resp.setStatus(SC_OK);
log.info("Bytes received: {}", Integer.toString(bytes));
}
}
}
} catch (FileUploadException e) {
throw new IOException(e);
}
}
private void logRequest(HttpServletRequest req) {
log.info("Request received " + req.getRequestURL());
log.info(" Method: " + req.getMethod());
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
Enumeration<String> values = req.getHeaders(headerName);
List<String> valueList = new ArrayList<>();
while (values.hasMoreElements()) {
valueList.add(values.nextElement());
}
log.info(" Header {}: {}", headerName, valueList);
}
}
private void logResponse(HttpServletResponse resp) {
Collection<String> headerNames = resp.getHeaderNames();
for (String headerName : headerNames) {
Collection<String> values = resp.getHeaders(headerName);
log.info(" Header {}: {}", headerName, values);
}
}
/**
* Return the sessionId from the request.
*
* @param request
* The servlet request we are processing
*/
protected String extractSessionId(HttpServletRequest request) {
// Path info without leading "/"
String pathInfo = request.getPathInfo();
if (pathInfo != null && pathInfo.length() >= 1) {
return pathInfo.substring(1);
}
return null;
}
/**
* Handle a partial PUT. New content specified in request is appended to
* existing content in oldRevisionContent (if present). This code does not
* support simultaneous partial updates to the same resource.
*/
protected File executePartialPut(HttpServletRequest req, Range range,
String sessionId) throws IOException {
// TODO: Change this implementation to avoid Files. Try to
// make the work on the repository implementation.
// Append data specified in ranges to existing content for this
// resource - create a temp. file on the local filesystem to
// perform this operation
// Assume just one range is specified for now
File tempDir = (File) getServletContext().getAttribute(
ServletContext.TEMPDIR);
// Convert all '/' characters to '.' in resourcePath
String convertedResourcePath = sessionId.replace('/', '.');
File contentFile = new File(tempDir, convertedResourcePath);
if (contentFile.createNewFile()) {
// Clean up contentFile when Tomcat is terminated
contentFile.deleteOnExit();
}
try (RandomAccessFile randAccessContentFile = new RandomAccessFile(
contentFile, "rw")) {
RepositoryHttpEndpointImpl repoItemHttpElem = repoHttpManager
.getHttpRepoItemElem(sessionId);
// Copy data in oldRevisionContent to contentFile
if (repoItemHttpElem != null) {
try (BufferedInputStream bufOldRevStream = new BufferedInputStream(
repoItemHttpElem.createRepoItemInputStream(),
FILE_BUFFER_SIZE)) {
int numBytesRead;
byte[] copyBuffer = new byte[FILE_BUFFER_SIZE];
while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
randAccessContentFile
.write(copyBuffer, 0, numBytesRead);
}
}
}
randAccessContentFile.setLength(range.length);
// Append data in request input stream to contentFile
randAccessContentFile.seek(range.start);
int numBytesRead;
byte[] transferBuffer = new byte[FILE_BUFFER_SIZE];
try (BufferedInputStream requestBufInStream = new BufferedInputStream(
req.getInputStream(), FILE_BUFFER_SIZE)) {
while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
randAccessContentFile
.write(transferBuffer, 0, numBytesRead);
}
}
}
return contentFile;
}
/**
* 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 resourceAttributes
* 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
*/
protected boolean checkIfHeaders(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) throws IOException {
// TODO Investigate how to load properties for RepositoryItem (Mongo or
// Filesystem)
return checkIfMatch(request, response, resourceAttributes)
&& checkIfModifiedSince(request, response, resourceAttributes)
&& checkIfNoneMatch(request, response, resourceAttributes)
&& checkIfUnmodifiedSince(request, response, resourceAttributes);
}
/**
* 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?
*
* @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) throws IOException,
ServletException {
boolean serveContent = content;
// Identify the requested resource path
String sessionId = extractSessionId(request);
RepositoryHttpEndpointImpl elem = repoHttpManager
.getHttpRepoItemElem(sessionId);
if (elem == null) {
if (debug > 0) {
log("Resource with sessionId '" + sessionId + "' not found");
}
response.sendError(SC_NOT_FOUND, request.getRequestURI());
return;
}
elem.fireStartedEventIfFirstTime();
RepositoryItem repositoryItem = elem.getRepositoryItem();
RepositoryItemAttributes attributes = repositoryItem.getAttributes();
if (debug > 0) {
if (serveContent) {
log("Serving resource with sessionId '"
+ sessionId
+ "' headers and data. This resource corresponds to repository item '"
+ repositoryItem.getId() + "'");
} else {
log("Serving resource with sessionId '"
+ sessionId
+ "' headers only. This resource corresponds to repository item '"
+ repositoryItem.getId() + "'");
}
}
boolean malformedRequest = response.getStatus() >= SC_BAD_REQUEST;
if (!malformedRequest && !checkIfHeaders(request, response, attributes)) {
return;
}
String contentType = getContentType(elem, attributes);
List<Range> ranges = null;
if (!malformedRequest) {
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("ETag", attributes.getETag());
response.setHeader("Last-Modified",
attributes.getLastModifiedHttp());
ranges = parseRange(request, response, attributes);
}
long contentLength = attributes.getContentLength();
// Special case for zero length files, which would cause a
// (silent) ISE when setting the output buffer size
if (contentLength == 0L) {
serveContent = false;
}
// 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.
boolean contentWritten = response.isCommitted();
if (contentWritten) {
ranges = FULL;
}
boolean noRanges = (ranges == null || ranges.isEmpty());
if (malformedRequest
|| (noRanges && request.getHeader("Range") == null)
|| ranges == FULL) {
setContentType(response, contentType);
if (contentLength >= 0) {
// Don't set a content length if something else has already
// written to the response.
if (!contentWritten) {
setContentLength(response, contentLength);
}
}
// Copy the input stream to our output stream (if requested)
if (serveContent) {
copy(elem, response);
}
} else {
if (noRanges) {
return;
}
// Partial content response.
response.setStatus(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;
setContentLength(response, length);
setContentType(response, contentType);
if (serveContent) {
copy(elem, response, range);
}
} else {
response.setContentType("multipart/byteranges; boundary="
+ MIME_SEPARATION);
if (serveContent) {
copy(elem, response, ranges, contentType);
}
}
}
elem.stopInTimeout();
}
private String getContentType(RepositoryHttpEndpointImpl repoItemHttpElem,
RepositoryItemAttributes attributes) {
String contentType = attributes.getMimeType();
if (contentType == null) {
contentType = getServletContext().getMimeType(
repoItemHttpElem.getRepositoryItem().getId());
attributes.setMimeType(contentType);
}
return contentType;
}
private void setContentType(HttpServletResponse response, String contentType) {
if (contentType != null) {
if (debug > 0) {
log("contentType='" + contentType + "'");
}
response.setContentType(contentType);
}
}
private void setContentLength(HttpServletResponse response, long length) {
if (debug > 0) {
log("contentLength=" + length);
}
if (length < Integer.MAX_VALUE) {
response.setContentLength((int) length);
} else {
// Set the content-length as String to be able to use a long
response.setHeader("content-length", "" + length);
}
}
/**
* Parse the content-range header.
*
* @param request
* The servlet request we are 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(SC_BAD_REQUEST);
return null;
}
rangeHeader = rangeHeader.substring(6).trim();
int dashPos = rangeHeader.indexOf('-');
int slashPos = rangeHeader.indexOf('/');
if (dashPos == -1) {
response.sendError(SC_BAD_REQUEST);
return null;
}
if (slashPos == -1) {
response.sendError(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));
String lengthString = rangeHeader.substring(slashPos + 1,
rangeHeader.length());
if (lengthString.equals("*")) {
range.length = -1;
} else {
range.length = Long.parseLong(lengthString);
}
} catch (NumberFormatException e) {
response.sendError(SC_BAD_REQUEST);
return null;
}
if (!range.validate()) {
response.sendError(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
* @return Vector of ranges
*/
protected List<Range> parseRange(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) 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 = resourceAttributes.getETag();
long lastModified = resourceAttributes.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 = resourceAttributes.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(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
rangeHeader = rangeHeader.substring(6);
// Vector which will contain all the ranges which are successfully
// parsed.
List<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(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(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(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
}
if (!currentRange.validate()) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
result.add(currentRange);
}
return result;
}
/**
* 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 resourceAttributes
* 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
*/
protected boolean checkIfMatch(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) throws IOException {
String eTag = resourceAttributes.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(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 resourceAttributes
* 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
*/
protected boolean checkIfModifiedSince(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) {
try {
long headerValue = request.getDateHeader("If-Modified-Since");
long lastModified = resourceAttributes.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(SC_NOT_MODIFIED);
response.setHeader("ETag", resourceAttributes.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 resourceAttributes
* 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
*/
protected boolean checkIfNoneMatch(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) throws IOException {
String eTag = resourceAttributes.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(SC_NOT_MODIFIED);
response.setHeader("ETag", eTag);
return false;
}
response.sendError(SC_PRECONDITION_FAILED);
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 resourceAttributes
* 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
*/
protected boolean checkIfUnmodifiedSince(HttpServletRequest request,
HttpServletResponse response,
RepositoryItemAttributes resourceAttributes) throws IOException {
try {
long lastModified = resourceAttributes.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(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 repoItemHttpElem
* The cache entry for the source resource
* @param response
* The HttpResponse where the resource will be copied
*
* @exception IOException
* if an input/output error occurs
*/
protected void copy(RepositoryHttpEndpointImpl repoItemHttpElem,
HttpServletResponse response) throws IOException {
copy(repoItemHttpElem, response, 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 repoItemHttpElem
* The cache entry for the source resource
* @param response
* The response we are writing to
* @param range
* Range asked by the client
* @exception IOException
* if an input/output error occurs
*/
protected void copy(RepositoryHttpEndpointImpl repoItemHttpElem,
HttpServletResponse response, Range range) throws IOException {
try {
response.setBufferSize(OUTPUT_BUFFER_SIZE);
} catch (IllegalStateException e) {
// Silent catch
}
IOException exception;
try (ServletOutputStream ostream = response.getOutputStream()) {
try (InputStream istream = new BufferedInputStream(
repoItemHttpElem.createRepoItemInputStream(),
INPUT_BUFFER_SIZE)) {
if (range != null) {
exception = copyStreamsRange(istream, ostream, range);
} else {
exception = copyStreams(istream, ostream);
}
}
}
// 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 repoItemHttpElem
* The cache entry for the source resource
* @param response
* The response we are writing 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(RepositoryHttpEndpointImpl repoItemHttpElem,
HttpServletResponse response, List<Range> ranges, String contentType)
throws IOException {
try {
response.setBufferSize(OUTPUT_BUFFER_SIZE);
} catch (IllegalStateException e) {
// Silent catch
}
IOException exception = null;
try (ServletOutputStream ostream = response.getOutputStream()) {
for (Range currentRange : ranges) {
try (InputStream istream = new BufferedInputStream(
repoItemHttpElem.createRepoItemInputStream(),
INPUT_BUFFER_SIZE)) {
// 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();
exception = copyStreamsRange(istream, ostream, currentRange);
if (exception != null) {
break;
}
}
}
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 istream
* The input stream to read from
* @param ostream
* The output stream to write to
* @return Exception which occurred during processing
*/
protected IOException copyStreams(InputStream istream, OutputStream ostream) {
// Copy the input stream to the output stream
IOException exception = null;
byte buffer[] = new byte[INPUT_BUFFER_SIZE];
int len = buffer.length;
while (true) {
try {
len = istream.read(buffer);
if (len == -1) {
break;
}
ostream.write(buffer, 0, len);
log.debug("{} bytes have been written to item" + 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 range
* Range we are copying
*
* @return Exception which occurred during processing
*/
protected IOException copyStreamsRange(InputStream istream,
OutputStream ostream, Range range) {
long start = range.start;
long end = range.end;
if (debug > 10) {
log("Serving bytes:" + start + "-" + end);
}
long skipped = 0;
try {
skipped = istream.skip(start);
} catch (IOException e) {
return e;
}
if (skipped < start) {
return new IOException("Has been skiped " + skipped + " when "
+ start + " is required");
}
IOException exception = null;
long remBytes = end - start + 1;
byte buffer[] = new byte[INPUT_BUFFER_SIZE];
int readBytes = buffer.length;
while (remBytes > 0) {
try {
readBytes = istream.read(buffer);
if (readBytes == -1) {
break;
} else if (readBytes <= remBytes) {
ostream.write(buffer, 0, readBytes);
remBytes -= readBytes;
} else {
ostream.write(buffer, 0, (int) remBytes);
break;
}
} catch (IOException e) {
exception = e;
break;
}
}
return exception;
}
}