View Javadoc

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