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