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