View Javadoc

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