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