/*
* $Header: /home/cvs/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/GetMethod.java,v 1.40.2.3 2004/02/09 07:27:51 ozeigermann Exp $
* $Revision: 1.40.2.3 $
* $Date: 2004/02/09 07:27:51 $
*
* ====================================================================
*
* Copyright 1999-2002 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.slide.webdav.method;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.RevisionContentNotFoundException;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.content.RevisionNotFoundException;
import org.apache.slide.structure.LinkedObjectNotFoundException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.util.Configuration;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.util.DeltavConstants;
import org.apache.slide.webdav.util.LabeledRevisionNotFoundException;
import org.apache.slide.webdav.util.PreconditionViolationException;
import org.apache.slide.webdav.util.VersioningHelper;
import org.apache.slide.webdav.util.ViolatedPrecondition;
import org.apache.slide.webdav.util.WebdavUtils;
import org.apache.util.WebdavStatus;
/**
* GET method.
*
* @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
*/
public class GetMethod extends AbstractWebdavMethod {
// -------------------------------------------------------------- Constants
protected final int BUFFER_SIZE = 2048;
/**
* The set of SimpleDateFormat formats to use in getDateHeader().
*/
protected static final SimpleDateFormat formats[] = {
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
};
/**
* MIME multipart separation string
*/
protected static final String mimeSeparation = "SLIDE_MIME_BOUNDARY";
/**
* The input buffer size to use when serving resources.
*/
protected int input = 2048;
/**
* The output buffer size to use when serving resources.
*/
protected int output = 2048;
/**
* Print content.
*/
protected boolean printContent = true;
/**
* The VersioningHelper used by this instance.
*/
protected VersioningHelper vHelp = null;
// ----------------------------------------------------- Instance Variables
/**
* Resource to be retrieved.
*/
protected String resourcePath;
// ----------------------------------------------------------- Constructors
/**
* Constructor.
*
* @param token the token for accessing the namespace
* @param config configuration of the WebDAV servlet
*/
public GetMethod(NamespaceAccessToken token, WebdavServletConfig config) {
super(token, config);
}
// ------------------------------------------------------ Protected Methods
/**
* Parse XML request.
*/
protected void parseRequest()
throws WebdavException {
vHelp = VersioningHelper.getVersioningHelper(
slideToken, token, req, resp, getConfig() );
resourcePath = requestUri;
if (resourcePath == null) {
resourcePath = "/";
}
// evaluate "Label" header
if (Configuration.useVersionControl()) {
try {
String labelHeader = WebdavUtils.fixTomcatHeader(requestHeaders.getLabel(), "UTF-8");
resourcePath = vHelp.getLabeledResourceUri(resourcePath, labelHeader);
}
catch (LabeledRevisionNotFoundException e) {
ViolatedPrecondition violatedPrecondition =
new ViolatedPrecondition(DeltavConstants.C_MUST_SELECT_VERSION_IN_HISTORY,
WebdavStatus.SC_CONFLICT);
try {
sendPreconditionViolation(new PreconditionViolationException(violatedPrecondition,
resourcePath));
} catch (IOException ioe) {}
throw new WebdavException( WebdavStatus.SC_CONFLICT );
}
catch (SlideException e) {
int statusCode = getErrorCode( (Exception)e );
sendError( statusCode, e );
throw new WebdavException( statusCode );
}
}
}
/**
* Execute request.
*
* @exception WebdavException Can't access resource
*/
protected void executeRequest()
throws WebdavException {
// check lock-null resources
try {
if (isLockNull(resourcePath)) {
int statusCode = WebdavStatus.SC_NOT_FOUND;
sendError( statusCode, "lock-null resource", new Object[]{resourcePath} );
throw new WebdavException( statusCode );
}
}
catch (ServiceAccessException e) {
int statusCode = getErrorCode((Exception)e);
sendError( statusCode, e );
throw new WebdavException( statusCode );
}
try {
// Then we must get object contents ...
ObjectNode object = structure.retrieve(slideToken, resourcePath);
NodeRevisionDescriptors revisionDescriptors =
content.retrieve(slideToken, resourcePath);
if (revisionDescriptors.hasRevisions()) {
// Retrieve latest revision descriptor
NodeRevisionDescriptor revisionDescriptor =
content.retrieve(slideToken, revisionDescriptors);
if (revisionDescriptor != null) {
ResourceInfo resourceInfo =
new ResourceInfo(resourcePath, revisionDescriptor);
// Checking If headers
if (!checkIfHeaders(req, resp, resourceInfo))
return;
ServletOutputStream os = resp.getOutputStream();
InputStream is = null;
if (printContent) {
is = content.retrieve
(slideToken, revisionDescriptors,
revisionDescriptor).streamContent();
}
Vector ranges = parseRange(req, resp, resourceInfo);
// ETag header
resp.setHeader("ETag", revisionDescriptor.getETag() );
resp.setHeader
("Content-Language", revisionDescriptor.getContentLanguage());
resp.addHeader
("Last-Modified",
revisionDescriptor.getLastModified().toString());
if ( ((ranges == null) || (ranges.isEmpty()))
&& (req.getHeader("Range") == null) ) {
resp.setContentType
(revisionDescriptor.getContentType());
resp.setContentLength
((int) revisionDescriptor.getContentLength());
// Copy the input stream to our output stream
// (if requested)
if (printContent) {
resp.setBufferSize(output);
copy(resourceInfo, is, os);
}
} else {
if ((ranges == null) || (ranges.isEmpty()))
return;
// Partial content response.
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (ranges.size() == 1) {
Range range = (Range) ranges.elementAt(0);
resp.addHeader("Content-Range", "bytes "
+ range.start
+ "-" + range.end + "/"
+ range.fileLength);
resp.setContentLength((int) range.length);
resp.setContentType
(revisionDescriptor.getContentType());
if (printContent) {
resp.setBufferSize(output);
copy(resourceInfo, is, os, range);
}
} else {
resp.setContentType
("multipart/byteranges; boundary="
+ mimeSeparation);
if (printContent) {
resp.setBufferSize(output);
copy(resourceInfo, is, os,
ranges.elements(),
revisionDescriptor.getContentType());
}
}
}
} else {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
} else {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
} catch (Exception e) {
int statusCode = getErrorCode( e );
sendError( statusCode, e );
throw new WebdavException( statusCode );
}
}
/**
* Get return status based on exception type.
*/
protected int getErrorCode(Exception ex) {
try {
throw ex;
} catch (RevisionNotFoundException e) {
return WebdavStatus.SC_NOT_FOUND;
} catch (RevisionContentNotFoundException e) {
return WebdavStatus.SC_NOT_FOUND;
} catch (RevisionDescriptorNotFoundException e) {
return WebdavStatus.SC_NOT_FOUND;
} catch (LinkedObjectNotFoundException e) {
return WebdavStatus.SC_NOT_FOUND;
} catch (Exception e) {
return super.getErrorCode(e);
}
}
// -------------------------------------------------------- Private Methods
/**
* 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 resourceInfo File object
* @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
*/
private boolean checkIfHeaders(HttpServletRequest request,
HttpServletResponse response,
ResourceInfo resourceInfo)
throws IOException {
String eTag = getETag(resourceInfo, true);
long fileLength = resourceInfo.length;
long lastModified = resourceInfo.date;
StringTokenizer commaTokenizer;
String headerValue;
// Checking If-Match
headerValue = request.getHeader("If-Match");
if (headerValue != null) {
if (headerValue.indexOf("*") == -1) {
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;
}
}
}
// Checking If-Modified-Since
headerValue = request.getHeader("If-Modified-Since");
if (headerValue != null) {
// If an If-None-Match header has been specified, if modified since
// is ignored.
if (request.getHeader("If-None-Match") == null) {
Date date = null;
// Parsing the HTTP Date
for (int i = 0; (date == null) && (i < formats.length); i++) {
try {
synchronized (formats[i]) {
date = formats[i].parse(headerValue);
}
} catch (ParseException e) {
;
}
}
if ((date != null)
&& (lastModified <= (date.getTime() + 1000)) ) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
response.sendError
(HttpServletResponse.SC_NOT_MODIFIED);
return false;
}
}
}
// Checking If-None-Match
headerValue = request.getHeader("If-None-Match");
if (headerValue != null) {
if (headerValue.indexOf("*") == -1) {
commaTokenizer = new StringTokenizer(headerValue, ",");
boolean conditionSatisfied = false;
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag))
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.sendError
(HttpServletResponse.SC_NOT_MODIFIED);
return false;
} else {
response.sendError
(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
} else {
if (resourceInfo.exists()) {
}
}
}
// Checking If-Unmodified-Since
headerValue = request.getHeader("If-Unmodified-Since");
if (headerValue != null) {
Date date = null;
// Parsing the HTTP Date
for (int i = 0; (date == null) && (i < formats.length); i++) {
try {
synchronized (formats[i]) {
date = formats[i].parse(headerValue);
}
} catch (ParseException e) {
;
}
}
if ( (date != null) && (lastModified > date.getTime()) ) {
// 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;
}
}
return true;
}
/**
* Get the ETag value associated with a file.
*
* @param resourceInfo File object
* @param strong True if we want a strong ETag, in which case a checksum
* of the file has to be calculated
*/
private String getETagValue(ResourceInfo resourceInfo, boolean strong) {
// FIXME : Compute a strong ETag if requested, using an MD5 digest
// of the file contents
return resourceInfo.length + "-" + resourceInfo.date;
}
/**
* Get the ETag associated with a file.
*
* @param resourceInfo File object
* @param strong True if we want a strong ETag, in which case a checksum
* of the file has to be calculated
*/
private String getETag(ResourceInfo resourceInfo, boolean strong) {
if (strong)
return "\"" + getETagValue(resourceInfo, strong) + "\"";
else
return "W/\"" + getETagValue(resourceInfo, strong) + "\"";
}
/**
* 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
*
* @exception IOException if an input/output error occurs
*/
private void copy(ResourceInfo resourceInfo,
InputStream resourceInputStream,
ServletOutputStream ostream)
throws IOException {
IOException exception = null;
InputStream istream = new BufferedInputStream
(resourceInputStream, input);
// Copy the input stream to the output stream
exception = copyRange(istream, ostream);
// Clean up the input and output streams
try {
istream.close();
} catch (Throwable t) {
;
}
try {
ostream.flush();
} catch (Throwable t) {
;
}
try {
ostream.close();
} catch (Throwable t) {
;
}
// 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 resourceInfo The ResourceInfo object
* @param ostream The output stream to write to
* @param range Range the client wanted to retrieve
* @exception IOException if an input/output error occurs
*/
private void copy(ResourceInfo resourceInfo,
InputStream resourceInputStream,
ServletOutputStream ostream,
Range range)
throws IOException {
IOException exception = null;
InputStream istream =
new BufferedInputStream(resourceInputStream, input);
exception = copyRange(istream, ostream, range.start, range.end);
// Clean up the input and output streams
try {
istream.close();
} catch (Throwable t) {
;
}
try {
ostream.flush();
} catch (Throwable t) {
;
}
try {
ostream.close();
} catch (Throwable t) {
;
}
// 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 resourceInfo The ResourceInfo object
* @param ostream The output stream to write to
* @param ranges Enumeration of the ranges the client wanted to retrieve
* @param contentType Content type of the resource
* @exception IOException if an input/output error occurs
*/
private void copy(ResourceInfo resourceInfo,
InputStream resourceInputStream,
ServletOutputStream ostream,
Enumeration ranges, String contentType)
throws IOException {
IOException exception = null;
while ( (exception == null) && (ranges.hasMoreElements()) ) {
InputStream istream =
new BufferedInputStream(resourceInputStream, input);
Range currentRange = (Range) ranges.nextElement();
// Writing MIME header.
ostream.println("--" + mimeSeparation);
if (contentType != null)
ostream.println("Content-Type: " + contentType);
ostream.println("Content-Range: bytes " + currentRange.start
+ "-" + currentRange.end + "/"
+ currentRange.fileLength);
ostream.println();
// Printing content
exception = copyRange(istream, ostream, currentRange.start,
currentRange.end);
try {
istream.close();
} catch (Throwable t) {
;
}
}
ostream.print("--" + mimeSeparation + "--");
// Clean up the output streams
try {
ostream.flush();
} catch (Throwable t) {
;
}
try {
ostream.close();
} catch (Throwable t) {
;
}
// 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 occured during processing
*/
private 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 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 occured during processing
*/
private IOException copyRange(InputStream istream,
ServletOutputStream ostream,
long start, long end) {
try {
istream.skip(start);
} catch (IOException e) {
return e;
}
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;
}
/**
* Parse the range header.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @return Vector of ranges
*/
private Vector parseRange(HttpServletRequest request,
HttpServletResponse response,
ResourceInfo resourceInfo)
throws IOException {
// Checking If-Range
String headerValue = request.getHeader("If-Range");
if (headerValue != null) {
String eTag = getETag(resourceInfo, true);
long lastModified = resourceInfo.date;
Date date = null;
// Parsing the HTTP Date
for (int i = 0; (date == null) && (i < formats.length); i++) {
try {
synchronized (formats[i]) {
date = formats[i].parse(headerValue);
}
} catch (ParseException e) {
;
}
}
if (date == null) {
// If the ETag the client gave does not match the entity
// etag, then the entire entity is returned.
if (!eTag.equals(headerValue.trim()))
return null;
} 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 > (date.getTime() + 1000))
return null;
}
}
long fileLength = resourceInfo.length;
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.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
rangeHeader = rangeHeader.substring(6);
// Vector which will contain all the ranges which are successfully
// parsed.
Vector result = new Vector();
StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
// Parsing the range list
while (commaTokenizer.hasMoreTokens()) {
String rangeDefinition = commaTokenizer.nextToken();
Range currentRange = new Range();
currentRange.fileLength = fileLength;
int dashPos = rangeDefinition.indexOf('-');
if (dashPos == -1) {
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.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.sendError
(HttpServletResponse
.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
}
currentRange.length = (currentRange.end - currentRange.start + 1);
if (!currentRange.validate()) {
response.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
result.addElement(currentRange);
}
return result;
}
// ------------------------------------------------------ Range Inner Class
private class Range {
public long start;
public long end;
public long length;
public long fileLength;
/**
* Validate range.
*/
public boolean validate() {
return ( (start >= 0) && (end >= 0) && (length > 0)
&& (start <= end) && (end < fileLength) && (fileLength >= length));
}
}
// ---------------------------------------------- ResourceInfo Inner Class
private class ResourceInfo {
/**
* Constructor.
*
* @param pathname Path name of the file
*/
public ResourceInfo(String path, NodeRevisionDescriptor properties) {
this.path = path;
this.exists = true;
this.creationDate = properties.getCreationDateAsDate().getTime();
this.date = properties.getLastModifiedAsDate().getTime();
this.httpDate = properties.getLastModified();
this.length = properties.getContentLength();
}
public String path;
public long creationDate;
public String httpDate;
public long date;
public long length;
//public boolean collection;
public boolean exists;
/**
* Test if the associated resource exists.
*/
public boolean exists() {
return exists;
}
/**
* String representation.
*/
public String toString() {
return path;
}
}
}