/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/GetMethod.java,v 1.58 2005/02/25 17:03:52 luetzkendorf Exp $
 * $Revision: 1.58 $
 * $Date: 2005/02/25 17:03:52 $
 *
 * ====================================================================
 *
 * 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.ViolatedPrecondition;
import org.apache.slide.webdav.util.WebdavStatus;
import org.apache.slide.webdav.util.WebdavUtils;

/**
 * GET method.
 *
 */
public class GetMethod extends AbstractWebdavMethod implements ReadMethod, FineGrainedLockingMethod {


    // -------------------------------------------------------------- Constants


    /** The default buffer size */
    protected static final int DEFAULT_BUFFER_SIZE = 2048;

    /** The maximum buffer size to use */
    protected int maxBufferSize = DEFAULT_BUFFER_SIZE;


    /**
     * MIME multipart separation string
     */
    protected static final String mimeSeparation = "SLIDE_MIME_BOUNDARY";

    /**
     * Print content.
     */
    protected boolean printContent = true;

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

        // Try to get the max buffer size from the config, else we default to
        // the DEFAULT_BUFFER_SIZE
        String maxBufferSizeString = 
            token.getNamespaceConfig().getParameter("max-get-buffer-size");
        if (maxBufferSizeString != null)
            maxBufferSize = Integer.parseInt(maxBufferSizeString);
    }

    /**
     * @see org.apache.slide.webdav.method.FineGrainedLockingMethod#acquireFineGrainLocks()
     */
    public void acquireFineGrainLocks() {
        acquireStandardLocks(resourcePath);
    }

    // ------------------------------------------------------ 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 {
        resourcePath = requestUri;
        if (resourcePath == null) {
            resourcePath = "/";
        }

        // evaluate "Label" header
        if (Configuration.useVersionControl()) {
            try {

                String labelHeader = WebdavUtils.fixTomcatHeader(requestHeaders.getLabel(), "UTF-8");
                resourcePath = versioningHelper.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 {
                        
                        // Check if this is a redirect reference.
                        if (WebdavUtils.isRedirectref(revisionDescriptor)) {
                            resp.sendError(HttpServletResponse.SC_FORBIDDEN,
                                    "Redirect references do not have bodies");
                            return;
                        }
                    
                        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(getBufferSize(resourceInfo.length));

                        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, getBufferSize(resourceInfo.length));

        // Copy the input stream to the output stream
        exception = copyRange(istream, ostream, resourceInfo.length);

        // 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, 
                                    getBufferSize(range.end - range.start));
        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, 
                                        getBufferSize(resourceInfo.length));

            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,
                                  long resourceLength) {

        // Copy the input stream to the output stream
        IOException exception = null;
        byte buffer[] = new byte[getBufferSize(resourceLength)];
        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[getBufferSize(bytesToRead)];
        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;

    }

    /**
     * Get a reasonable buffer size.
     */
    private int getBufferSize(long resourceLength)
    {
        if (resourceLength <= 0)
            return DEFAULT_BUFFER_SIZE;

        return (int)Math.min(resourceLength, maxBufferSize);
    }

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

    }
}
