View Javadoc

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