// ========================================================================
// $Id: ResourceHandler.java,v 1.66 2005/08/24 08:18:17 gregwilkins Exp $
// Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.mortbay.http.handler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.http.HttpException;
import org.mortbay.http.HttpFields;
import org.mortbay.http.HttpRequest;
import org.mortbay.http.HttpResponse;
import org.mortbay.http.InclusiveByteRange;
import org.mortbay.http.MultiPartResponse;
import org.mortbay.http.ResourceCache;
import org.mortbay.util.CachedResource;
import org.mortbay.util.IO;
import org.mortbay.util.LogSupport;
import org.mortbay.util.Resource;
import org.mortbay.util.StringMap;
import org.mortbay.util.TypeUtil;
import org.mortbay.util.URI;

/* ------------------------------------------------------------ */
/** Handler to serve files and resources.
 * Serves files from a given resource URL base and implements
 * the GET, HEAD, DELETE, OPTIONS, PUT, MOVE methods and the
 * IfModifiedSince and IfUnmodifiedSince header fields.
 * A simple memory cache is also provided to reduce file I/O.
 * HTTP/1.1 ranges are supported.
 * 
 * @version $Id: ResourceHandler.java,v 1.66 2005/08/24 08:18:17 gregwilkins Exp $
 * @author Nuno Pregui?a (sorry, wasn't UTF-8)
 * @author Greg Wilkins
 */
public class ResourceHandler extends AbstractHttpHandler
{
    private static Log log = LogFactory.getLog(ResourceHandler.class);

    /* ----------------------------------------------------------------- */
    private boolean _acceptRanges=true;
    private boolean _redirectWelcomeFiles ;
    private String[] _methods=null;
    private String _allowed;
    private boolean _dirAllowed=true;
    private int _minGzipLength =-1;
    private StringMap _methodMap = new StringMap();
    {
        setAllowedMethods(new String[]
            {
                HttpRequest.__GET,
                HttpRequest.__POST,
                HttpRequest.__HEAD,
                HttpRequest.__OPTIONS,
                HttpRequest.__TRACE
            });
    }

    /* ----------------------------------------------------------------- */
    /** Construct a ResourceHandler.
     */
    public ResourceHandler()
    {}

 
    /* ----------------------------------------------------------------- */
    public synchronized void start()
        throws Exception
    {        
        super.start();
    }
 
    /* ----------------------------------------------------------------- */
    public void stop()
        throws InterruptedException
    {
        super.stop();
    }

    /* ------------------------------------------------------------ */
    public String[] getAllowedMethods()
    {
        return _methods;
    }

    /* ------------------------------------------------------------ */
    public void setAllowedMethods(String[] methods)
    {
        StringBuffer b = new StringBuffer();
        _methods=methods;
        _methodMap.clear();
        for (int i=0;i<methods.length;i++)
        {
            _methodMap.put(methods[i],methods[i]);
            if (i>0)
                b.append(',');
            b.append(methods[i]);
        }
        _allowed=b.toString();
    }

    /* ------------------------------------------------------------ */
    public boolean isMethodAllowed(String method)
    {
        return _methodMap.get(method)!=null;
    }

    /* ------------------------------------------------------------ */
    public String getAllowedString()
    {
        return _allowed;
    }
    
    /* ------------------------------------------------------------ */
    public boolean isDirAllowed()
    {
        return _dirAllowed;
    }
    
    /* ------------------------------------------------------------ */
    public void setDirAllowed(boolean dirAllowed)
    {
        _dirAllowed = dirAllowed;
    }
    
