//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.servlet;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator.CachedHttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.server.ResourceCache;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;



/* ------------------------------------------------------------ */
/** The default servlet.
 * 
 * This servlet, normally mapped to /, provides the handling for static
 * content, OPTION and TRACE methods for the context.
 * The following initParameters are supported, these can be set either
 * on the servlet itself or as ServletContext initParameters with a prefix
 * of org.eclipse.jetty.servlet.Default. :
 * <PRE>
 *  acceptRanges      If true, range requests and responses are
 *                    supported
 *
 *  dirAllowed        If true, directory listings are returned if no
 *                    welcome file is found. Else 403 Forbidden.
 *
 *  welcomeServlets   If true, attempt to dispatch to welcome files
 *                    that are servlets, but only after no matching static
 *                    resources could be found. If false, then a welcome
 *                    file must exist on disk. If "exact", then exact
 *                    servlet matches are supported without an existing file.
 *                    Default is true.
 *
 *                    This must be false if you want directory listings,
 *                    but have index.jsp in your welcome file list.
 *
 *  redirectWelcome   If true, welcome files are redirected rather than
 *                    forwarded to.
 *
 *  gzip              If set to true, then static content will be served as
 *                    gzip content encoded if a matching resource is
 *                    found ending with ".gz"
 *
 *  resourceBase      Set to replace the context resource base
 *
 *  resourceCache     If set, this is a context attribute name, which the servlet
 *                    will use to look for a shared ResourceCache instance.
 *
 *  relativeResourceBase
 *                    Set with a pathname relative to the base of the
 *                    servlet context root. Useful for only serving static content out
 *                    of only specific subdirectories.
 *
 *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
 *
 *  stylesheet	      Set with the location of an optional stylesheet that will be used
 *                    to decorate the directory listing html.
 *
 *  etags             If True, weak etags will be generated and handled.
 *
 *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
 *  maxCachedFileSize The maximum size of a file to cache
 *  maxCachedFiles    The maximum number of files to cache
 *
 *  useFileMappedBuffer
 *                    If set to true, it will use mapped file buffer to serve static content
 *                    when using NIO connector. Setting this value to false means that
 *                    a direct buffer will be used instead of a mapped file buffer.
 *                    This is set to false by default by this class, but may be overridden
 *                    by eg webdefault.xml 
 *
 *  cacheControl      If set, all static content will have this value set as the cache-control
 *                    header.
 *                    
 * otherGzipFileExtensions
 *                    Other file extensions that signify that a file is gzip compressed. Eg ".svgz"
 *
 *
 * </PRE>
 *
 *
 *
 *
 */
public class DefaultServlet extends HttpServlet implements ResourceFactory
{
    private static final Logger LOG = Log.getLogger(DefaultServlet.class);

    private static final long serialVersionUID = 4930458713846881193L;
    
    private static final CachedHttpField ACCEPT_RANGES = new CachedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
    
    private ServletContext _servletContext;
    private ContextHandler _contextHandler;

    private boolean _acceptRanges=true;
    private boolean _dirAllowed=true;
    private boolean _welcomeServlets=false;
    private boolean _welcomeExactServlets=false;
    private boolean _redirectWelcome=false;
    private boolean _gzip=false;
    private boolean _pathInfoOnly=false;
    private boolean _etags=false;

    private Resource _resourceBase;
    private ResourceCache _cache;

    private MimeTypes _mimeTypes;
    private String[] _welcomes;
    private Resource _stylesheet;
    private boolean _useFileMappedBuffer=false;
    private HttpField _cacheControl;
    private String _relativeResourceBase;
    private ServletHandler _servletHandler;
    private ServletHolder _defaultHolder;
    private List<String> _gzipEquivalentFileExtensions;

