View Javadoc

1   // ========================================================================
2   // Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty.servlet;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.net.MalformedURLException;
22  import java.util.Enumeration;
23  import java.util.List;
24  import java.util.Map.Entry;
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.mortbay.io.Buffer;
35  import org.mortbay.io.ByteArrayBuffer;
36  import org.mortbay.io.WriterOutputStream;
37  import org.mortbay.io.nio.DirectNIOBuffer;
38  import org.mortbay.io.nio.IndirectNIOBuffer;
39  import org.mortbay.io.nio.NIOBuffer;
40  import org.mortbay.jetty.Connector;
41  import org.mortbay.jetty.HttpConnection;
42  import org.mortbay.jetty.HttpContent;
43  import org.mortbay.jetty.HttpFields;
44  import org.mortbay.jetty.HttpHeaderValues;
45  import org.mortbay.jetty.HttpHeaders;
46  import org.mortbay.jetty.HttpMethods;
47  import org.mortbay.jetty.InclusiveByteRange;
48  import org.mortbay.jetty.MimeTypes;
49  import org.mortbay.jetty.ResourceCache;
50  import org.mortbay.jetty.Response;
51  import org.mortbay.jetty.handler.ContextHandler;
52  import org.mortbay.jetty.nio.NIOConnector;
53  import org.mortbay.log.Log;
54  import org.mortbay.resource.FileResource;
55  import org.mortbay.resource.Resource;
56  import org.mortbay.resource.ResourceFactory;
57  import org.mortbay.util.IO;
58  import org.mortbay.util.MultiPartOutputStream;
59  import org.mortbay.util.TypeUtil;
60  import org.mortbay.util.URIUtil;
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.mortbay.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. 
81   *                    
82   *                    This must be false if you want directory listings,
83   *                    but have index.jsp in your welcome file list.
84   *
85   *   redirectWelcome  If true, welcome files are redirected rather than
86   *                    forwarded to.
87   *
88   *   gzip             If set to true, then static content will be served as 
89   *                    gzip content encoded if a matching resource is 
90   *                    found ending with ".gz"
91   *
92   *  resourceBase      Set to replace the context resource base
93   *
94   *  relativeResourceBase    
95   *                    Set with a pathname relative to the base of the
96   *                    servlet context root. Useful for only serving static content out
97   *                    of only specific subdirectories.
98   * 
99   *  aliases           If True, aliases of resources are allowed (eg. symbolic
100  *                    links and caps variations). May bypass security constraints.
101  *                    
102  *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
103  *  maxCachedFileSize The maximum size of a file to cache
104  *  maxCachedFiles    The maximum number of files to cache
105  *  cacheType         Set to "bio", "nio" or "both" to determine the type resource cache. 
106  *                    A bio cached buffer may be used by nio but is not as efficient as an
107  *                    nio buffer.  An nio cached buffer may not be used by bio.    
108  *  
109  *  useFileMappedBuffer 
110  *                    If set to true, it will use mapped file buffer to serve static content
111  *                    when using NIO connector. Setting this value to false means that
112  *                    a direct buffer will be used instead of a mapped file buffer. 
113  *                    By default, this is set to true.
114  *                    
115  *  cacheControl      If set, all static content will have this value set as the cache-control
116  *                    header.
117  *                    
118  * 
119  * </PRE>
120  *                                                                    
121  *
122  * @author Greg Wilkins (gregw)
123  * @author Nigel Canonizado
124  */
125 public class DefaultServlet extends HttpServlet implements ResourceFactory
126 {   
127     private ContextHandler.SContext _context;
128     
129     private boolean _acceptRanges=true;
130     private boolean _dirAllowed=true;
131     private boolean _welcomeServlets=false;
132     private boolean _redirectWelcome=false;
133     private boolean _gzip=true;
134     
135     private Resource _resourceBase;
136     private NIOResourceCache _nioCache;
137     private ResourceCache _bioCache;
138     
139     private MimeTypes _mimeTypes;
140     private String[] _welcomes;
141     private boolean _aliases=false;
142     private boolean _useFileMappedBuffer=false;
143     ByteArrayBuffer _cacheControl;
144     
145     
146     /* ------------------------------------------------------------ */
147     public void init()
148         throws UnavailableException
149     {
150         ServletContext config=getServletContext();
151         _context = (ContextHandler.SContext)config;
152         _mimeTypes = _context.getContextHandler().getMimeTypes();
153         
154         _welcomes = _context.getContextHandler().getWelcomeFiles();
155         if (_welcomes==null)
156             _welcomes=new String[] {"index.jsp","index.html"};
157         
158         _acceptRanges=getInitBoolean("acceptRanges",_acceptRanges);
159         _dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
160         _welcomeServlets=getInitBoolean("welcomeServlets", _welcomeServlets);
161         _redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
162         _gzip=getInitBoolean("gzip",_gzip);
163         
164         _aliases=getInitBoolean("aliases",_aliases);
165 
166         if (!_aliases && !FileResource.getCheckAliases())
167             throw new IllegalStateException("Alias checking disabled");
168         if (_aliases)
169             config.log("Aliases are enabled");
170         
171         _useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);
172         
173         String rrb = getInitParameter("relativeResourceBase");
174         if (rrb!=null)
175         {
176             try
177             {
178                 _resourceBase = _context.getContextHandler().getResource(URIUtil.SLASH).addPath(rrb);
179             }
180             catch (Exception e) 
181             {
182                 Log.warn(Log.EXCEPTION,e);
183                 throw new UnavailableException(e.toString()); 
184             }
185         }
186         
187         String rb=getInitParameter("resourceBase");
188         if (rrb != null && rb != null)
189             throw new  UnavailableException("resourceBase & relativeResourceBase");    
190         
191         if (rb!=null)
192         {
193             try{_resourceBase=Resource.newResource(rb);}
194             catch (Exception e) 
195             {
196                 Log.warn(Log.EXCEPTION,e);
197                 throw new UnavailableException(e.toString()); 
198             }
199         }
200         
201         String t=getInitParameter("cacheControl");
202         if (t!=null)
203             _cacheControl=new ByteArrayBuffer(t);
204         
205         try
206         {
207             if (_resourceBase==null)
208                 _resourceBase = _context.getContextHandler().getResource(URIUtil.SLASH);
209 
210             String cache_type =getInitParameter("cacheType");
211             int max_cache_size=getInitInt("maxCacheSize", -2);
212             int max_cached_file_size=getInitInt("maxCachedFileSize", -2);
213             int max_cached_files=getInitInt("maxCachedFiles", -2);
214 
215             if (cache_type==null || "nio".equals(cache_type)|| "both".equals(cache_type))
216             {
217                 if (max_cache_size==-2 || max_cache_size>0)
218                 {
219                     _nioCache=new NIOResourceCache(_mimeTypes);
220                     if (max_cache_size>0)
221                         _nioCache.setMaxCacheSize(max_cache_size);    
222                     if (max_cached_file_size>=-1)
223                         _nioCache.setMaxCachedFileSize(max_cached_file_size);    
224                     if (max_cached_files>=-1)
225                         _nioCache.setMaxCachedFiles(max_cached_files);
226                     _nioCache.start();
227                 }
228             }
229             if ("bio".equals(cache_type)|| "both".equals(cache_type))
230             {
231                 if (max_cache_size==-2 || max_cache_size>0)
232                 {
233                     _bioCache=new ResourceCache(_mimeTypes);
234                     if (max_cache_size>0)
235                         _bioCache.setMaxCacheSize(max_cache_size);    
236                     if (max_cached_file_size>=-1)
237                         _bioCache.setMaxCachedFileSize(max_cached_file_size);    
238                     if (max_cached_files>=-1)
239                         _bioCache.setMaxCachedFiles(max_cached_files);
240                     _bioCache.start();
241                 }
242             }
243             if (_nioCache==null)
244                 _bioCache=null;
245            
246         }
247         catch (Exception e) 
248         {
249             Log.warn(Log.EXCEPTION,e);
250             throw new UnavailableException(e.toString()); 
251         }
252         
253         if (Log.isDebugEnabled()) Log.debug("resource base = "+_resourceBase);
254     }
255 
256     /* ------------------------------------------------------------ */
257     public String getInitParameter(String name)
258     {
259         String value=getServletContext().getInitParameter("org.mortbay.jetty.servlet.Default."+name);
260 	if (value==null)
261 	    value=super.getInitParameter(name);
262 	return value;
263     }
264     
265     /* ------------------------------------------------------------ */
266     private boolean getInitBoolean(String name, boolean dft)
267     {
268         String value=getInitParameter(name);
269         if (value==null || value.length()==0)
270             return dft;
271         return (value.startsWith("t")||
272                 value.startsWith("T")||
273                 value.startsWith("y")||
274                 value.startsWith("Y")||
275                 value.startsWith("1"));
276     }
277     
278     /* ------------------------------------------------------------ */
279     private int getInitInt(String name, int dft)
280     {
281         String value=getInitParameter(name);
282 	if (value==null)
283             value=getInitParameter(name);
284         if (value!=null && value.length()>0)
285             return Integer.parseInt(value);
286         return dft;
287     }
288     
289     /* ------------------------------------------------------------ */
290     /** get Resource to serve.
291      * Map a path to a resource. The default implementation calls
292      * HttpContext.getResource but derived servlets may provide
293      * their own mapping.
294      * @param pathInContext The path to find a resource for.
295      * @return The resource to serve.
296      */
297     public Resource getResource(String pathInContext)
298     {
299         if (_resourceBase==null)
300             return null;
301         Resource r=null;
302         try
303         {
304             r = _resourceBase.addPath(pathInContext);
305             if (!_aliases && r.getAlias()!=null)
306             {
307                 if (r.exists())
308                     Log.warn("Aliased resource: "+r+"=="+r.getAlias());
309                 return null;
310             }
311             if (Log.isDebugEnabled()) Log.debug("RESOURCE="+r);
312         }
313         catch (IOException e)
314         {
315             Log.ignore(e);
316         }
317         return r;
318     }
319     
320     /* ------------------------------------------------------------ */
321     protected void doGet(HttpServletRequest request, HttpServletResponse response)
322     	throws ServletException, IOException
323     {
324         String servletPath=null;
325         String pathInfo=null;
326         Enumeration reqRanges = null;
327         Boolean included =(Boolean)request.getAttribute(Dispatcher.__INCLUDE_JETTY);
328         if (included!=null && included.booleanValue())
329         {
330             servletPath=(String)request.getAttribute(Dispatcher.__INCLUDE_SERVLET_PATH);
331             pathInfo=(String)request.getAttribute(Dispatcher.__INCLUDE_PATH_INFO);
332             if (servletPath==null)
333             {
334                 servletPath=request.getServletPath();
335                 pathInfo=request.getPathInfo();
336             }
337         }
338         else
339         {
340             included=Boolean.FALSE;
341             servletPath=request.getServletPath();
342             pathInfo=request.getPathInfo();
343             
344             // Is this a range request?
345             reqRanges = request.getHeaders(HttpHeaders.RANGE);
346             if (reqRanges!=null && !reqRanges.hasMoreElements())
347                 reqRanges=null;
348         }
349         
350         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
351         boolean endsWithSlash=pathInContext.endsWith(URIUtil.SLASH);
352         
353         // Can we gzip this request?
354         String pathInContextGz=null;
355         boolean gzip=false;
356         if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
357         {
358             String accept=request.getHeader(HttpHeaders.ACCEPT_ENCODING);
359             if (accept!=null && accept.indexOf("gzip")>=0)
360                 gzip=true;
361         }
362         
363         // Find the resource and content
364         Resource resource=null;
365         HttpContent content=null;
366         
367         Connector connector = HttpConnection.getCurrentConnection().getConnector();
368         ResourceCache cache=(connector instanceof NIOConnector) ?_nioCache:_bioCache;
369         try
370         {   
371             // Try gzipped content first
372             if (gzip)
373             {
374                 pathInContextGz=pathInContext+".gz";  
375                 resource=getResource(pathInContextGz);
376 
377                 if (resource==null || !resource.exists()|| resource.isDirectory())
378                 {
379                     gzip=false;
380                     pathInContextGz=null;
381                 }
382                 else if (cache!=null)
383                 {
384                     content=cache.lookup(pathInContextGz,resource);
385                     if (content!=null)
386                         resource=content.getResource();
387                 }
388 
389                 if (resource==null || !resource.exists()|| resource.isDirectory())
390                 {
391                     gzip=false;
392                     pathInContextGz=null;
393                 }
394             }
395         
396             // find resource
397             if (!gzip)
398             {
399                 if (cache==null)
400                     resource=getResource(pathInContext);
401                 else
402                 {
403                     content=cache.lookup(pathInContext,this);
404 
405                     if (content!=null)
406                         resource=content.getResource();
407                     else
408                         resource=getResource(pathInContext);
409                 }
410             }
411             
412             if (Log.isDebugEnabled())
413                 Log.debug("resource="+resource+(content!=null?" content":""));
414                         
415             // Handle resource
416             if (resource==null || !resource.exists())
417                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
418             else if (!resource.isDirectory())
419             {   
420                 // ensure we have content
421                 if (content==null)
422                     content=new UnCachedContent(resource);
423                 
424                 if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))  
425                 {
426                     if (gzip)
427                     {
428                        response.setHeader(HttpHeaders.CONTENT_ENCODING,"gzip");
429                        String mt=_context.getMimeType(pathInContext);
430                        if (mt!=null)
431                            response.setContentType(mt);
432                     }
433                     sendData(request,response,included.booleanValue(),resource,content,reqRanges);  
434                 }
435             }
436             else
437             {
438                 String welcome=null;
439                 
440                 if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.mortbay.jetty.nullPathInfo")!=null))
441                 {
442                     StringBuffer buf=request.getRequestURL();
443                     int param=buf.lastIndexOf(";");
444                     if (param<0)
445                         buf.append('/');
446                     else
447                         buf.insert(param,'/');
448                     String q=request.getQueryString();
449                     if (q!=null&&q.length()!=0)
450                     {
451                         buf.append('?');
452                         buf.append(q);
453                     }
454                     response.setContentLength(0);
455                     response.sendRedirect(response.encodeRedirectURL(buf.toString()));
456                 }
457                 // else look for a welcome file
458                 else if (null!=(welcome=getWelcomeFile(resource)))
459                 {
460                     String ipath=URIUtil.addPaths(pathInContext,welcome);
461                     if (_redirectWelcome)
462                     {
463                         // Redirect to the index
464                         response.setContentLength(0);
465                         String q=request.getQueryString();
466                         if (q!=null&&q.length()!=0)
467                             response.sendRedirect(URIUtil.addPaths( _context.getContextPath(),ipath)+"?"+q);
468                         else
469                             response.sendRedirect(URIUtil.addPaths( _context.getContextPath(),ipath));
470                     }
471                     else
472                     {
473                         // Forward to the index
474                         RequestDispatcher dispatcher=request.getRequestDispatcher(ipath);
475                         if (dispatcher!=null)
476                         {
477                             if (included.booleanValue())
478                                 dispatcher.include(request,response);
479                             else
480                             {
481                                 request.setAttribute("org.mortbay.jetty.welcome",ipath);
482                                 dispatcher.forward(request,response);
483                             }
484                         }
485                     }
486                 }
487                 else 
488                 {
489                     content=new UnCachedContent(resource);
490                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
491                         sendDirectory(request,response,resource,pathInContext.length()>1);
492                 }
493             }
494         }
495         catch(IllegalArgumentException e)
496         {
497             Log.warn(Log.EXCEPTION,e);
498             if(!response.isCommitted())
499                 response.sendError(500, e.getMessage());
500         }
501         finally
502         {
503             if (content!=null)
504                 content.release();
505             else if (resource!=null)
506                 resource.release();
507         }
508         
509     }
510     
511     /* ------------------------------------------------------------ */
512     protected void doPost(HttpServletRequest request, HttpServletResponse response)
513         throws ServletException, IOException
514     {
515         doGet(request,response);
516     }
517     
518     /* ------------------------------------------------------------ */
519     /* (non-Javadoc)
520      * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
521      */
522     protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
523     {
524         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
525     }
526 
527     /* ------------------------------------------------------------ */
528     /**
529      * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of 
530      * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
531      * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping. 
532      * If there is none, then <code>null</code> is returned.
533      * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
534      * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
535      * @param resource
536      * @return The name of the matching welcome file.
537      * @throws IOException
538      * @throws MalformedURLException
539      */
540     private String getWelcomeFile(Resource resource) throws MalformedURLException, IOException
541     {
542         if (!resource.isDirectory() || _welcomes==null)
543             return null;
544 
545         for (int i=0;i<_welcomes.length;i++)
546         {
547             Resource welcome=resource.addPath(_welcomes[i]);
548             if (welcome.exists())
549                 return _welcomes[i];
550         }
551         
552         if (_welcomeServlets) 
553         {
554             ServletHandler servletHandler = (ServletHandler)_context.getContextHandler().getChildHandlerByClass(ServletHandler.class);
555             for (int i=0;i<_welcomes.length;i++)
556             {
557                 if (servletHandler.matchesPath(_welcomes[i]))
558                     return _welcomes[i];
559             }
560         }
561 
562         return null;
563     }
564 
565     /* ------------------------------------------------------------ */
566     /* Check modification date headers.
567      */
568     protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
569     throws IOException
570     {
571         try
572         {
573             if (!request.getMethod().equals(HttpMethods.HEAD) )
574             {
575                 String ifms=request.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
576                 if (ifms!=null)
577                 {
578                     if (content!=null)
579                     {
580                         Buffer mdlm=content.getLastModified();
581                         if (mdlm!=null)
582                         {
583                             if (ifms.equals(mdlm.toString()))
584                             {
585                                 response.reset();
586                                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
587                                 response.flushBuffer();
588                                 return false;
589                             }
590                         }
591                     }
592                         
593                     long ifmsl=request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
594                     if (ifmsl!=-1)
595                     {
596                         if (resource.lastModified()/1000 <= ifmsl/1000)
597                         {
598                             response.reset();
599                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
600                             response.flushBuffer();
601                             return false;
602                         }
603                     }
604                 }
605 
606                 // Parse the if[un]modified dates and compare to resource
607                 long date=request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);
608                 
609                 if (date!=-1)
610                 {
611                     if (resource.lastModified()/1000 > date/1000)
612                     {
613                         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
614                         return false;
615                     }
616                 }
617                 
618             }
619         }
620         catch(IllegalArgumentException iae)
621         {
622             if(!response.isCommitted())
623                 response.sendError(400, iae.getMessage());
624             throw iae;
625         }
626         return true;
627     }
628     
629     
630     /* ------------------------------------------------------------------- */
631     protected void sendDirectory(HttpServletRequest request,
632                                  HttpServletResponse response,
633                                  Resource resource,
634                                  boolean parent)
635     throws IOException
636     {
637         if (!_dirAllowed)
638         {
639             response.sendError(HttpServletResponse.SC_FORBIDDEN);
640             return;
641         }
642         
643         byte[] data=null;
644         String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
645         String dir = resource.getListHTML(base,parent);
646         if (dir==null)
647         {
648             response.sendError(HttpServletResponse.SC_FORBIDDEN,
649             "No directory");
650             return;
651         }
652         
653         data=dir.getBytes("UTF-8");
654         response.setContentType("text/html; charset=UTF-8");
655         response.setContentLength(data.length);
656         response.getOutputStream().write(data);
657     }
658     
659     /* ------------------------------------------------------------ */
660     protected void sendData(HttpServletRequest request,
661                             HttpServletResponse response,
662                             boolean include,
663                             Resource resource,
664                             HttpContent content,
665                             Enumeration reqRanges)
666     throws IOException
667     {
668         long content_length=resource.length();
669         
670         // Get the output stream (or writer)
671         OutputStream out =null;
672         try{out = response.getOutputStream();}
673         catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());}
674         
675         if ( reqRanges == null || !reqRanges.hasMoreElements())
676         {
677             //  if there were no ranges, send entire entity
678             if (include)
679             {
680                 resource.writeTo(out,0,content_length);
681             }
682             else
683             {
684                 // See if a direct methods can be used?
685                 if (out instanceof HttpConnection.Output)
686                 {
687                     if (response instanceof Response)
688                     {
689                         writeOptionHeaders(((Response)response).getHttpFields());
690                         ((HttpConnection.Output)out).sendContent(content);
691                     }
692                     else
693                     {
694                         writeHeaders(response,content,content_length);
695                         ((HttpConnection.Output)out).sendContent(content.getBuffer());
696                     }
697                 }
698                 else
699                 {
700                     // Write content normally
701                     writeHeaders(response,content,content_length);
702                     resource.writeTo(out,0,content_length);
703                 }
704             }
705         }
706         else
707         {
708             // Parse the satisfiable ranges
709             List ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
710             
711             //  if there are no satisfiable ranges, send 416 response
712             if (ranges==null || ranges.size()==0)
713             {
714                 writeHeaders(response, content, content_length);
715                 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
716                 response.setHeader(HttpHeaders.CONTENT_RANGE, 
717                         InclusiveByteRange.to416HeaderRangeString(content_length));
718                 resource.writeTo(out,0,content_length);
719                 return;
720             }
721             
722             
723             //  if there is only a single valid range (must be satisfiable 
724             //  since were here now), send that range with a 216 response
725             if ( ranges.size()== 1)
726             {
727                 InclusiveByteRange singleSatisfiableRange =
728                     (InclusiveByteRange)ranges.get(0);
729                 long singleLength = singleSatisfiableRange.getSize(content_length);
730                 writeHeaders(response,content,singleLength                     );
731                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
732                 response.setHeader(HttpHeaders.CONTENT_RANGE, 
733                         singleSatisfiableRange.toHeaderRangeString(content_length));
734                 resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
735                 return;
736             }
737             
738             
739             //  multiple non-overlapping valid ranges cause a multipart
740             //  216 response which does not require an overall 
741             //  content-length header
742             //
743             writeHeaders(response,content,-1);
744             String mimetype=content.getContentType().toString();
745             MultiPartOutputStream multi = new MultiPartOutputStream(out);
746             response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
747             
748             // If the request has a "Request-Range" header then we need to
749             // send an old style multipart/x-byteranges Content-Type. This
750             // keeps Netscape and acrobat happy. This is what Apache does.
751             String ctp;
752             if (request.getHeader(HttpHeaders.REQUEST_RANGE)!=null)
753                 ctp = "multipart/x-byteranges; boundary=";
754             else
755                 ctp = "multipart/byteranges; boundary=";
756             response.setContentType(ctp+multi.getBoundary());
757             
758             InputStream in=resource.getInputStream();
759             long pos=0;
760             
761             for (int i=0;i<ranges.size();i++)
762             {
763                 InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i);
764                 String header=HttpHeaders.CONTENT_RANGE+": "+
765                 ibr.toHeaderRangeString(content_length);
766                 multi.startPart(mimetype,new String[]{header});
767                 
768                 long start=ibr.getFirst(content_length);
769                 long size=ibr.getSize(content_length);
770                 if (in!=null)
771                 {
772                     // Handle non cached resource
773                     if (start<pos)
774                     {
775                         in.close();
776                         in=resource.getInputStream();
777                         pos=0;
778                     }
779                     if (pos<start)
780                     {
781                         in.skip(start-pos);
782                         pos=start;
783                     }
784                     IO.copy(in,multi,size);
785                     pos+=size;
786                 }
787                 else
788                     // Handle cached resource
789                     (resource).writeTo(multi,start,size);
790                 
791             }
792             if (in!=null)
793                 in.close();
794             multi.close();
795         }
796         return;
797     }
798     
799     /* ------------------------------------------------------------ */
800     protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
801         throws IOException
802     {   
803         if (content.getContentType()!=null && response.getContentType()==null)
804             response.setContentType(content.getContentType().toString());
805         
806         if (response instanceof Response)
807         {
808             Response r=(Response)response;
809             HttpFields fields = r.getHttpFields();
810 
811             if (content.getLastModified()!=null)  
812                 fields.put(HttpHeaders.LAST_MODIFIED_BUFFER,content.getLastModified(),content.getResource().lastModified());
813             else if (content.getResource()!=null)
814             {
815                 long lml=content.getResource().lastModified();
816                 if (lml!=-1)
817                     fields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER,lml);
818             }
819                 
820             if (count != -1)
821                 r.setLongContentLength(count);
822 
823             writeOptionHeaders(fields);
824         }
825         else
826         {
827             long lml=content.getResource().lastModified();
828             if (lml>=0)
829                 response.setDateHeader(HttpHeaders.LAST_MODIFIED,lml);
830 
831             if (count != -1)
832             {
833                 if (count<Integer.MAX_VALUE)
834                     response.setContentLength((int)count);
835                 else 
836                     response.setHeader(HttpHeaders.CONTENT_LENGTH,TypeUtil.toString(count));
837             }
838 
839             writeOptionHeaders(response);
840         }
841     }
842 
843     /* ------------------------------------------------------------ */
844     protected void writeOptionHeaders(HttpFields fields) throws IOException
845     { 
846         if (_acceptRanges)
847             fields.put(HttpHeaders.ACCEPT_RANGES_BUFFER,HttpHeaderValues.BYTES_BUFFER);
848 
849         if (_cacheControl!=null)
850             fields.put(HttpHeaders.CACHE_CONTROL_BUFFER,_cacheControl);
851     }
852     
853     /* ------------------------------------------------------------ */
854     protected void writeOptionHeaders(HttpServletResponse response) throws IOException
855     { 
856         if (_acceptRanges)
857             response.setHeader(HttpHeaders.ACCEPT_RANGES,"bytes");
858 
859         if (_cacheControl!=null)
860             response.setHeader(HttpHeaders.CACHE_CONTROL,_cacheControl.toString());
861     }
862 
863     /* ------------------------------------------------------------ */
864     /* 
865      * @see javax.servlet.Servlet#destroy()
866      */
867     public void destroy()
868     {
869         try
870         {
871             if (_nioCache!=null)
872                 _nioCache.stop();
873             if (_bioCache!=null)
874                 _bioCache.stop();
875         }
876         catch(Exception e)
877         {
878             Log.warn(Log.EXCEPTION,e);
879         }
880         finally
881         {
882             super.destroy();
883         }
884     }
885 
886     /* ------------------------------------------------------------ */
887     /* ------------------------------------------------------------ */
888     /* ------------------------------------------------------------ */
889     private class UnCachedContent implements HttpContent
890     {
891         Resource _resource;
892         
893         UnCachedContent(Resource resource)
894         {
895             _resource=resource;
896         }
897         
898         /* ------------------------------------------------------------ */
899         public Buffer getContentType()
900         {
901             return _mimeTypes.getMimeByExtension(_resource.toString());
902         }
903 
904         /* ------------------------------------------------------------ */
905         public Buffer getLastModified()
906         {
907             return null;
908         }
909 
910         /* ------------------------------------------------------------ */
911         public Buffer getBuffer()
912         {
913             return null;
914         }
915 
916         /* ------------------------------------------------------------ */
917         public long getContentLength()
918         {
919             return _resource.length();
920         }
921 
922         /* ------------------------------------------------------------ */
923         public InputStream getInputStream() throws IOException
924         {
925             return _resource.getInputStream();
926         }
927 
928         /* ------------------------------------------------------------ */
929         public Resource getResource()
930         {
931             return _resource;
932         }
933 
934         /* ------------------------------------------------------------ */
935         public void release()
936         {
937             _resource.release();
938             _resource=null;
939         }
940         
941     }
942 
943     /* ------------------------------------------------------------ */
944     /* ------------------------------------------------------------ */
945     class NIOResourceCache extends ResourceCache
946     {
947         /* ------------------------------------------------------------ */
948         public NIOResourceCache(MimeTypes mimeTypes)
949         {
950             super(mimeTypes);
951         }
952 
953         /* ------------------------------------------------------------ */
954         protected void fill(Content content) throws IOException
955         {
956             Buffer buffer=null;
957             Resource resource=content.getResource();
958             long length=resource.length();
959 
960             if (_useFileMappedBuffer && resource.getFile()!=null) 
961             {    
962                 buffer = new DirectNIOBuffer(resource.getFile());
963             } 
964             else 
965             {
966                 InputStream is = resource.getInputStream();
967                 try
968                 {
969                     Connector connector = HttpConnection.getCurrentConnection().getConnector();
970                     buffer = ((NIOConnector)connector).getUseDirectBuffers()?
971                             (NIOBuffer)new DirectNIOBuffer((int)length):
972                             (NIOBuffer)new IndirectNIOBuffer((int)length);
973                                 
974                 }
975                 catch(OutOfMemoryError e)
976                 {
977                     Log.warn(e.toString());
978                     Log.debug(e);
979                     buffer = new IndirectNIOBuffer((int) length);
980                 }
981                 buffer.readFrom(is,(int)length);
982                 is.close();
983             }
984             content.setBuffer(buffer);
985         }
986     }
987 }