View Javadoc

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