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