    /* ------------------------------------------------------------ */
    @Override
    public void init()
    throws UnavailableException
    {
        _servletContext=getServletContext();
        _contextHandler = initContextHandler(_servletContext);

        _mimeTypes = _contextHandler.getMimeTypes();

        _welcomes = _contextHandler.getWelcomeFiles();
        if (_welcomes==null)
            _welcomes=new String[] {"index.html","index.jsp"};

        _acceptRanges=getInitBoolean("acceptRanges",_acceptRanges);
        _dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
        _redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
        _gzip=getInitBoolean("gzip",_gzip);
        _pathInfoOnly=getInitBoolean("pathInfoOnly",_pathInfoOnly);

        if ("exact".equals(getInitParameter("welcomeServlets")))
        {
            _welcomeExactServlets=true;
            _welcomeServlets=false;
        }
        else
            _welcomeServlets=getInitBoolean("welcomeServlets", _welcomeServlets);

        _useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);

        _relativeResourceBase = getInitParameter("relativeResourceBase");

        String rb=getInitParameter("resourceBase");
        if (rb!=null)
        {
            if (_relativeResourceBase!=null)
                throw new  UnavailableException("resourceBase & relativeResourceBase");
            try{_resourceBase=_contextHandler.newResource(rb);}
            catch (Exception e)
            {
                LOG.warn(Log.EXCEPTION,e);
                throw new UnavailableException(e.toString());
            }
        }

        String css=getInitParameter("stylesheet");
        try
        {
            if(css!=null)
            {
                _stylesheet = Resource.newResource(css);
                if(!_stylesheet.exists())
                {
                    LOG.warn("!" + css);
                    _stylesheet = null;
                }
            }
            if(_stylesheet == null)
            {
                _stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
            }
        }
        catch(Exception e)
        {
            LOG.warn(e.toString());
            LOG.debug(e);
        }

        String cc=getInitParameter("cacheControl");
        if (cc!=null)
            _cacheControl=new CachedHttpField(HttpHeader.CACHE_CONTROL, cc);
        
        String resourceCache = getInitParameter("resourceCache");
        int max_cache_size=getInitInt("maxCacheSize", -2);
        int max_cached_file_size=getInitInt("maxCachedFileSize", -2);
        int max_cached_files=getInitInt("maxCachedFiles", -2);
        if (resourceCache!=null)
        {
            if (max_cache_size!=-1 || max_cached_file_size!= -2 || max_cached_files!=-2)
                LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
            if (_relativeResourceBase!=null || _resourceBase!=null)
                throw new UnavailableException("resourceCache specified with resource bases");
            _cache=(ResourceCache)_servletContext.getAttribute(resourceCache);

            if (LOG.isDebugEnabled())
                LOG.debug("Cache {}={}",resourceCache,_cache);
        }

        _etags = getInitBoolean("etags",_etags);
        
