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