View Javadoc

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