    /* ------------------------------------------------------------ */
    public boolean isAcceptRanges()
    {
        return _acceptRanges;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @return True if welcome files are redirected to. False if forward is used.
     */
    public boolean getRedirectWelcome()
    {
        return _redirectWelcomeFiles;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @param redirectWelcome True if welcome files are redirected to. False
     * if forward is used. 
     */
    public void setRedirectWelcome(boolean redirectWelcome)
    {
        _redirectWelcomeFiles = redirectWelcome;
    }
    
    /* ------------------------------------------------------------ */
    /** Set if the handler accepts range requests.
     * Default is false;
     * @param ar True if the handler should accept ranges
     */
    public void setAcceptRanges(boolean ar)
    {
        _acceptRanges=ar;
    }
    
    /* ------------------------------------------------------------ */
    /** Get minimum content length for GZIP encoding.
     * @return Minimum length of content for gzip encoding or -1 if disabled.
     */
    public int getMinGzipLength()
    {
        return _minGzipLength;
    }
    
    /* ------------------------------------------------------------ */
    /** Set minimum content length for GZIP encoding.
     * @param minGzipLength If set to a positive integer, then static content
     * larger than this will be served as gzip content encoded
     * if a matching resource is found ending with ".gz"
     */
    public void setMinGzipLength(int minGzipLength)
    {
        _minGzipLength = minGzipLength;
    }

    
    /* ------------------------------------------------------------ */
    /** get Resource to serve.
     * Map a path to a resource. The default implementation calls
     * HttpContext.getResource but derived handers may provide
     * their own mapping.
     * @param pathInContext The path to find a resource for.
     * @return The resource to serve.
     */
    protected Resource getResource(String pathInContext)
        throws IOException
    {
        return getHttpContext().getResource(pathInContext);
    }
    
    /* ------------------------------------------------------------ */
    public void handle(String pathInContext,
                       String pathParams,
                       HttpRequest request,
                       HttpResponse response)
        throws HttpException, IOException
    {
        Resource resource = getResource(pathInContext);
        if (resource==null)
            return;

        // Is the method allowed?
        if (!isMethodAllowed(request.getMethod()))
        {
            if(log.isDebugEnabled())log.debug("Method not allowed: "+request.getMethod());
            if (resource.exists())
            {
                setAllowHeader(response);
                response.sendError(HttpResponse.__405_Method_Not_Allowed);
            }
            return;
        }

        // Handle the request
        try
        {
            if(log.isDebugEnabled())log.debug("PATH="+pathInContext+" RESOURCE="+resource);
            
            // check filename
            String method=request.getMethod();
            if (method.equals(HttpRequest.__GET) ||
                method.equals(HttpRequest.__POST) ||
                method.equals(HttpRequest.__HEAD))
                handleGet(request, response, pathInContext, pathParams, resource);  
            else if (method.equals(HttpRequest.__PUT))
                handlePut(request, response, pathInContext, resource);
            else if (method.equals(HttpRequest.__DELETE))
                handleDelete(request, response, pathInContext, resource);
            else if (method.equals(HttpRequest.__OPTIONS))
                handleOptions(response, pathInContext);
            else if (method.equals(HttpRequest.__MOVE))
                handleMove(request, response, pathInContext, resource);
            else if (method.equals(HttpRequest.__TRACE))
                handleTrace(request, response);
            else
            {
                if(log.isDebugEnabled())log.debug("Unknown action:"+method);
                // anything else...
                try{
                    if (resource.exists())
                        response.sendError(HttpResponse.__501_Not_Implemented);
                }
                catch(Exception e) {LogSupport.ignore(log,e);}
            }
        }
        catch(IllegalArgumentException e)
        {
            LogSupport.ignore(log,e);
        }
        finally
        {
            if (resource!=null && !(resource instanceof CachedResource))
                resource.release();
        }
    }

    /* ------------------------------------------------------------------- */
    public void handleGet(HttpRequest request,
                          HttpResponse response,
                          String pathInContext,
                          String pathParams,
                          Resource resource)
        throws IOException
    {
        if(log.isDebugEnabled())log.debug("Looking for "+resource);
        
        if (resource!=null && resource.exists())
        {            
            // check if directory
            if (resource.isDirectory())
            {
                if (!pathInContext.endsWith("/") && !pathInContext.equals("/"))
                {
                    log.debug("Redirect to directory/");
                    
                    String q=request.getQuery();
                    
                    // Properly fix URI
                    URI urifix = new URI(request.getRequestURL().toString());
                    urifix.setPath(urifix.getPath());
                    StringBuffer buf = new StringBuffer(urifix.toString());
                    urifix = null;
                    
                    if (q!=null&&q.length()!=0)
                    {
                        buf.append('?');
                        buf.append(q);
                    }
                    response.setField(HttpFields.__Location, URI.addPaths(buf.toString(),"/"));
                    response.setStatus(302);
                    request.setHandled(true);
                    return;
                }
  
                // See if index file exists
                String welcome=getHttpContext().getWelcomeFile(resource);
                if (welcome!=null)
                {     
                    // Forward to the index
                    String ipath=URI.addPaths(pathInContext,welcome);
                    if (_redirectWelcomeFiles)
                    {
                        // Redirect to the index
                        ipath=URI.addPaths(getHttpContext().getContextPath(),ipath);
                        response.setContentLength(0);
                        response.sendRedirect(ipath);
                    }
                    else
                    {
                        URI uri=request.getURI();
                        uri.setPath(URI.addPaths(uri.getPath(),welcome));
                        getHttpContext().handle(ipath,pathParams,request,response);
                    }
                    return;
                }

                // Check modified dates
                if (!passConditionalHeaders(request,response,resource))
                    return;
                // If we got here, no forward to index took place
                sendDirectory(request,response,resource,pathInContext.length()>1);
            }
            // check if it is a file
            else if (resource.exists())
            {
                // Check modified dates
                if (!passConditionalHeaders(request,response,resource))
                    return;
                sendData(request,response,pathInContext,resource,true);
            }
            else
                // don't know what it is
                log.warn("Unknown file type");
        }
    }

 
    /* ------------------------------------------------------------ */
    /* Check modification date headers.
     */
    private boolean passConditionalHeaders(HttpRequest request,
                                           HttpResponse response,
                                           Resource resource)
        throws IOException
    {
        if (!request.getMethod().equals(HttpRequest.__HEAD))
        {
            // If we have meta data for the file
            // Try a direct match for most common requests. Avoids
            // parsing the date.
            ResourceCache.ResourceMetaData metaData =
                (ResourceCache.ResourceMetaData)resource.getAssociate();
            if (metaData!=null)
            {
                String ifms=request.getField(HttpFields.__IfModifiedSince);
                String mdlm=metaData.getLastModified();
                if (ifms!=null && mdlm!=null && ifms.equals(mdlm))
                {
                    response.setStatus(HttpResponse.__304_Not_Modified);
                    request.setHandled(true);
                    return false;
                }
            }

            
            long date=0;
            // Parse the if[un]modified dates and compare to resource
            
            if ((date=request.getDateField(HttpFields.__IfUnmodifiedSince))>0)
            {
                if (resource.lastModified()/1000 > date/1000)
                {
                    response.sendError(HttpResponse.__412_Precondition_Failed);
                    return false;
                }
            }
            
            if ((date=request.getDateField(HttpFields.__IfModifiedSince))>0)
            {
                
                if (resource.lastModified()/1000 <= date/1000)
                {
                    response.setStatus(HttpResponse.__304_Not_Modified);
                    request.setHandled(true);
                    return false;
                }
            }
   
        }
        return true;
    }
 
 
    /* ------------------------------------------------------------ */
    void handlePut(HttpRequest request,
                   HttpResponse response,
                   String pathInContext,
                   Resource resource)
        throws IOException
    {
        if(log.isDebugEnabled())log.debug("PUT "+pathInContext+" in "+resource);

        boolean exists=resource!=null && resource.exists();
        if (exists &&
            !passConditionalHeaders(request,response,resource))
            return;
        
        if (pathInContext.endsWith("/"))
        {
            if (!exists)
            {
                if (!resource.getFile().mkdirs())
                    response.sendError(HttpResponse.__403_Forbidden, "Directories could not be created");
                else
                {
                    request.setHandled(true);
                    response.setStatus(HttpResponse.__201_Created);
                    response.commit();
                }
            }
            else
            {
                request.setHandled(true);
                response.setStatus(HttpResponse.__200_OK);
                response.commit();
            }
        }
        else
        {
            try
            {
                int toRead = request.getContentLength();
                InputStream in = request.getInputStream();
                OutputStream out = resource.getOutputStream();
                if (toRead>=0)
                    IO.copy(in,out,toRead);
                else
                    IO.copy(in,out);
                out.close();
                request.setHandled(true);
                response.setStatus(exists
                                   ?HttpResponse.__200_OK
                                   :HttpResponse.__201_Created);
                response.commit();
            }
            catch (Exception ex)
            {
                log.warn(LogSupport.EXCEPTION,ex);
                response.sendError(HttpResponse.__403_Forbidden,
                                   ex.getMessage());
            }
        }
    }

    /* ------------------------------------------------------------ */
    void handleDelete(HttpRequest request,
                      HttpResponse response,
                      String pathInContext,
                      Resource resource)
        throws IOException
    {
        if(log.isDebugEnabled())log.debug("DELETE "+pathInContext+" from "+resource);  
 
        if (!resource.exists() ||
            !passConditionalHeaders(request,response,resource))
            return;
 
        try
        {
            // delete the file
            if (resource.delete())
                response.setStatus(HttpResponse.__204_No_Content);
            else
                response.sendError(HttpResponse.__403_Forbidden);

            // Send response
            request.setHandled(true);
        }
        catch (SecurityException sex)
        {
            log.warn(LogSupport.EXCEPTION,sex);
            response.sendError(HttpResponse.__403_Forbidden, sex.getMessage());
        }
    }

 
    /* ------------------------------------------------------------ */
    void handleMove(HttpRequest request,
                    HttpResponse response,
                    String pathInContext,
                    Resource resource)
        throws IOException
    {
        if (!resource.exists() || !passConditionalHeaders(request,response,resource))
            return;

 
        String newPath = URI.canonicalPath(request.getField("New-uri"));
        if (newPath==null)
        {
            response.sendError(HttpResponse.__405_Method_Not_Allowed,
                               "Bad new uri");
            return;
        }

        String contextPath = getHttpContext().getContextPath();
        if (contextPath!=null && !newPath.startsWith(contextPath))
        {
            response.sendError(HttpResponse.__405_Method_Not_Allowed,
                               "Not in context");
            return;
        }
        

        // Find path
        try
        {
            // TODO - Check this
            String newInfo=newPath;
            if (contextPath!=null)
                newInfo=newInfo.substring(contextPath.length());
            Resource newFile = getHttpContext().getBaseResource().addPath(newInfo);
     
            if(log.isDebugEnabled())log.debug("Moving "+resource+" to "+newFile);
            resource.renameTo(newFile);
    
            response.setStatus(HttpResponse.__204_No_Content);
            request.setHandled(true);
        }
        catch (Exception ex)
        {
            log.warn(LogSupport.EXCEPTION,ex);
            setAllowHeader(response);
            response.sendError(HttpResponse.__405_Method_Not_Allowed,
                               "Error:"+ex);
            return;
        }
    }
 
    /* ------------------------------------------------------------ */
    void handleOptions(HttpResponse response, String pathInContext)
        throws IOException
    {
        if ("*".equals(pathInContext))
            return;
        setAllowHeader(response);
        response.commit();
    }
 
    /* ------------------------------------------------------------ */
    void setAllowHeader(HttpResponse response)
    {
        response.setField(HttpFields.__Allow, getAllowedString());
    }
    
    /* ------------------------------------------------------------ */
    public void writeHeaders(HttpResponse response,Resource resource, long count)
        throws IOException
    {
        ResourceCache.ResourceMetaData metaData =
            (ResourceCache.ResourceMetaData)resource.getAssociate();

        response.setContentType(metaData.getMimeType());
        if (count != -1)
        {
            if (count==resource.length())
                response.setField(HttpFields.__ContentLength,metaData.getLength());
            else
                response.setContentLength((int)count);
        }

        response.setField(HttpFields.__LastModified,metaData.getLastModified());
        
        if (_acceptRanges && response.getHttpRequest().getDotVersion()>0)
            response.setField(HttpFields.__AcceptRanges,"bytes");
    }

    /* ------------------------------------------------------------ */
    public void sendData(HttpRequest request,
                         HttpResponse response,
                         String pathInContext,
                         Resource resource,
                         boolean writeHeaders)
        throws IOException
    {
        long resLength=resource.length();
        
        //  see if there are any range headers
        Enumeration reqRanges =
            request.getDotVersion()>0
            ?request.getFieldValues(HttpFields.__Range)
            :null;
        
        if (!writeHeaders || reqRanges == null || !reqRanges.hasMoreElements())
        {
            // look for a gziped content.
            Resource data=resource;
            if (_minGzipLength>0)
            {
                String accept=request.getField(HttpFields.__AcceptEncoding);
                if (accept!=null && resLength>_minGzipLength &&
                    !pathInContext.endsWith(".gz"))
                {
                    Resource gz = getHttpContext().getResource(pathInContext+".gz");
                    if (gz.exists() && accept.indexOf("gzip")>=0)
                    {
                        if(log.isDebugEnabled())log.debug("gzip="+gz);
                        response.setField(HttpFields.__ContentEncoding,"gzip");
                        data=gz;
                        resLength=data.length();
                    }
                }
            }
            writeHeaders(response,resource,resLength);
            
            request.setHandled(true);
            OutputStream out = response.getOutputStream();
            data.writeTo(out,0,resLength);
            return;
        }
            
        // Parse the satisfiable ranges
        List ranges =InclusiveByteRange.satisfiableRanges(reqRanges,resLength);
        if(log.isDebugEnabled())log.debug("ranges: " + reqRanges + " == " + ranges);
        
        //  if there are no satisfiable ranges, send 416 response
        if (ranges==null || ranges.size()==0)
        {
            log.debug("no satisfiable ranges");
            writeHeaders(response, resource, resLength);
            response.setStatus(HttpResponse.__416_Requested_Range_Not_Satisfiable);
            response.setReason((String)HttpResponse.__statusMsg
                               .get(TypeUtil.newInteger(HttpResponse.__416_Requested_Range_Not_Satisfiable)));

            response.setField(HttpFields.__ContentRange, 
                              InclusiveByteRange.to416HeaderRangeString(resLength));
            
            OutputStream out = response.getOutputStream();
            resource.writeTo(out,0,resLength);
            request.setHandled(true);
            return;
        }

        
        //  if there is only a single valid range (must be satisfiable 
        //  since were here now), send that range with a 216 response
        if ( ranges.size()== 1)
        {
            InclusiveByteRange singleSatisfiableRange =
                (InclusiveByteRange)ranges.get(0);
            if(log.isDebugEnabled())log.debug("single satisfiable range: " + singleSatisfiableRange);
            long singleLength = singleSatisfiableRange.getSize(resLength);
            writeHeaders(response,resource,singleLength);
            response.setStatus(HttpResponse.__206_Partial_Content);
            response.setReason((String)HttpResponse.__statusMsg
                               .get(TypeUtil.newInteger(HttpResponse.__206_Partial_Content)));
            response.setField(HttpFields.__ContentRange, 
                              singleSatisfiableRange.toHeaderRangeString(resLength));
            OutputStream out = response.getOutputStream();
            resource.writeTo(out,
                             singleSatisfiableRange.getFirst(resLength), 
                             singleLength);
            request.setHandled(true);
            return;
        }

        
        //  multiple non-overlapping valid ranges cause a multipart
        //  216 response which does not require an overall 
        //  content-length header
        //
        ResourceCache.ResourceMetaData metaData =
            (ResourceCache.ResourceMetaData)resource.getAssociate();
        String encoding = metaData.getMimeType();
        MultiPartResponse multi = new MultiPartResponse(response);
        response.setStatus(HttpResponse.__206_Partial_Content);
        response.setReason((String)HttpResponse.__statusMsg
                           .get(TypeUtil.newInteger(HttpResponse.__206_Partial_Content)));

	// If the request has a "Request-Range" header then we need to
	// send an old style multipart/x-byteranges Content-Type. This
	// keeps Netscape and acrobat happy. This is what Apache does.
	String ctp;
	if (request.containsField(HttpFields.__RequestRange))
	    ctp = "multipart/x-byteranges; boundary=";
	else
	    ctp = "multipart/byteranges; boundary=";
	response.setContentType(ctp+multi.getBoundary());

        InputStream in=(resource instanceof CachedResource)
            ?null:resource.getInputStream();
        OutputStream out = response.getOutputStream();
        long pos=0;
            
        for (int i=0;i<ranges.size();i++)
        {
            InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
            String header=HttpFields.__ContentRange+": "+
                ibr.toHeaderRangeString(resLength);
            if(log.isDebugEnabled())log.debug("multi range: "+encoding+" "+header);
            multi.startPart(encoding,new String[]{header});

            long start=ibr.getFirst(resLength);
            long size=ibr.getSize(resLength);
            if (in!=null)
            {
                // Handle non cached resource
                if (start<pos)
                {
                    in.close();
                    in=resource.getInputStream();
                    pos=0;
                }
                if (pos<start)
                {
                    in.skip(start-pos);
                    pos=start;
                }
                IO.copy(in,out,size);
                pos+=size;
            }
            else
                // Handle cached resource
                resource.writeTo(out,start,size);
            
        }
        if (in!=null)
            in.close();
        multi.close();

        request.setHandled(true);

        return;
    }


    /* ------------------------------------------------------------------- */
    void sendDirectory(HttpRequest request,
                       HttpResponse response,
                       Resource resource,
                       boolean parent)
        throws IOException
    {
        if (!_dirAllowed)
        {
            response.sendError(HttpResponse.__403_Forbidden);
            return;
        }
        
        request.setHandled(true);
        
        if(log.isDebugEnabled())log.debug("sendDirectory: "+resource);
        byte[] data=null;
        if (resource instanceof CachedResource)
            data=((CachedResource)resource).getCachedData();
        
        if (data==null)
        {
            String base = URI.addPaths(request.getPath(),"/");
            String dir = resource.getListHTML(URI.encodePath(base),parent);
            if (dir==null)
            {
                response.sendError(HttpResponse.__403_Forbidden,
                                   "No directory");
                return;
            }
            data=dir.getBytes("UTF8");
            if (resource instanceof CachedResource)
                ((CachedResource)resource).setCachedData(data);
        }
        
        response.setContentType("text/html; charset=UTF8");
        response.setContentLength(data.length);
        
        if (request.getMethod().equals(HttpRequest.__HEAD))
        {
            response.commit();
            return;
        }
        
        response.getOutputStream().write(data,0,data.length);
        response.commit();
    }
}



