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()) LOG.debug("resource base = "+_resourceBase);
284     }
285 
286     /**
287      * Compute the field _contextHandler.<br/>
288      * In the case where the DefaultServlet is deployed on the HttpService it is likely that
289      * this method needs to be overwritten to unwrap the ServletContext facade until we reach
290      * the original jetty's ContextHandler.
291      * @param servletContext The servletContext of this servlet.
292      * @return the jetty's ContextHandler for this servletContext.
293      */
294     protected ContextHandler initContextHandler(ServletContext servletContext)
295     {
296         ContextHandler.Context scontext=ContextHandler.getCurrentContext();
297         if (scontext==null)
298         {
299             if (servletContext instanceof ContextHandler.Context)
300                 return ((ContextHandler.Context)servletContext).getContextHandler();
301             else
302                 throw new IllegalArgumentException("The servletContext " + servletContext + " " + 
303                     servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
304         }
305         else
306             return ContextHandler.getCurrentContext().getContextHandler();
307     }
308 
309     /* ------------------------------------------------------------ */
310     @Override
311     public String getInitParameter(String name)
312     {
313         String value=getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default."+name);
314         if (value==null)
315             value=super.getInitParameter(name);
316         return value;
317     }
318 
319     /* ------------------------------------------------------------ */
320     private boolean getInitBoolean(String name, boolean dft)
321     {
322         String value=getInitParameter(name);
323         if (value==null || value.length()==0)
324             return dft;
325         return (value.startsWith("t")||
326                 value.startsWith("T")||
327                 value.startsWith("y")||
328                 value.startsWith("Y")||
329                 value.startsWith("1"));
330     }
331 
332     /* ------------------------------------------------------------ */
333     private int getInitInt(String name, int dft)
334     {
335         String value=getInitParameter(name);
336         if (value==null)
337             value=getInitParameter(name);
338         if (value!=null && value.length()>0)
339             return Integer.parseInt(value);
340         return dft;
341     }
342 
343     /* ------------------------------------------------------------ */
344     /** get Resource to serve.
345      * Map a path to a resource. The default implementation calls
346      * HttpContext.getResource but derived servlets may provide
347      * their own mapping.
348      * @param pathInContext The path to find a resource for.
349      * @return The resource to serve.
350      */
351     public Resource getResource(String pathInContext)
352     {	
353         Resource r=null;
354         if (_relativeResourceBase!=null)
355             pathInContext=URIUtil.addPaths(_relativeResourceBase,pathInContext);
356 
357         try
358         {
359             if (_resourceBase!=null)
360             {
361                 r = _resourceBase.addPath(pathInContext);
362             }
363             else
364             {
365                 URL u = _servletContext.getResource(pathInContext);
366                 r = _contextHandler.newResource(u);
367             }
368 
369             if (LOG.isDebugEnabled())
370                 LOG.debug("Resource "+pathInContext+"="+r);
371         }
372         catch (IOException e)
373         {
374             LOG.ignore(e);
375         }
376 
377         if((r==null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
378             r=_stylesheet;
379 
380         return r;
381     }
382 
383     /* ------------------------------------------------------------ */
384     @Override
385     protected void doGet(HttpServletRequest request, HttpServletResponse response)
386     throws ServletException, IOException
387     {
388         String servletPath=null;
389         String pathInfo=null;
390         Enumeration<String> reqRanges = null;
391         Boolean included =request.getAttribute(Dispatcher.INCLUDE_REQUEST_URI)!=null;
392         if (included!=null && included.booleanValue())
393         {
394             servletPath=(String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH);
395             pathInfo=(String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO);
396             if (servletPath==null)
397             {
398                 servletPath=request.getServletPath();
399                 pathInfo=request.getPathInfo();
400             }
401         }
402         else
403         {
404             included = Boolean.FALSE;
405             servletPath = _pathInfoOnly?"/":request.getServletPath();
406             pathInfo = request.getPathInfo();
407 
408             // Is this a Range request?
409             reqRanges = request.getHeaders(HttpHeaders.RANGE);
410             if (!hasDefinedRange(reqRanges))
411                 reqRanges = null;
412         }
413         
414         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
415         boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
416         
417         // Can we gzip this request?
418         String pathInContextGz=null;
419         boolean gzip=false;
420         if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
421         {
422             String accept=request.getHeader(HttpHeaders.ACCEPT_ENCODING);
423             if (accept!=null && accept.indexOf("gzip")>=0)
424                 gzip=true;
425         }
426 
427         // Find the resource and content
428         Resource resource=null;
429         HttpContent content=null;
430 
431         try
432         {
433             // Try gzipped content first
434             if (gzip)
435             {
436                 pathInContextGz=pathInContext+".gz";
437 
438                 if (_cache==null)
439                 {
440                     resource=getResource(pathInContextGz);
441                 }
442                 else
443                 {
444                     content=_cache.lookup(pathInContextGz);
445                     resource=(content==null)?null:content.getResource();
446                 }
447 
448                 if (resource==null || !resource.exists() || resource.isDirectory())
449                 {
450                     gzip=false;
451                     pathInContextGz=null;
452                 }
453             }
454 
455             // find resource
456             if (!gzip)
457             {
458                 if (_cache==null)
459                     resource=getResource(pathInContext);
460                 else
461                 {
462                     content=_cache.lookup(pathInContext);
463                     resource=content==null?null:content.getResource();
464                 }
465             }
466 
467             if (LOG.isDebugEnabled())
468                 LOG.debug("uri="+request.getRequestURI()+" resource="+resource+(content!=null?" content":""));
469             
470             // Handle resource
471             if (resource==null || !resource.exists())
472             {
473                 if (included) 
474                     throw new FileNotFoundException("!" + pathInContext);
475                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
476             }
477             else if (!resource.isDirectory())
478             {
479                 if (endsWithSlash && _contextHandler.isAliases() && pathInContext.length()>1)
480                 {
481                     String q=request.getQueryString();
482                     pathInContext=pathInContext.substring(0,pathInContext.length()-1);
483                     if (q!=null&&q.length()!=0)
484                         pathInContext+="?"+q;
485                     response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext)));
486                 }
487                 else
488                 {
489                     // ensure we have content
490                     if (content==null)
491                         content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),response.getBufferSize());
492 
493                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
494                     {
495                         if (gzip)
496                         {
497                             response.setHeader(HttpHeaders.CONTENT_ENCODING,"gzip");
498                             String mt=_servletContext.getMimeType(pathInContext);
499                             if (mt!=null)
500                                 response.setContentType(mt);
501                         }
502                         sendData(request,response,included.booleanValue(),resource,content,reqRanges);
503                     }
504                 }
505             }
506             else
507             {
508                 String welcome=null;
509 
510                 if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null))
511                 {
512                     StringBuffer buf=request.getRequestURL();
513                     synchronized(buf)
514                     {
515                         int param=buf.lastIndexOf(";");
516                         if (param<0)
517                             buf.append('/');
518                         else
519                             buf.insert(param,'/');
520                         String q=request.getQueryString();
521                         if (q!=null&&q.length()!=0)
522                         {
523                             buf.append('?');
524                             buf.append(q);
525                         }
526                         response.setContentLength(0);
527                         response.sendRedirect(response.encodeRedirectURL(buf.toString()));
528                     }
529                 }
530                 // else look for a welcome file
531                 else if (null!=(welcome=getWelcomeFile(pathInContext)))
532                 {
533                     LOG.debug("welcome={}",welcome);
534                     if (_redirectWelcome)
535                     {
536                         // Redirect to the index
537                         response.setContentLength(0);
538                         String q=request.getQueryString();
539                         if (q!=null&&q.length()!=0)
540                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q));
541                         else
542                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)));
543                     }
544                     else
545                     {
546                         // Forward to the index
547                         RequestDispatcher dispatcher=request.getRequestDispatcher(welcome);
548                         if (dispatcher!=null)
549                         {
550                             if (included.booleanValue())
551                                 dispatcher.include(request,response);
552                             else
553                             {
554                                 request.setAttribute("org.eclipse.jetty.server.welcome",welcome);
555                                 dispatcher.forward(request,response);
556                             }
557                         }
558                     }
559                 }
560                 else
561                 {
562                     content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()));
563                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
564                         sendDirectory(request,response,resource,pathInContext);
565                 }
566             }
567         }
568         catch(IllegalArgumentException e)
569         {
570             LOG.warn(Log.EXCEPTION,e);
571             if(!response.isCommitted())
572                 response.sendError(500, e.getMessage());
573         }
574         finally
575         {
576             if (content!=null)
577                 content.release();
578             else if (resource!=null)
579                 resource.release();
580         }
581 
582     }
583 
584     /* ------------------------------------------------------------ */
585     private boolean hasDefinedRange(Enumeration<String> reqRanges)
586     {
587         return (reqRanges!=null && reqRanges.hasMoreElements());
588     }
589 
590     /* ------------------------------------------------------------ */
591     @Override
592     protected void doPost(HttpServletRequest request, HttpServletResponse response)
593     throws ServletException, IOException
594     {
595         doGet(request,response);
596     }
597 
598     /* ------------------------------------------------------------ */
599     /* (non-Javadoc)
600      * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
601      */
602     @Override
603     protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
604     {
605         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
606     }
607 
608     /* ------------------------------------------------------------ */
609     @Override
610     protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
611     throws ServletException, IOException
612     {
613         resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
614     }
615 
616     /* ------------------------------------------------------------ */
617     /**
618      * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
619      * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
620      * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
621      * If there is none, then <code>null</code> is returned.
622      * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
623      * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
624      * @param resource
625      * @return The path of the matching welcome file in context or null.
626      * @throws IOException
627      * @throws MalformedURLException
628      */
629     private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
630     {
631         if (_welcomes==null)
632             return null;
633 
634         String welcome_servlet=null;
635         for (int i=0;i<_welcomes.length;i++)
636         {
637             String welcome_in_context=URIUtil.addPaths(pathInContext,_welcomes[i]);
638             Resource welcome=getResource(welcome_in_context);
639             if (welcome!=null && welcome.exists())
640                 return _welcomes[i];
641 
642             if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
643             {
644                 Map.Entry entry=_servletHandler.getHolderEntry(welcome_in_context);
645                 if (entry!=null && entry.getValue()!=_defaultHolder &&
646                         (_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
647                     welcome_servlet=welcome_in_context;
648 
649             }
650         }
651         return welcome_servlet;
652     }
653 
654     /* ------------------------------------------------------------ */
655     /* Check modification date headers.
656      */
657     protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
658     throws IOException
659     {
660         try
661         {
662             if (!request.getMethod().equals(HttpMethods.HEAD) )
663             {
664                 String ifms=request.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
665                 if (ifms!=null)
666                 {
667                     if (content!=null)
668                     {
669                         Buffer mdlm=content.getLastModified();
670                         if (mdlm!=null)
671                         {
672                             if (ifms.equals(mdlm.toString()))
673                             {
674                                 response.reset();
675                                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
676                                 response.flushBuffer();
677                                 return false;
678                             }
679                         }
680                     }
681 
682                     long ifmsl=request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
683                     if (ifmsl!=-1)
684                     {
685                         if (resource.lastModified()/1000 <= ifmsl/1000)
686                         {
687                             response.reset();
688                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
689                             response.flushBuffer();
690                             return false;
691                         }
692                     }
693                 }
694 
695                 // Parse the if[un]modified dates and compare to resource
696                 long date=request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);
697 
698                 if (date!=-1)
699                 {
700                     if (resource.lastModified()/1000 > date/1000)
701                     {
702                         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
703                         return false;
704                     }
705                 }
706 
707             }
708         }
709         catch(IllegalArgumentException iae)
710         {
711             if(!response.isCommitted())
712                 response.sendError(400, iae.getMessage());
713             throw iae;
714         }
715         return true;
716     }
717 
718 
719     /* ------------------------------------------------------------------- */
720     protected void sendDirectory(HttpServletRequest request,
721             HttpServletResponse response,
722             Resource resource,
723             String pathInContext)
724     throws IOException
725     {
726         if (!_dirAllowed)
727         {
728             response.sendError(HttpServletResponse.SC_FORBIDDEN);
729             return;
730         }
731 
732         byte[] data=null;
733         String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
734 
735         // handle ResourceCollection
736         if (_resourceBase instanceof ResourceCollection)
737             resource=_resourceBase.addPath(pathInContext);
738         else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
739             resource=_contextHandler.getBaseResource().addPath(pathInContext);
740 
741         String dir = resource.getListHTML(base,pathInContext.length()>1);
742         if (dir==null)
743         {
744             response.sendError(HttpServletResponse.SC_FORBIDDEN,
745             "No directory");
746             return;
747         }
748 
749         data=dir.getBytes("UTF-8");
750         response.setContentType("text/html; charset=UTF-8");
751         response.setContentLength(data.length);
752         response.getOutputStream().write(data);
753     }
754 
755     /* ------------------------------------------------------------ */
756     protected void sendData(HttpServletRequest request,
757             HttpServletResponse response,
758             boolean include,
759             Resource resource,
760             HttpContent content,
761             Enumeration reqRanges)
762     throws IOException
763     {
764         boolean direct;
765         long content_length;
766         if (content==null)
767         {
768             direct=false;
769             content_length=resource.length();
770         }
771         else
772         {
773             Connector connector = HttpConnection.getCurrentConnection().getConnector();
774             direct=connector instanceof NIOConnector && ((NIOConnector)connector).getUseDirectBuffers() && !(connector instanceof SslConnector);
775             content_length=content.getContentLength();
776         }
777 
778 
779         // Get the output stream (or writer)
780         OutputStream out =null;
781         boolean written;
782         try
783         {
784             out = response.getOutputStream();
785 
786             // has a filter already written to the response?
787             written = out instanceof HttpOutput 
788                 ? ((HttpOutput)out).isWritten() 
789                 : HttpConnection.getCurrentConnection().getGenerator().isWritten();
790         }
791         catch(IllegalStateException e) 
792         {
793             out = new WriterOutputStream(response.getWriter());
794             written=true; // there may be data in writer buffer, so assume written
795         }
796         
797         if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0)
798         {
799             //  if there were no ranges, send entire entity
800             if (include)
801             {
802                 resource.writeTo(out,0,content_length);
803             }
804             else
805             {
806                 // See if a direct methods can be used?
807                 if (content!=null && !written && out instanceof HttpOutput)
808                 {
809                     if (response instanceof Response)
810                     {
811                         writeOptionHeaders(((Response)response).getHttpFields());
812                         ((HttpConnection.Output)out).sendContent(content);
813                     }
814                     else 
815                     {
816                         Buffer buffer = direct?content.getDirectBuffer():content.getIndirectBuffer();
817                         if (buffer!=null)
818                         {
819                             writeHeaders(response,content,content_length);
820                             ((HttpConnection.Output)out).sendContent(buffer);
821                         }
822                         else
823                         {
824                             writeHeaders(response,content,content_length);
825                             resource.writeTo(out,0,content_length);
826                         }
827                     }
828                 }
829                 else 
830                 {
831                     // Write headers normally
832                     writeHeaders(response,content,written?-1:content_length);
833 
834                     // Write content normally
835                     Buffer buffer = (content==null)?null:content.getIndirectBuffer();
836                     if (buffer!=null)
837                         buffer.writeTo(out);
838                     else
839                         resource.writeTo(out,0,content_length);
840                 }
841             }
842         }
843         else
844         {
845             // Parse the satisfiable ranges
846             List ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
847 
848             //  if there are no satisfiable ranges, send 416 response
849             if (ranges==null || ranges.size()==0)
850             {
851                 writeHeaders(response, content, content_length);
852                 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
853                 response.setHeader(HttpHeaders.CONTENT_RANGE,
854                         InclusiveByteRange.to416HeaderRangeString(content_length));
855                 resource.writeTo(out,0,content_length);
856                 return;
857             }
858 
859             //  if there is only a single valid range (must be satisfiable
860             //  since were here now), send that range with a 216 response
861             if ( ranges.size()== 1)
862             {
863                 InclusiveByteRange singleSatisfiableRange =
864                     (InclusiveByteRange)ranges.get(0);
865                 long singleLength = singleSatisfiableRange.getSize(content_length);
866                 writeHeaders(response,content,singleLength                     );
867                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
868                 response.setHeader(HttpHeaders.CONTENT_RANGE,
869                         singleSatisfiableRange.toHeaderRangeString(content_length));
870                 resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
871                 return;
872             }
873 
874             //  multiple non-overlapping valid ranges cause a multipart
875             //  216 response which does not require an overall
876             //  content-length header
877             //
878             writeHeaders(response,content,-1);
879             String mimetype=content.getContentType().toString();
880             MultiPartOutputStream multi = new MultiPartOutputStream(out);
881             response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
882 
883             // If the request has a "Request-Range" header then we need to
884             // send an old style multipart/x-byteranges Content-Type. This
885             // keeps Netscape and acrobat happy. This is what Apache does.
886             String ctp;
887             if (request.getHeader(HttpHeaders.REQUEST_RANGE)!=null)
888                 ctp = "multipart/x-byteranges; boundary=";
889             else
890                 ctp = "multipart/byteranges; boundary=";
891             response.setContentType(ctp+multi.getBoundary());
892 
893             InputStream in=resource.getInputStream();
894             long pos=0;
895 
896             // calculate the content-length
897             int length=0;
898             String[] header = new String[ranges.size()];
899             for (int i=0;i<ranges.size();i++)
900             {
901                 InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
902                 header[i]=ibr.toHeaderRangeString(content_length);
903                 length+=
904                     ((i>0)?2:0)+
905                     2+multi.getBoundary().length()+2+
906                     HttpHeaders.CONTENT_TYPE.length()+2+mimetype.length()+2+
907                     HttpHeaders.CONTENT_RANGE.length()+2+header[i].length()+2+
908                     2+
909                     (ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
910             }
911             length+=2+2+multi.getBoundary().length()+2+2;
912             response.setContentLength(length);
913 
914             for (int i=0;i<ranges.size();i++)
915             {
916                 InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
917                 multi.startPart(mimetype,new String[]{HttpHeaders.CONTENT_RANGE+": "+header[i]});
918 
919                 long start=ibr.getFirst(content_length);
920                 long size=ibr.getSize(content_length);
921                 if (in!=null)
922                 {
923                     // Handle non cached resource
924                     if (start<pos)
925                     {
926                         in.close();
927                         in=resource.getInputStream();
928                         pos=0;
929                     }
930                     if (pos<start)
931                     {
932                         in.skip(start-pos);
933                         pos=start;
934                     }
935                     IO.copy(in,multi,size);
936                     pos+=size;
937                 }
938                 else
939                     // Handle cached resource
940                     (resource).writeTo(multi,start,size);
941 
942             }
943             if (in!=null)
944                 in.close();
945             multi.close();
946         }
947         return;
948     }
949 
950     /* ------------------------------------------------------------ */
951     protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
952     throws IOException
953     {
954         if (content.getContentType()!=null && response.getContentType()==null)
955             response.setContentType(content.getContentType().toString());
956 
957         if (response instanceof Response)
958         {
959             Response r=(Response)response;
960             HttpFields fields = r.getHttpFields();
961 
962             if (content.getLastModified()!=null)
963                 fields.put(HttpHeaders.LAST_MODIFIED_BUFFER,content.getLastModified());
964             else if (content.getResource()!=null)
965             {
966                 long lml=content.getResource().lastModified();
967                 if (lml!=-1)
968                     fields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER,lml);
969             }
970 
971             if (count != -1)
972                 r.setLongContentLength(count);
973 
974             writeOptionHeaders(fields);
975         }
976         else
977         {
978             long lml=content.getResource().lastModified();
979             if (lml>=0)
980                 response.setDateHeader(HttpHeaders.LAST_MODIFIED,lml);
981 
982             if (count != -1)
983             {
984                 if (count<Integer.MAX_VALUE)
985                     response.setContentLength((int)count);
986                 else
987                     response.setHeader(HttpHeaders.CONTENT_LENGTH,Long.toString(count));
988             }
989 
990             writeOptionHeaders(response);
991         }
992     }
993 
994     /* ------------------------------------------------------------ */
995     protected void writeOptionHeaders(HttpFields fields) throws IOException
996     {
997         if (_acceptRanges)
998             fields.put(HttpHeaders.ACCEPT_RANGES_BUFFER,HttpHeaderValues.BYTES_BUFFER);
999 
1000         if (_cacheControl!=null)
1001             fields.put(HttpHeaders.CACHE_CONTROL_BUFFER,_cacheControl);
1002     }
1003 
1004     /* ------------------------------------------------------------ */
1005     protected void writeOptionHeaders(HttpServletResponse response) throws IOException
1006     {
1007         if (_acceptRanges)
1008             response.setHeader(HttpHeaders.ACCEPT_RANGES,"bytes");
1009 
1010         if (_cacheControl!=null)
1011             response.setHeader(HttpHeaders.CACHE_CONTROL,_cacheControl.toString());
1012     }
1013 
1014     /* ------------------------------------------------------------ */
1015     /*
1016      * @see javax.servlet.Servlet#destroy()
1017      */
1018     @Override
1019     public void destroy()
1020     {
1021         if (_cache!=null)
1022             _cache.flushCache();
1023         super.destroy();
1024     }
1025 
1026 }