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