View Javadoc

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