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