        try
        {
            if (_cache==null && (max_cached_files!=-2 || max_cache_size!=-2 || max_cached_file_size!=-2))
            {
                _cache= new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags);

                if (max_cache_size>=0)
                    _cache.setMaxCacheSize(max_cache_size);
                if (max_cached_file_size>=-1)
                    _cache.setMaxCachedFileSize(max_cached_file_size);
                if (max_cached_files>=-1)
                    _cache.setMaxCachedFiles(max_cached_files);
            }
        }
        catch (Exception e)
        {
            LOG.warn(Log.EXCEPTION,e);
            throw new UnavailableException(e.toString());
        }
        
       _gzipEquivalentFileExtensions = new ArrayList<String>();
       String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
       if (otherGzipExtensions != null)
       {
           //comma separated list
           StringTokenizer tok = new StringTokenizer(otherGzipExtensions,",",false);
           while (tok.hasMoreTokens())
           {
               String s = tok.nextToken().trim();
               _gzipEquivalentFileExtensions.add((s.charAt(0)=='.'?s:"."+s));
           }
       }
       else
       {
           //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip
           _gzipEquivalentFileExtensions.add(".svgz");   
       }

       _servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class);
       for (ServletHolder h :_servletHandler.getServlets())
           if (h.getServletInstance()==this)
               _defaultHolder=h;


       if (LOG.isDebugEnabled())
           LOG.debug("resource base = "+_resourceBase);
    }

    /**
     * Compute the field _contextHandler.<br/>
     * In the case where the DefaultServlet is deployed on the HttpService it is likely that
     * this method needs to be overwritten to unwrap the ServletContext facade until we reach
     * the original jetty's ContextHandler.
     * @param servletContext The servletContext of this servlet.
     * @return the jetty's ContextHandler for this servletContext.
     */
    protected ContextHandler initContextHandler(ServletContext servletContext)
    {
        ContextHandler.Context scontext=ContextHandler.getCurrentContext();
        if (scontext==null)
        {
            if (servletContext instanceof ContextHandler.Context)
                return ((ContextHandler.Context)servletContext).getContextHandler();
            else
                throw new IllegalArgumentException("The servletContext " + servletContext + " " +
                    servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
        }
        else
            return ContextHandler.getCurrentContext().getContextHandler();
    }

    /* ------------------------------------------------------------ */
    @Override
    public String getInitParameter(String name)
    {
        String value=getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default."+name);
        if (value==null)
            value=super.getInitParameter(name);
        return value;
    }

    /* ------------------------------------------------------------ */
    private boolean getInitBoolean(String name, boolean dft)
    {
        String value=getInitParameter(name);
        if (value==null || value.length()==0)
            return dft;
        return (value.startsWith("t")||
                value.startsWith("T")||
                value.startsWith("y")||
                value.startsWith("Y")||
                value.startsWith("1"));
    }

    /* ------------------------------------------------------------ */
    private int getInitInt(String name, int dft)
    {
        String value=getInitParameter(name);
        if (value==null)
            value=getInitParameter(name);
        if (value!=null && value.length()>0)
            return Integer.parseInt(value);
        return dft;
    }

    /* ------------------------------------------------------------ */
    /** get Resource to serve.
     * Map a path to a resource. The default implementation calls
     * HttpContext.getResource but derived servlets may provide
     * their own mapping.
     * @param pathInContext The path to find a resource for.
     * @return The resource to serve.
     */
    @Override
    public Resource getResource(String pathInContext)
    {
        Resource r=null;
        if (_relativeResourceBase!=null)
            pathInContext=URIUtil.addPaths(_relativeResourceBase,pathInContext);

        try
        {
            if (_resourceBase!=null)
            {
                r = _resourceBase.addPath(pathInContext);
                if (!_contextHandler.checkAlias(pathInContext,r))
                    r=null;
            }
            else if (_servletContext instanceof ContextHandler.Context)
            {
                r = _contextHandler.getResource(pathInContext);
            }
            else
            {
                URL u = _servletContext.getResource(pathInContext);
                r = _contextHandler.newResource(u);
            }

            if (LOG.isDebugEnabled())
                LOG.debug("Resource "+pathInContext+"="+r);
        }
        catch (IOException e)
        {
            LOG.ignore(e);
        }

        if((r==null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
            r=_stylesheet;

        return r;
    }

    /* ------------------------------------------------------------ */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
    {
        String servletPath=null;
        String pathInfo=null;
        Enumeration<String> reqRanges = null;
        Boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null;
        if (included!=null && included.booleanValue())
        {
            servletPath=(String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
            pathInfo=(String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
            if (servletPath==null)
            {
                servletPath=request.getServletPath();
                pathInfo=request.getPathInfo();
            }
        }
        else
        {
            included = Boolean.FALSE;
            servletPath = _pathInfoOnly?"/":request.getServletPath();
            pathInfo = request.getPathInfo();

            // Is this a Range request?
            reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
            if (!hasDefinedRange(reqRanges))
                reqRanges = null;
        }

        String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
        boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);


        // Find the resource and content
        Resource resource=null;
        HttpContent content=null;
        boolean close_content=true;
        try
        {
            // is gzip enabled?
            String pathInContextGz=null;
            boolean gzip=false;
            if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
            {
                // Look for a gzip resource
                pathInContextGz=pathInContext+".gz";
                if (_cache==null)
                    resource=getResource(pathInContextGz);
                else
                {
                    content=_cache.lookup(pathInContextGz);
                    resource=(content==null)?null:content.getResource();
                }

                // Does a gzip resource exist?
                if (resource!=null && resource.exists() && !resource.isDirectory())
                {
                    // Tell caches that response may vary by accept-encoding
                    response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString());
                    
                    // Does the client accept gzip?
                    String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
                    if (accept!=null && accept.indexOf("gzip")>=0)
                        gzip=true;
                }
            }

            // find resource
            if (!gzip)
            {
                if (_cache==null)
                    resource=getResource(pathInContext);
                else
                {
                    content=_cache.lookup(pathInContext);
                    resource=content==null?null:content.getResource();
                }
            }

            if (LOG.isDebugEnabled())
                LOG.debug(String.format("uri=%s, resource=%s, content=%s",request.getRequestURI(),resource,content));

            // Handle resource
            if (resource==null || !resource.exists())
            {
                if (included)
                    throw new FileNotFoundException("!" + pathInContext);
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
            else if (!resource.isDirectory())
            {
                if (endsWithSlash && pathInContext.length()>1)
                {
                    String q=request.getQueryString();
                    pathInContext=pathInContext.substring(0,pathInContext.length()-1);
                    if (q!=null&&q.length()!=0)
                        pathInContext+="?"+q;
                    response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext)));
                }
                else
                {
                    // ensure we have content
                    if (content==null)
                        content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),response.getBufferSize(),_etags);

                    if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
                    {
                        if (gzip || isGzippedContent(pathInContext))
                        {
                            response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip");
                            String mt=_servletContext.getMimeType(pathInContext);
                            if (mt!=null)
                                response.setContentType(mt);
                        }
                        close_content=sendData(request,response,included.booleanValue(),resource,content,reqRanges);
                    }
                }
            }
            else
            {
                String welcome=null;

                if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null))
                {
                    StringBuffer buf=request.getRequestURL();
                    synchronized(buf)
                    {
                        int param=buf.lastIndexOf(";");
                        if (param<0)
                            buf.append('/');
                        else
                            buf.insert(param,'/');
                        String q=request.getQueryString();
                        if (q!=null&&q.length()!=0)
                        {
                            buf.append('?');
                            buf.append(q);
                        }
                        response.setContentLength(0);
                        response.sendRedirect(response.encodeRedirectURL(buf.toString()));
                    }
                }
                // else look for a welcome file
                else if (null!=(welcome=getWelcomeFile(pathInContext)))
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("welcome={}",welcome);
                    if (_redirectWelcome)
                    {
                        // Redirect to the index
                        response.setContentLength(0);
                        String q=request.getQueryString();
                        if (q!=null&&q.length()!=0)
                            response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q));
                        else
                            response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)));
                    }
                    else
                    {
                        // Forward to the index
                        RequestDispatcher dispatcher=request.getRequestDispatcher(welcome);
                        if (dispatcher!=null)
                        {
                            if (included.booleanValue())
                                dispatcher.include(request,response);
                            else
                            {
                                request.setAttribute("org.eclipse.jetty.server.welcome",welcome);
                                dispatcher.forward(request,response);
                            }
                        }
                    }
                }
                else
                {
                    content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),_etags);
                    if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
                        sendDirectory(request,response,resource,pathInContext);
                }
            }
        }
        catch(IllegalArgumentException e)
        {
            LOG.warn(Log.EXCEPTION,e);
            if(!response.isCommitted())
                response.sendError(500, e.getMessage());
        }
        finally
        {
            if (close_content)
            {
                if (content!=null)
                    content.release();
                else if (resource!=null)
                    resource.close();
            }
        }

    }

    /**
     * @param resource
     * @return
     */
    protected boolean isGzippedContent(String path)
    {
        if (path == null) return false;
      
        for (String suffix:_gzipEquivalentFileExtensions)
            if (path.endsWith(suffix))
                return true;
        return false;
    }

    /* ------------------------------------------------------------ */
    private boolean hasDefinedRange(Enumeration<String> reqRanges)
    {
        return (reqRanges!=null && reqRanges.hasMoreElements());
    }

    /* ------------------------------------------------------------ */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
    {
        doGet(request,response);
    }

    /* ------------------------------------------------------------ */
    /* (non-Javadoc)
     * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    /* ------------------------------------------------------------ */
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
    {
        resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
    }

    /* ------------------------------------------------------------ */
    /**
     * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
     * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
     * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
     * If there is none, then <code>null</code> is returned.
     * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
     * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
     * @param resource
     * @return The path of the matching welcome file in context or null.
     * @throws IOException
     * @throws MalformedURLException
     */
    private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
    {
        if (_welcomes==null)
            return null;

        String welcome_servlet=null;
        for (int i=0;i<_welcomes.length;i++)
        {
            String welcome_in_context=URIUtil.addPaths(pathInContext,_welcomes[i]);
            Resource welcome=getResource(welcome_in_context);
            if (welcome!=null && welcome.exists())
                return _welcomes[i];

            if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
            {
                MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
                if (entry!=null && entry.getValue()!=_defaultHolder &&
                        (_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
                    welcome_servlet=welcome_in_context;

            }
        }
        return welcome_servlet;
    }

    /* ------------------------------------------------------------ */
    /* Check modification date headers.
     */
    protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
    throws IOException
    {
        try
        {
            if (!HttpMethod.HEAD.is(request.getMethod()))
            {
                if (_etags)
                {
                    String ifm=request.getHeader(HttpHeader.IF_MATCH.asString());
                    if (ifm!=null)
                    {
                        boolean match=false;
                        if (content.getETag()!=null)
                        {
                            QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm,", ",false,true);
                            while (!match && quoted.hasMoreTokens())
                            {
                                String tag = quoted.nextToken();
                                if (content.getETag().equals(tag))
                                    match=true;
                            }
                        }

                        if (!match)
                        {
                            response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
                            return false;
                        }
                    }
                    
                    String if_non_match_etag=request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
                    if (if_non_match_etag!=null && content.getETag()!=null)
                    {
                        // Look for GzipFiltered version of etag
                        if (content.getETag().equals(request.getAttribute("o.e.j.s.GzipFilter.ETag")))
                        {
                            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                            response.setHeader(HttpHeader.ETAG.asString(),if_non_match_etag);
                            return false;
                        }
                        
                        // Handle special case of exact match.
                        if (content.getETag().equals(if_non_match_etag))
                        {
                            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                            response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
                            return false;
                        }

                        // Handle list of tags
                        QuotedStringTokenizer quoted = new QuotedStringTokenizer(if_non_match_etag,", ",false,true);
                        while (quoted.hasMoreTokens())
                        {
                            String tag = quoted.nextToken();
                            if (content.getETag().equals(tag))
                            {
                                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                                response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
                                return false;
                            }
                        }
                        
                        // If etag requires content to be served, then do not check if-modified-since
                        return true;
                    }
                }
                
                // Handle if modified since
                String ifms=request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
                if (ifms!=null)
                {
                    //Get jetty's Response impl
                    String mdlm=content.getLastModified();
                    if (mdlm!=null && ifms.equals(mdlm))
                    {
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        if (_etags)
                            response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
                        response.flushBuffer();
                        return false;
                    }

                    long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
                    if (ifmsl!=-1 && resource.lastModified()/1000 <= ifmsl/1000)
                    { 
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        if (_etags)
                            response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
                        response.flushBuffer();
                        return false;
                    }
                }

                // Parse the if[un]modified dates and compare to resource
                long date=request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
                if (date!=-1 && resource.lastModified()/1000 > date/1000)
                {
                    response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                    return false;
                }

            }
        }
        catch(IllegalArgumentException iae)
        {
            if(!response.isCommitted())
                response.sendError(400, iae.getMessage());
            throw iae;
        }
        return true;
    }


    /* ------------------------------------------------------------------- */
    protected void sendDirectory(HttpServletRequest request,
            HttpServletResponse response,
            Resource resource,
            String pathInContext)
    throws IOException
    {
        if (!_dirAllowed)
        {
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        byte[] data=null;
        String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);

        //If the DefaultServlet has a resource base set, use it
        if (_resourceBase != null)
        {
            // handle ResourceCollection
            if (_resourceBase instanceof ResourceCollection)
                resource=_resourceBase.addPath(pathInContext);
        }
        //Otherwise, try using the resource base of its enclosing context handler
        else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
            resource=_contextHandler.getBaseResource().addPath(pathInContext);

        String dir = resource.getListHTML(base,pathInContext.length()>1, request.getQueryString());
        if (dir==null)
        {
            response.sendError(HttpServletResponse.SC_FORBIDDEN,
            "No directory");
            return;
        }

        data=dir.getBytes("UTF-8");
        response.setContentType("text/html; charset=UTF-8");
        response.setContentLength(data.length);
        response.getOutputStream().write(data);
    }

    /* ------------------------------------------------------------ */
    protected boolean sendData(HttpServletRequest request,
            HttpServletResponse response,
            boolean include,
            Resource resource,
            final HttpContent content,
            Enumeration<String> reqRanges)
    throws IOException
    {
        final long content_length = (content==null)?resource.length():content.getContentLength();
        
        // Get the output stream (or writer)
        OutputStream out =null;
        boolean written;
        try
        {
            out = response.getOutputStream();

            // has a filter already written to the response?
            written = out instanceof HttpOutput
                ? ((HttpOutput)out).isWritten()
                : true;
        }
        catch(IllegalStateException e)
        {
            out = new WriterOutputStream(response.getWriter());
            written=true; // there may be data in writer buffer, so assume written
        }
        
        if (LOG.isDebugEnabled())
            LOG.debug(String.format("sendData content=%s out=%s async=%b",content,out,request.isAsyncSupported()));

        if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0)
        {
            //  if there were no ranges, send entire entity
            if (include)
            {
                resource.writeTo(out,0,content_length);
            }
            // else if we can't do a bypass write because of wrapping
            else if (content==null || written || !(out instanceof HttpOutput))
            {
                // write normally
                writeHeaders(response,content,written?-1:content_length);
                ByteBuffer buffer = (content==null)?null:content.getIndirectBuffer();
                if (buffer!=null)
                    BufferUtil.writeTo(buffer,out);
                else
                    resource.writeTo(out,0,content_length);
            }
            // else do a bypass write
            else
            {
                // write the headers
                if (response instanceof Response)
                {
                    Response r = (Response)response;
                    writeOptionHeaders(r.getHttpFields());
                    r.setHeaders(content);
                }
                else
                    writeHeaders(response,content,content_length);

                // write the content asynchronously if supported
                if (request.isAsyncSupported())
                {
                    final AsyncContext context = request.startAsync();
                    context.setTimeout(0);

                    ((HttpOutput)out).sendContent(content,new Callback()
                    {
                        @Override
                        public void succeeded()
                        {   
                            context.complete();
                            content.release();
                        }

                        @Override
                        public void failed(Throwable x)
                        {
                            if (x instanceof IOException)
                                LOG.debug(x);
                            else
                                LOG.warn(x);
                            context.complete();
                            content.release();
                        }
                        
                        @Override
                        public String toString() 
                        {
                            return String.format("DefaultServlet@%x$CB", DefaultServlet.this.hashCode());
                        }
                    });
                    return false;
                }
                // otherwise write content blocking
                ((HttpOutput)out).sendContent(content);
                
            }
        }
        else
        {
            // Parse the satisfiable ranges
            List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);

            //  if there are no satisfiable ranges, send 416 response
            if (ranges==null || ranges.size()==0)
            {
                writeHeaders(response, content, content_length);
                response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
                        InclusiveByteRange.to416HeaderRangeString(content_length));
                resource.writeTo(out,0,content_length);
                return true;
            }

            //  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 = ranges.get(0);
                long singleLength = singleSatisfiableRange.getSize(content_length);
                writeHeaders(response,content,singleLength                     );
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                if (!response.containsHeader(HttpHeader.DATE.asString()))
                    response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
                response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
                        singleSatisfiableRange.toHeaderRangeString(content_length));
                resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
                return true;
            }

            //  multiple non-overlapping valid ranges cause a multipart
            //  216 response which does not require an overall
            //  content-length header
            //
            writeHeaders(response,content,-1);
            String mimetype=(content==null?null:content.getContentType());
            if (mimetype==null)
                LOG.warn("Unknown mimetype for "+request.getRequestURI());
            MultiPartOutputStream multi = new MultiPartOutputStream(out);
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            if (!response.containsHeader(HttpHeader.DATE.asString()))
                response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());

            // 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.getHeader(HttpHeader.REQUEST_RANGE.asString())!=null)
                ctp = "multipart/x-byteranges; boundary=";
            else
                ctp = "multipart/byteranges; boundary=";
            response.setContentType(ctp+multi.getBoundary());

            InputStream in=resource.getInputStream();
            long pos=0;

            // calculate the content-length
            int length=0;
            String[] header = new String[ranges.size()];
            for (int i=0;i<ranges.size();i++)
            {
                InclusiveByteRange ibr = ranges.get(i);
                header[i]=ibr.toHeaderRangeString(content_length);
                length+=
                    ((i>0)?2:0)+
                    2+multi.getBoundary().length()+2+
                    (mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+
                    HttpHeader.CONTENT_RANGE.asString().length()+2+header[i].length()+2+
                    2+
                    (ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
            }
            length+=2+2+multi.getBoundary().length()+2+2;
            response.setContentLength(length);

            for (int i=0;i<ranges.size();i++)
            {
                InclusiveByteRange ibr =  ranges.get(i);
                multi.startPart(mimetype,new String[]{HttpHeader.CONTENT_RANGE+": "+header[i]});

                long start=ibr.getFirst(content_length);
                long size=ibr.getSize(content_length);
                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,multi,size);
                    pos+=size;
                }
                else
                    // Handle cached resource
                    (resource).writeTo(multi,start,size);
            }
            if (in!=null)
                in.close();
            multi.close();
        }
        return true;
    }

    /* ------------------------------------------------------------ */
    protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
    {
        if (content == null)
        {
            // No content, then no headers to process
            // This is possible during bypass write because of wrapping
            // See .sendData() for more details.
            return;
        }
        
        if (content.getContentType()!=null && response.getContentType()==null)
            response.setContentType(content.getContentType().toString());

        if (response instanceof Response)
        {
            Response r=(Response)response;
            HttpFields fields = r.getHttpFields();

            if (content.getLastModified()!=null)
                fields.put(HttpHeader.LAST_MODIFIED,content.getLastModified());
            else if (content.getResource()!=null)
            {
                long lml=content.getResource().lastModified();
                if (lml!=-1)
                    fields.putDateField(HttpHeader.LAST_MODIFIED,lml);
            }

            if (count != -1)
                r.setLongContentLength(count);

            writeOptionHeaders(fields);
            
            if (_etags)
                fields.put(HttpHeader.ETAG,content.getETag());
        }
        else
        {
            long lml=content.getResource().lastModified();
            if (lml>=0)
                response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(),lml);

            if (count != -1)
            {
                if (count<Integer.MAX_VALUE)
                    response.setContentLength((int)count);
                else
                    response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(count));
            }

            writeOptionHeaders(response);

            if (_etags)
                response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
        }
    }

    /* ------------------------------------------------------------ */
    protected void writeOptionHeaders(HttpFields fields)
    {
        if (_acceptRanges)
            fields.put(ACCEPT_RANGES);

        if (_cacheControl!=null)
            fields.put(_cacheControl);
    }

    /* ------------------------------------------------------------ */
    protected void writeOptionHeaders(HttpServletResponse response)
    {
        if (_acceptRanges)
            response.setHeader(HttpHeader.ACCEPT_RANGES.asString(),"bytes");

        if (_cacheControl!=null)
            response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl.getValue());
    }

    /* ------------------------------------------------------------ */
    /*
     * @see javax.servlet.Servlet#destroy()
     */
    @Override
    public void destroy()
    {
        if (_cache!=null)
            _cache.flushCache();
        super.destroy();
    }

}
