View Javadoc

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