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