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