/*
* $Header: /home/cvs/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/GetMethod.java,v 1.51.2.3 2004/11/09 20:30:50 ozeigermann Exp $
* $Revision: 1.51.2.3 $
* $Date: 2004/11/09 20:30:50 $
*
* ====================================================================
*
* 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.util.Date;
import java.util.Enumeration;
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.event.EventDispatcher;
import org.apache.slide.security.AccessDeniedException;
import org.apache.slide.structure.LinkedObjectNotFoundException;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.util.Configuration;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.event.WebdavEvent;
import org.apache.slide.webdav.util.DeltavConstants;
import org.apache.slide.webdav.util.DirectoryIndexGenerator;
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.WebdavStatus;
import org.apache.slide.webdav.util.WebdavUtils;
/**
* GET method.
*
*/
public class GetMethod extends AbstractWebdavMethod implements ReadMethod {
// -------------------------------------------------------------- Constants
protected final int BUFFER_SIZE = 2048;
/**
* 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
protected void displayDirectory() throws IOException {
String directoryBrowsing = config.getInitParameter( "directory-browsing" );
if( "true".equalsIgnoreCase(directoryBrowsing) ) {
try {
DirectoryIndexGenerator directoryIndexGenerator =
new DirectoryIndexGenerator
(token, config);
directoryIndexGenerator.generate(req, resp, slideToken);
} catch (AccessDeniedException e) {
resp.sendError(WebdavStatus.SC_FORBIDDEN);
} catch (ObjectNotFoundException e) {
resp.sendError(WebdavStatus.SC_NOT_FOUND);
} catch (LinkedObjectNotFoundException e) {
resp.sendError(WebdavStatus.SC_NOT_FOUND);
} catch (SlideException e) {
resp.setStatus(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
}
} else {
resp.sendError(WebdavStatus.SC_FORBIDDEN);
}
}
/**
* 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 {
// fire put event
if ( WebdavEvent.GET.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(WebdavEvent.GET, new WebdavEvent(this));
// Then we must get object contents ...
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) {
if (revisionDescriptor.propertyValueContains(
NodeRevisionDescriptor.RESOURCE_TYPE,
NodeRevisionDescriptor.COLLECTION_TYPE)) {
displayDirectory();
} else {
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 (tag must be quoted)
resp.setHeader("ETag", getETag(resourceInfo, true) );
resp.setHeader
("Content-Language", revisionDescriptor.getContentLanguage());
resp.addHeader
("Last-Modified",
revisionDescriptor.getLastModified().toString());
// do this before setting content length, as Tomcat 5 seems to be picky
// about this
resp.setBufferSize(output);
if ( ((ranges == null) || (ranges.isEmpty())) ) {
// full content response
resp.setContentType
(revisionDescriptor.getContentType());
resp.setContentLength
((int) revisionDescriptor.getContentLength());
// Copy the input stream to our output stream
// (if requested)
if (printContent) {
copy(resourceInfo, is, os);
}
} else {
// 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) {
copy(resourceInfo, is, os, range);
}
} else {
resp.setContentType
("multipart/byteranges; boundary="
+ mimeSeparation);
if (printContent) {
copy(resourceInfo, is, os,
ranges.elements(),
revisionDescriptor.getContentType());
}
}
}
}
} else {
// XXX if there is no revision descriptor, this should be a directory
displayDirectory();
// resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
} else {
// XXX if there is no revisions, this should be a directory
displayDirectory();
// resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
} catch (Exception e) {
int statusCode;
// XXX If there is some sort of IOException it has been issued by the copy methods
// which indicates the client aborted the connection
// like org.apache.catalina.connector.ClientAbortException thrown by Tomcat
if (e instanceof IOException) {
// XXX like in WebdavUtils which reacts on a failure upon put, we use this return method,
// however, is it sensible?
statusCode = WebdavStatus.SC_PRECONDITION_FAILED;
} else {
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
/**
* 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 or <code>null</code> of no ranges are requested
* @throws WebdavException if the range request is wrong
*/
private Vector parseRange(HttpServletRequest request,
HttpServletResponse response,
ResourceInfo resourceInfo)
throws IOException, WebdavException
{
// Retrieving the range header (if any is specified)
String rangeHeader = request.getHeader("Range");
if (rangeHeader == null) {
return null;
}
// Checking If-Range
String headerValue = request.getHeader("If-Range");
if (headerValue != null) {
headerValue = headerValue.trim();
long lastModified = resourceInfo.date;
Date date = parseHttpDate(headerValue);
if (date == null) {
// If the ETag the client gave does not match the entity
// etag, then the entire entity is returned.
if (headerValue.startsWith("\"")) {
if (!getETag(resourceInfo, true).equals(headerValue)) {
return null;
}
} else {
// HACK Some clients does not quote the ETag properly
if (!getETagValue(resourceInfo, true).equals(headerValue)) {
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;
// 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);
throw new WebdavException(
HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
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);
throw new WebdavException(
HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
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);
throw new WebdavException(
HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
} 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);
throw new WebdavException(
HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
}
currentRange.length = (currentRange.end - currentRange.start + 1);
if (!currentRange.validate()) {
response.sendError
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
throw new WebdavException(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
result.addElement(currentRange);
}
return result;
}
// ------------------------------------------------------ Range Inner Class
private static 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));
}
}
}