]> WPIA git - gigi.git/blob - lib/jetty/org/eclipse/jetty/servlet/DefaultServlet.java
Merge "Update notes about password security"
[gigi.git] / lib / jetty / org / eclipse / jetty / servlet / DefaultServlet.java
1 //
2 //  ========================================================================
3 //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4 //  ------------------------------------------------------------------------
5 //  All rights reserved. This program and the accompanying materials
6 //  are made available under the terms of the Eclipse Public License v1.0
7 //  and Apache License v2.0 which accompanies this distribution.
8 //
9 //      The Eclipse Public License is available at
10 //      http://www.eclipse.org/legal/epl-v10.html
11 //
12 //      The Apache License v2.0 is available at
13 //      http://www.opensource.org/licenses/apache2.0.php
14 //
15 //  You may elect to redistribute this code under either of these licenses.
16 //  ========================================================================
17 //
18
19 package org.eclipse.jetty.servlet;
20
21 import java.io.FileNotFoundException;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.ByteBuffer;
28 import java.util.ArrayList;
29 import java.util.Enumeration;
30 import java.util.List;
31 import java.util.StringTokenizer;
32
33 import javax.servlet.AsyncContext;
34 import javax.servlet.RequestDispatcher;
35 import javax.servlet.ServletContext;
36 import javax.servlet.ServletException;
37 import javax.servlet.UnavailableException;
38 import javax.servlet.http.HttpServlet;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41
42 import org.eclipse.jetty.http.HttpContent;
43 import org.eclipse.jetty.http.HttpField;
44 import org.eclipse.jetty.http.HttpFields;
45 import org.eclipse.jetty.http.HttpGenerator.CachedHttpField;
46 import org.eclipse.jetty.http.HttpHeader;
47 import org.eclipse.jetty.http.HttpMethod;
48 import org.eclipse.jetty.http.MimeTypes;
49 import org.eclipse.jetty.http.PathMap.MappedEntry;
50 import org.eclipse.jetty.io.WriterOutputStream;
51 import org.eclipse.jetty.server.HttpOutput;
52 import org.eclipse.jetty.server.InclusiveByteRange;
53 import org.eclipse.jetty.server.ResourceCache;
54 import org.eclipse.jetty.server.Response;
55 import org.eclipse.jetty.server.handler.ContextHandler;
56 import org.eclipse.jetty.util.BufferUtil;
57 import org.eclipse.jetty.util.Callback;
58 import org.eclipse.jetty.util.IO;
59 import org.eclipse.jetty.util.MultiPartOutputStream;
60 import org.eclipse.jetty.util.QuotedStringTokenizer;
61 import org.eclipse.jetty.util.URIUtil;
62 import org.eclipse.jetty.util.log.Log;
63 import org.eclipse.jetty.util.log.Logger;
64 import org.eclipse.jetty.util.resource.Resource;
65 import org.eclipse.jetty.util.resource.ResourceCollection;
66 import org.eclipse.jetty.util.resource.ResourceFactory;
67
68
69
70 /* ------------------------------------------------------------ */
71 /** The default servlet.
72  * 
73  * This servlet, normally mapped to /, provides the handling for static
74  * content, OPTION and TRACE methods for the context.
75  * The following initParameters are supported, these can be set either
76  * on the servlet itself or as ServletContext initParameters with a prefix
77  * of org.eclipse.jetty.servlet.Default. :
78  * <PRE>
79  *  acceptRanges      If true, range requests and responses are
80  *                    supported
81  *
82  *  dirAllowed        If true, directory listings are returned if no
83  *                    welcome file is found. Else 403 Forbidden.
84  *
85  *  welcomeServlets   If true, attempt to dispatch to welcome files
86  *                    that are servlets, but only after no matching static
87  *                    resources could be found. If false, then a welcome
88  *                    file must exist on disk. If "exact", then exact
89  *                    servlet matches are supported without an existing file.
90  *                    Default is true.
91  *
92  *                    This must be false if you want directory listings,
93  *                    but have index.jsp in your welcome file list.
94  *
95  *  redirectWelcome   If true, welcome files are redirected rather than
96  *                    forwarded to.
97  *
98  *  gzip              If set to true, then static content will be served as
99  *                    gzip content encoded if a matching resource is
100  *                    found ending with ".gz"
101  *
102  *  resourceBase      Set to replace the context resource base
103  *
104  *  resourceCache     If set, this is a context attribute name, which the servlet
105  *                    will use to look for a shared ResourceCache instance.
106  *
107  *  relativeResourceBase
108  *                    Set with a pathname relative to the base of the
109  *                    servlet context root. Useful for only serving static content out
110  *                    of only specific subdirectories.
111  *
112  *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
113  *
114  *  stylesheet        Set with the location of an optional stylesheet that will be used
115  *                    to decorate the directory listing html.
116  *
117  *  etags             If True, weak etags will be generated and handled.
118  *
119  *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
120  *  maxCachedFileSize The maximum size of a file to cache
121  *  maxCachedFiles    The maximum number of files to cache
122  *
123  *  useFileMappedBuffer
124  *                    If set to true, it will use mapped file buffer to serve static content
125  *                    when using NIO connector. Setting this value to false means that
126  *                    a direct buffer will be used instead of a mapped file buffer.
127  *                    This is set to false by default by this class, but may be overridden
128  *                    by eg webdefault.xml 
129  *
130  *  cacheControl      If set, all static content will have this value set as the cache-control
131  *                    header.
132  *                    
133  * otherGzipFileExtensions
134  *                    Other file extensions that signify that a file is gzip compressed. Eg ".svgz"
135  *
136  *
137  * </PRE>
138  *
139  *
140  *
141  *
142  */
143 public class DefaultServlet extends HttpServlet implements ResourceFactory
144 {
145     private static final Logger LOG = Log.getLogger(DefaultServlet.class);
146
147     private static final long serialVersionUID = 4930458713846881193L;
148     
149     private static final CachedHttpField ACCEPT_RANGES = new CachedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
150     
151     private ServletContext _servletContext;
152     private ContextHandler _contextHandler;
153
154     private boolean _acceptRanges=true;
155     private boolean _dirAllowed=true;
156     private boolean _welcomeServlets=false;
157     private boolean _welcomeExactServlets=false;
158     private boolean _redirectWelcome=false;
159     private boolean _gzip=false;
160     private boolean _pathInfoOnly=false;
161     private boolean _etags=false;
162
163     private Resource _resourceBase;
164     private ResourceCache _cache;
165
166     private MimeTypes _mimeTypes;
167     private String[] _welcomes;
168     private Resource _stylesheet;
169     private boolean _useFileMappedBuffer=false;
170     private HttpField _cacheControl;
171     private String _relativeResourceBase;
172     private ServletHandler _servletHandler;
173     private ServletHolder _defaultHolder;
174     private List<String> _gzipEquivalentFileExtensions;
175
176     /* ------------------------------------------------------------ */
177     @Override
178     public void init()
179     throws UnavailableException
180     {
181         _servletContext=getServletContext();
182         _contextHandler = initContextHandler(_servletContext);
183
184         _mimeTypes = _contextHandler.getMimeTypes();
185
186         _welcomes = _contextHandler.getWelcomeFiles();
187         if (_welcomes==null)
188             _welcomes=new String[] {"index.html","index.jsp"};
189
190         _acceptRanges=getInitBoolean("acceptRanges",_acceptRanges);
191         _dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
192         _redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
193         _gzip=getInitBoolean("gzip",_gzip);
194         _pathInfoOnly=getInitBoolean("pathInfoOnly",_pathInfoOnly);
195
196         if ("exact".equals(getInitParameter("welcomeServlets")))
197         {
198             _welcomeExactServlets=true;
199             _welcomeServlets=false;
200         }
201         else
202             _welcomeServlets=getInitBoolean("welcomeServlets", _welcomeServlets);
203
204         _useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);
205
206         _relativeResourceBase = getInitParameter("relativeResourceBase");
207
208         String rb=getInitParameter("resourceBase");
209         if (rb!=null)
210         {
211             if (_relativeResourceBase!=null)
212                 throw new  UnavailableException("resourceBase & relativeResourceBase");
213             try{_resourceBase=_contextHandler.newResource(rb);}
214             catch (Exception e)
215             {
216                 LOG.warn(Log.EXCEPTION,e);
217                 throw new UnavailableException(e.toString());
218             }
219         }
220
221         String css=getInitParameter("stylesheet");
222         try
223         {
224             if(css!=null)
225             {
226                 _stylesheet = Resource.newResource(css);
227                 if(!_stylesheet.exists())
228                 {
229                     LOG.warn("!" + css);
230                     _stylesheet = null;
231                 }
232             }
233             if(_stylesheet == null)
234             {
235                 _stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
236             }
237         }
238         catch(Exception e)
239         {
240             LOG.warn(e.toString());
241             LOG.debug(e);
242         }
243
244         String cc=getInitParameter("cacheControl");
245         if (cc!=null)
246             _cacheControl=new CachedHttpField(HttpHeader.CACHE_CONTROL, cc);
247         
248         String resourceCache = getInitParameter("resourceCache");
249         int max_cache_size=getInitInt("maxCacheSize", -2);
250         int max_cached_file_size=getInitInt("maxCachedFileSize", -2);
251         int max_cached_files=getInitInt("maxCachedFiles", -2);
252         if (resourceCache!=null)
253         {
254             if (max_cache_size!=-1 || max_cached_file_size!= -2 || max_cached_files!=-2)
255                 LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
256             if (_relativeResourceBase!=null || _resourceBase!=null)
257                 throw new UnavailableException("resourceCache specified with resource bases");
258             _cache=(ResourceCache)_servletContext.getAttribute(resourceCache);
259
260             if (LOG.isDebugEnabled())
261                 LOG.debug("Cache {}={}",resourceCache,_cache);
262         }
263
264         _etags = getInitBoolean("etags",_etags);
265         
266         try
267         {
268             if (_cache==null && (max_cached_files!=-2 || max_cache_size!=-2 || max_cached_file_size!=-2))
269             {
270                 _cache= new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags);
271
272                 if (max_cache_size>=0)
273                     _cache.setMaxCacheSize(max_cache_size);
274                 if (max_cached_file_size>=-1)
275                     _cache.setMaxCachedFileSize(max_cached_file_size);
276                 if (max_cached_files>=-1)
277                     _cache.setMaxCachedFiles(max_cached_files);
278             }
279         }
280         catch (Exception e)
281         {
282             LOG.warn(Log.EXCEPTION,e);
283             throw new UnavailableException(e.toString());
284         }
285         
286        _gzipEquivalentFileExtensions = new ArrayList<String>();
287        String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
288        if (otherGzipExtensions != null)
289        {
290            //comma separated list
291            StringTokenizer tok = new StringTokenizer(otherGzipExtensions,",",false);
292            while (tok.hasMoreTokens())
293            {
294                String s = tok.nextToken().trim();
295                _gzipEquivalentFileExtensions.add((s.charAt(0)=='.'?s:"."+s));
296            }
297        }
298        else
299        {
300            //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip
301            _gzipEquivalentFileExtensions.add(".svgz");   
302        }
303
304        _servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class);
305        for (ServletHolder h :_servletHandler.getServlets())
306            if (h.getServletInstance()==this)
307                _defaultHolder=h;
308
309
310        if (LOG.isDebugEnabled())
311            LOG.debug("resource base = "+_resourceBase);
312     }
313
314     /**
315      * Compute the field _contextHandler.<br/>
316      * In the case where the DefaultServlet is deployed on the HttpService it is likely that
317      * this method needs to be overwritten to unwrap the ServletContext facade until we reach
318      * the original jetty's ContextHandler.
319      * @param servletContext The servletContext of this servlet.
320      * @return the jetty's ContextHandler for this servletContext.
321      */
322     protected ContextHandler initContextHandler(ServletContext servletContext)
323     {
324         ContextHandler.Context scontext=ContextHandler.getCurrentContext();
325         if (scontext==null)
326         {
327             if (servletContext instanceof ContextHandler.Context)
328                 return ((ContextHandler.Context)servletContext).getContextHandler();
329             else
330                 throw new IllegalArgumentException("The servletContext " + servletContext + " " +
331                     servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
332         }
333         else
334             return ContextHandler.getCurrentContext().getContextHandler();
335     }
336
337     /* ------------------------------------------------------------ */
338     @Override
339     public String getInitParameter(String name)
340     {
341         String value=getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default."+name);
342         if (value==null)
343             value=super.getInitParameter(name);
344         return value;
345     }
346
347     /* ------------------------------------------------------------ */
348     private boolean getInitBoolean(String name, boolean dft)
349     {
350         String value=getInitParameter(name);
351         if (value==null || value.length()==0)
352             return dft;
353         return (value.startsWith("t")||
354                 value.startsWith("T")||
355                 value.startsWith("y")||
356                 value.startsWith("Y")||
357                 value.startsWith("1"));
358     }
359
360     /* ------------------------------------------------------------ */
361     private int getInitInt(String name, int dft)
362     {
363         String value=getInitParameter(name);
364         if (value==null)
365             value=getInitParameter(name);
366         if (value!=null && value.length()>0)
367             return Integer.parseInt(value);
368         return dft;
369     }
370
371     /* ------------------------------------------------------------ */
372     /** get Resource to serve.
373      * Map a path to a resource. The default implementation calls
374      * HttpContext.getResource but derived servlets may provide
375      * their own mapping.
376      * @param pathInContext The path to find a resource for.
377      * @return The resource to serve.
378      */
379     @Override
380     public Resource getResource(String pathInContext)
381     {
382         Resource r=null;
383         if (_relativeResourceBase!=null)
384             pathInContext=URIUtil.addPaths(_relativeResourceBase,pathInContext);
385
386         try
387         {
388             if (_resourceBase!=null)
389             {
390                 r = _resourceBase.addPath(pathInContext);
391                 if (!_contextHandler.checkAlias(pathInContext,r))
392                     r=null;
393             }
394             else if (_servletContext instanceof ContextHandler.Context)
395             {
396                 r = _contextHandler.getResource(pathInContext);
397             }
398             else
399             {
400                 URL u = _servletContext.getResource(pathInContext);
401                 r = _contextHandler.newResource(u);
402             }
403
404             if (LOG.isDebugEnabled())
405                 LOG.debug("Resource "+pathInContext+"="+r);
406         }
407         catch (IOException e)
408         {
409             LOG.ignore(e);
410         }
411
412         if((r==null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
413             r=_stylesheet;
414
415         return r;
416     }
417
418     /* ------------------------------------------------------------ */
419     @Override
420     protected void doGet(HttpServletRequest request, HttpServletResponse response)
421     throws ServletException, IOException
422     {
423         String servletPath=null;
424         String pathInfo=null;
425         Enumeration<String> reqRanges = null;
426         Boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null;
427         if (included!=null && included.booleanValue())
428         {
429             servletPath=(String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
430             pathInfo=(String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
431             if (servletPath==null)
432             {
433                 servletPath=request.getServletPath();
434                 pathInfo=request.getPathInfo();
435             }
436         }
437         else
438         {
439             included = Boolean.FALSE;
440             servletPath = _pathInfoOnly?"/":request.getServletPath();
441             pathInfo = request.getPathInfo();
442
443             // Is this a Range request?
444             reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
445             if (!hasDefinedRange(reqRanges))
446                 reqRanges = null;
447         }
448
449         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
450         boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
451
452
453         // Find the resource and content
454         Resource resource=null;
455         HttpContent content=null;
456         boolean close_content=true;
457         try
458         {
459             // is gzip enabled?
460             String pathInContextGz=null;
461             boolean gzip=false;
462             if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
463             {
464                 // Look for a gzip resource
465                 pathInContextGz=pathInContext+".gz";
466                 if (_cache==null)
467                     resource=getResource(pathInContextGz);
468                 else
469                 {
470                     content=_cache.lookup(pathInContextGz);
471                     resource=(content==null)?null:content.getResource();
472                 }
473
474                 // Does a gzip resource exist?
475                 if (resource!=null && resource.exists() && !resource.isDirectory())
476                 {
477                     // Tell caches that response may vary by accept-encoding
478                     response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString());
479                     
480                     // Does the client accept gzip?
481                     String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
482                     if (accept!=null && accept.indexOf("gzip")>=0)
483                         gzip=true;
484                 }
485             }
486
487             // find resource
488             if (!gzip)
489             {
490                 if (_cache==null)
491                     resource=getResource(pathInContext);
492                 else
493                 {
494                     content=_cache.lookup(pathInContext);
495                     resource=content==null?null:content.getResource();
496                 }
497             }
498
499             if (LOG.isDebugEnabled())
500                 LOG.debug(String.format("uri=%s, resource=%s, content=%s",request.getRequestURI(),resource,content));
501
502             // Handle resource
503             if (resource==null || !resource.exists())
504             {
505                 if (included)
506                     throw new FileNotFoundException("!" + pathInContext);
507                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
508             }
509             else if (!resource.isDirectory())
510             {
511                 if (endsWithSlash && pathInContext.length()>1)
512                 {
513                     String q=request.getQueryString();
514                     pathInContext=pathInContext.substring(0,pathInContext.length()-1);
515                     if (q!=null&&q.length()!=0)
516                         pathInContext+="?"+q;
517                     response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext)));
518                 }
519                 else
520                 {
521                     // ensure we have content
522                     if (content==null)
523                         content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),response.getBufferSize(),_etags);
524
525                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
526                     {
527                         if (gzip || isGzippedContent(pathInContext))
528                         {
529                             response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip");
530                             String mt=_servletContext.getMimeType(pathInContext);
531                             if (mt!=null)
532                                 response.setContentType(mt);
533                         }
534                         close_content=sendData(request,response,included.booleanValue(),resource,content,reqRanges);
535                     }
536                 }
537             }
538             else
539             {
540                 String welcome=null;
541
542                 if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null))
543                 {
544                     StringBuffer buf=request.getRequestURL();
545                     synchronized(buf)
546                     {
547                         int param=buf.lastIndexOf(";");
548                         if (param<0)
549                             buf.append('/');
550                         else
551                             buf.insert(param,'/');
552                         String q=request.getQueryString();
553                         if (q!=null&&q.length()!=0)
554                         {
555                             buf.append('?');
556                             buf.append(q);
557                         }
558                         response.setContentLength(0);
559                         response.sendRedirect(response.encodeRedirectURL(buf.toString()));
560                     }
561                 }
562                 // else look for a welcome file
563                 else if (null!=(welcome=getWelcomeFile(pathInContext)))
564                 {
565                     if (LOG.isDebugEnabled())
566                         LOG.debug("welcome={}",welcome);
567                     if (_redirectWelcome)
568                     {
569                         // Redirect to the index
570                         response.setContentLength(0);
571                         String q=request.getQueryString();
572                         if (q!=null&&q.length()!=0)
573                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q));
574                         else
575                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)));
576                     }
577                     else
578                     {
579                         // Forward to the index
580                         RequestDispatcher dispatcher=request.getRequestDispatcher(welcome);
581                         if (dispatcher!=null)
582                         {
583                             if (included.booleanValue())
584                                 dispatcher.include(request,response);
585                             else
586                             {
587                                 request.setAttribute("org.eclipse.jetty.server.welcome",welcome);
588                                 dispatcher.forward(request,response);
589                             }
590                         }
591                     }
592                 }
593                 else
594                 {
595                     content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),_etags);
596                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
597                         sendDirectory(request,response,resource,pathInContext);
598                 }
599             }
600         }
601         catch(IllegalArgumentException e)
602         {
603             LOG.warn(Log.EXCEPTION,e);
604             if(!response.isCommitted())
605                 response.sendError(500, e.getMessage());
606         }
607         finally
608         {
609             if (close_content)
610             {
611                 if (content!=null)
612                     content.release();
613                 else if (resource!=null)
614                     resource.close();
615             }
616         }
617
618     }
619
620     /**
621      * @param resource
622      * @return
623      */
624     protected boolean isGzippedContent(String path)
625     {
626         if (path == null) return false;
627       
628         for (String suffix:_gzipEquivalentFileExtensions)
629             if (path.endsWith(suffix))
630                 return true;
631         return false;
632     }
633
634     /* ------------------------------------------------------------ */
635     private boolean hasDefinedRange(Enumeration<String> reqRanges)
636     {
637         return (reqRanges!=null && reqRanges.hasMoreElements());
638     }
639
640     /* ------------------------------------------------------------ */
641     @Override
642     protected void doPost(HttpServletRequest request, HttpServletResponse response)
643     throws ServletException, IOException
644     {
645         doGet(request,response);
646     }
647
648     /* ------------------------------------------------------------ */
649     /* (non-Javadoc)
650      * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
651      */
652     @Override
653     protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
654     {
655         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
656     }
657
658     /* ------------------------------------------------------------ */
659     @Override
660     protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
661     throws ServletException, IOException
662     {
663         resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
664     }
665
666     /* ------------------------------------------------------------ */
667     /**
668      * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
669      * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
670      * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
671      * If there is none, then <code>null</code> is returned.
672      * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
673      * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
674      * @param resource
675      * @return The path of the matching welcome file in context or null.
676      * @throws IOException
677      * @throws MalformedURLException
678      */
679     private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
680     {
681         if (_welcomes==null)
682             return null;
683
684         String welcome_servlet=null;
685         for (int i=0;i<_welcomes.length;i++)
686         {
687             String welcome_in_context=URIUtil.addPaths(pathInContext,_welcomes[i]);
688             Resource welcome=getResource(welcome_in_context);
689             if (welcome!=null && welcome.exists())
690                 return _welcomes[i];
691
692             if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
693             {
694                 MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
695                 if (entry!=null && entry.getValue()!=_defaultHolder &&
696                         (_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
697                     welcome_servlet=welcome_in_context;
698
699             }
700         }
701         return welcome_servlet;
702     }
703
704     /* ------------------------------------------------------------ */
705     /* Check modification date headers.
706      */
707     protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
708     throws IOException
709     {
710         try
711         {
712             if (!HttpMethod.HEAD.is(request.getMethod()))
713             {
714                 if (_etags)
715                 {
716                     String ifm=request.getHeader(HttpHeader.IF_MATCH.asString());
717                     if (ifm!=null)
718                     {
719                         boolean match=false;
720                         if (content.getETag()!=null)
721                         {
722                             QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm,", ",false,true);
723                             while (!match && quoted.hasMoreTokens())
724                             {
725                                 String tag = quoted.nextToken();
726                                 if (content.getETag().equals(tag))
727                                     match=true;
728                             }
729                         }
730
731                         if (!match)
732                         {
733                             response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
734                             return false;
735                         }
736                     }
737                     
738                     String if_non_match_etag=request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
739                     if (if_non_match_etag!=null && content.getETag()!=null)
740                     {
741                         // Look for GzipFiltered version of etag
742                         if (content.getETag().equals(request.getAttribute("o.e.j.s.GzipFilter.ETag")))
743                         {
744                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
745                             response.setHeader(HttpHeader.ETAG.asString(),if_non_match_etag);
746                             return false;
747                         }
748                         
749                         // Handle special case of exact match.
750                         if (content.getETag().equals(if_non_match_etag))
751                         {
752                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
753                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
754                             return false;
755                         }
756
757                         // Handle list of tags
758                         QuotedStringTokenizer quoted = new QuotedStringTokenizer(if_non_match_etag,", ",false,true);
759                         while (quoted.hasMoreTokens())
760                         {
761                             String tag = quoted.nextToken();
762                             if (content.getETag().equals(tag))
763                             {
764                                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
765                                 response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
766                                 return false;
767                             }
768                         }
769                         
770                         // If etag requires content to be served, then do not check if-modified-since
771                         return true;
772                     }
773                 }
774                 
775                 // Handle if modified since
776                 String ifms=request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
777                 if (ifms!=null)
778                 {
779                     //Get jetty's Response impl
780                     String mdlm=content.getLastModified();
781                     if (mdlm!=null && ifms.equals(mdlm))
782                     {
783                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
784                         if (_etags)
785                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
786                         response.flushBuffer();
787                         return false;
788                     }
789
790                     long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
791                     if (ifmsl!=-1 && resource.lastModified()/1000 <= ifmsl/1000)
792                     { 
793                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
794                         if (_etags)
795                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
796                         response.flushBuffer();
797                         return false;
798                     }
799                 }
800
801                 // Parse the if[un]modified dates and compare to resource
802                 long date=request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
803                 if (date!=-1 && resource.lastModified()/1000 > date/1000)
804                 {
805                     response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
806                     return false;
807                 }
808
809             }
810         }
811         catch(IllegalArgumentException iae)
812         {
813             if(!response.isCommitted())
814                 response.sendError(400, iae.getMessage());
815             throw iae;
816         }
817         return true;
818     }
819
820
821     /* ------------------------------------------------------------------- */
822     protected void sendDirectory(HttpServletRequest request,
823             HttpServletResponse response,
824             Resource resource,
825             String pathInContext)
826     throws IOException
827     {
828         if (!_dirAllowed)
829         {
830             response.sendError(HttpServletResponse.SC_FORBIDDEN);
831             return;
832         }
833
834         byte[] data=null;
835         String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
836
837         //If the DefaultServlet has a resource base set, use it
838         if (_resourceBase != null)
839         {
840             // handle ResourceCollection
841             if (_resourceBase instanceof ResourceCollection)
842                 resource=_resourceBase.addPath(pathInContext);
843         }
844         //Otherwise, try using the resource base of its enclosing context handler
845         else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
846             resource=_contextHandler.getBaseResource().addPath(pathInContext);
847
848         String dir = resource.getListHTML(base,pathInContext.length()>1);
849         if (dir==null)
850         {
851             response.sendError(HttpServletResponse.SC_FORBIDDEN,
852             "No directory");
853             return;
854         }
855
856         data=dir.getBytes("UTF-8");
857         response.setContentType("text/html; charset=UTF-8");
858         response.setContentLength(data.length);
859         response.getOutputStream().write(data);
860     }
861
862     /* ------------------------------------------------------------ */
863     protected boolean sendData(HttpServletRequest request,
864             HttpServletResponse response,
865             boolean include,
866             Resource resource,
867             final HttpContent content,
868             Enumeration<String> reqRanges)
869     throws IOException
870     {
871         final long content_length = (content==null)?resource.length():content.getContentLength();
872         
873         // Get the output stream (or writer)
874         OutputStream out =null;
875         boolean written;
876         try
877         {
878             out = response.getOutputStream();
879
880             // has a filter already written to the response?
881             written = out instanceof HttpOutput
882                 ? ((HttpOutput)out).isWritten()
883                 : true;
884         }
885         catch(IllegalStateException e)
886         {
887             out = new WriterOutputStream(response.getWriter());
888             written=true; // there may be data in writer buffer, so assume written
889         }
890         
891         if (LOG.isDebugEnabled())
892             LOG.debug(String.format("sendData content=%s out=%s async=%b",content,out,request.isAsyncSupported()));
893
894         if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0)
895         {
896             //  if there were no ranges, send entire entity
897             if (include)
898             {
899                 resource.writeTo(out,0,content_length);
900             }
901             // else if we can't do a bypass write because of wrapping
902             else if (content==null || written || !(out instanceof HttpOutput))
903             {
904                 // write normally
905                 writeHeaders(response,content,written?-1:content_length);
906                 ByteBuffer buffer = (content==null)?null:content.getIndirectBuffer();
907                 if (buffer!=null)
908                     BufferUtil.writeTo(buffer,out);
909                 else
910                     resource.writeTo(out,0,content_length);
911             }
912             // else do a bypass write
913             else
914             {
915                 // write the headers
916                 if (response instanceof Response)
917                 {
918                     Response r = (Response)response;
919                     writeOptionHeaders(r.getHttpFields());
920                     r.setHeaders(content);
921                 }
922                 else
923                     writeHeaders(response,content,content_length);
924
925                 // write the content asynchronously if supported
926                 if (request.isAsyncSupported())
927                 {
928                     final AsyncContext context = request.startAsync();
929                     context.setTimeout(0);
930
931                     ((HttpOutput)out).sendContent(content,new Callback()
932                     {
933                         @Override
934                         public void succeeded()
935                         {   
936                             context.complete();
937                             content.release();
938                         }
939
940                         @Override
941                         public void failed(Throwable x)
942                         {
943                             if (x instanceof IOException)
944                                 LOG.debug(x);
945                             else
946                                 LOG.warn(x);
947                             context.complete();
948                             content.release();
949                         }
950                         
951                         @Override
952                         public String toString() 
953                         {
954                             return String.format("DefaultServlet@%x$CB", DefaultServlet.this.hashCode());
955                         }
956                     });
957                     return false;
958                 }
959                 // otherwise write content blocking
960                 ((HttpOutput)out).sendContent(content);
961                 
962             }
963         }
964         else
965         {
966             // Parse the satisfiable ranges
967             List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
968
969             //  if there are no satisfiable ranges, send 416 response
970             if (ranges==null || ranges.size()==0)
971             {
972                 writeHeaders(response, content, content_length);
973                 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
974                 response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
975                         InclusiveByteRange.to416HeaderRangeString(content_length));
976                 resource.writeTo(out,0,content_length);
977                 return true;
978             }
979
980             //  if there is only a single valid range (must be satisfiable
981             //  since were here now), send that range with a 216 response
982             if ( ranges.size()== 1)
983             {
984                 InclusiveByteRange singleSatisfiableRange = ranges.get(0);
985                 long singleLength = singleSatisfiableRange.getSize(content_length);
986                 writeHeaders(response,content,singleLength                     );
987                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
988                 if (!response.containsHeader(HttpHeader.DATE.asString()))
989                     response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
990                 response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
991                         singleSatisfiableRange.toHeaderRangeString(content_length));
992                 resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
993                 return true;
994             }
995
996             //  multiple non-overlapping valid ranges cause a multipart
997             //  216 response which does not require an overall
998             //  content-length header
999             //
1000             writeHeaders(response,content,-1);
1001             String mimetype=(content==null?null:content.getContentType());
1002             if (mimetype==null)
1003                 LOG.warn("Unknown mimetype for "+request.getRequestURI());
1004             MultiPartOutputStream multi = new MultiPartOutputStream(out);
1005             response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
1006             if (!response.containsHeader(HttpHeader.DATE.asString()))
1007                 response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
1008
1009             // If the request has a "Request-Range" header then we need to
1010             // send an old style multipart/x-byteranges Content-Type. This
1011             // keeps Netscape and acrobat happy. This is what Apache does.
1012             String ctp;
1013             if (request.getHeader(HttpHeader.REQUEST_RANGE.asString())!=null)
1014                 ctp = "multipart/x-byteranges; boundary=";
1015             else
1016                 ctp = "multipart/byteranges; boundary=";
1017             response.setContentType(ctp+multi.getBoundary());
1018
1019             InputStream in=resource.getInputStream();
1020             long pos=0;
1021
1022             // calculate the content-length
1023             int length=0;
1024             String[] header = new String[ranges.size()];
1025             for (int i=0;i<ranges.size();i++)
1026             {
1027                 InclusiveByteRange ibr = ranges.get(i);
1028                 header[i]=ibr.toHeaderRangeString(content_length);
1029                 length+=
1030                     ((i>0)?2:0)+
1031                     2+multi.getBoundary().length()+2+
1032                     (mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+
1033                     HttpHeader.CONTENT_RANGE.asString().length()+2+header[i].length()+2+
1034                     2+
1035                     (ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
1036             }
1037             length+=2+2+multi.getBoundary().length()+2+2;
1038             response.setContentLength(length);
1039
1040             for (int i=0;i<ranges.size();i++)
1041             {
1042                 InclusiveByteRange ibr =  ranges.get(i);
1043                 multi.startPart(mimetype,new String[]{HttpHeader.CONTENT_RANGE+": "+header[i]});
1044
1045                 long start=ibr.getFirst(content_length);
1046                 long size=ibr.getSize(content_length);
1047                 if (in!=null)
1048                 {
1049                     // Handle non cached resource
1050                     if (start<pos)
1051                     {
1052                         in.close();
1053                         in=resource.getInputStream();
1054                         pos=0;
1055                     }
1056                     if (pos<start)
1057                     {
1058                         in.skip(start-pos);
1059                         pos=start;
1060                     }
1061                     
1062                     IO.copy(in,multi,size);
1063                     pos+=size;
1064                 }
1065                 else
1066                     // Handle cached resource
1067                     (resource).writeTo(multi,start,size);
1068             }
1069             if (in!=null)
1070                 in.close();
1071             multi.close();
1072         }
1073         return true;
1074     }
1075
1076     /* ------------------------------------------------------------ */
1077     protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
1078     {
1079         if (content == null)
1080         {
1081             // No content, then no headers to process
1082             // This is possible during bypass write because of wrapping
1083             // See .sendData() for more details.
1084             return;
1085         }
1086         
1087         if (content.getContentType()!=null && response.getContentType()==null)
1088             response.setContentType(content.getContentType().toString());
1089
1090         if (response instanceof Response)
1091         {
1092             Response r=(Response)response;
1093             HttpFields fields = r.getHttpFields();
1094
1095             if (content.getLastModified()!=null)
1096                 fields.put(HttpHeader.LAST_MODIFIED,content.getLastModified());
1097             else if (content.getResource()!=null)
1098             {
1099                 long lml=content.getResource().lastModified();
1100                 if (lml!=-1)
1101                     fields.putDateField(HttpHeader.LAST_MODIFIED,lml);
1102             }
1103
1104             if (count != -1)
1105                 r.setLongContentLength(count);
1106
1107             writeOptionHeaders(fields);
1108             
1109             if (_etags)
1110                 fields.put(HttpHeader.ETAG,content.getETag());
1111         }
1112         else
1113         {
1114             long lml=content.getResource().lastModified();
1115             if (lml>=0)
1116                 response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(),lml);
1117
1118             if (count != -1)
1119             {
1120                 if (count<Integer.MAX_VALUE)
1121                     response.setContentLength((int)count);
1122                 else
1123                     response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(count));
1124             }
1125
1126             writeOptionHeaders(response);
1127
1128             if (_etags)
1129                 response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
1130         }
1131     }
1132
1133     /* ------------------------------------------------------------ */
1134     protected void writeOptionHeaders(HttpFields fields)
1135     {
1136         if (_acceptRanges)
1137             fields.put(ACCEPT_RANGES);
1138
1139         if (_cacheControl!=null)
1140             fields.put(_cacheControl);
1141     }
1142
1143     /* ------------------------------------------------------------ */
1144     protected void writeOptionHeaders(HttpServletResponse response)
1145     {
1146         if (_acceptRanges)
1147             response.setHeader(HttpHeader.ACCEPT_RANGES.asString(),"bytes");
1148
1149         if (_cacheControl!=null)
1150             response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl.getValue());
1151     }
1152
1153     /* ------------------------------------------------------------ */
1154     /*
1155      * @see javax.servlet.Servlet#destroy()
1156      */
1157     @Override
1158     public void destroy()
1159     {
1160         if (_cache!=null)
1161             _cache.flushCache();
1162         super.destroy();
1163     }
1164
1165 }