2 // ========================================================================
3 // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.server.handler;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.net.MalformedURLException;
24 import java.nio.ByteBuffer;
25 import java.nio.channels.ReadableByteChannel;
27 import javax.servlet.AsyncContext;
28 import javax.servlet.RequestDispatcher;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServletRequest;
31 import javax.servlet.http.HttpServletResponse;
33 import org.eclipse.jetty.http.HttpFields;
34 import org.eclipse.jetty.http.HttpHeader;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.eclipse.jetty.http.HttpStatus;
37 import org.eclipse.jetty.http.MimeTypes;
38 import org.eclipse.jetty.io.WriterOutputStream;
39 import org.eclipse.jetty.server.HttpOutput;
40 import org.eclipse.jetty.server.Request;
41 import org.eclipse.jetty.server.Response;
42 import org.eclipse.jetty.server.handler.ContextHandler.Context;
43 import org.eclipse.jetty.util.BufferUtil;
44 import org.eclipse.jetty.util.Callback;
45 import org.eclipse.jetty.util.URIUtil;
46 import org.eclipse.jetty.util.log.Log;
47 import org.eclipse.jetty.util.log.Logger;
48 import org.eclipse.jetty.util.resource.FileResource;
49 import org.eclipse.jetty.util.resource.Resource;
52 /* ------------------------------------------------------------ */
55 * This handle will serve static content and handle If-Modified-Since headers.
57 * Requests for resources that do not exist are let pass (Eg no 404's).
60 * @org.apache.xbean.XBean
62 public class ResourceHandler extends HandlerWrapper
64 private static final Logger LOG = Log.getLogger(ResourceHandler.class);
66 ContextHandler _context;
67 Resource _baseResource;
68 Resource _defaultStylesheet;
70 String[] _welcomeFiles={"index.html"};
71 MimeTypes _mimeTypes = new MimeTypes();
75 int _minMemoryMappedContentLength=1024;
76 int _minAsyncContentLength=0;
78 /* ------------------------------------------------------------ */
79 public ResourceHandler()
84 /* ------------------------------------------------------------ */
85 public MimeTypes getMimeTypes()
90 /* ------------------------------------------------------------ */
91 public void setMimeTypes(MimeTypes mimeTypes)
93 _mimeTypes = mimeTypes;
96 /* ------------------------------------------------------------ */
97 /** Get the directory option.
98 * @return true if directories are listed.
100 public boolean isDirectoriesListed()
105 /* ------------------------------------------------------------ */
106 /** Set the directory.
107 * @param directory true if directories are listed.
109 public void setDirectoriesListed(boolean directory)
111 _directory = directory;
114 /* ------------------------------------------------------------ */
115 /** Get minimum memory mapped file content length.
116 * @return the minimum size in bytes of a file resource that will
117 * be served using a memory mapped buffer, or -1 (default) for no memory mapped
120 public int getMinMemoryMappedContentLength()
122 return _minMemoryMappedContentLength;
125 /* ------------------------------------------------------------ */
126 /** Set minimum memory mapped file content length.
127 * @param minMemoryMappedFileSize the minimum size in bytes of a file resource that will
128 * be served using a memory mapped buffer, or -1 for no memory mapped
131 public void setMinMemoryMappedContentLength(int minMemoryMappedFileSize)
133 _minMemoryMappedContentLength = minMemoryMappedFileSize;
136 /* ------------------------------------------------------------ */
137 /** Get the minimum content length for async handling.
138 * @return The minimum size in bytes of the content before asynchronous
139 * handling is used, or -1 for no async handling or 0 (default) for using
140 * {@link HttpServletResponse#getBufferSize()} as the minimum length.
142 public int getMinAsyncContentLength()
144 return _minAsyncContentLength;
147 /* ------------------------------------------------------------ */
148 /** Set the minimum content length for async handling.
149 * @param minAsyncContentLength The minimum size in bytes of the content before asynchronous
150 * handling is used, or -1 for no async handling or 0 for using
151 * {@link HttpServletResponse#getBufferSize()} as the minimum length.
153 public void setMinAsyncContentLength(int minAsyncContentLength)
155 _minAsyncContentLength = minAsyncContentLength;
158 /* ------------------------------------------------------------ */
160 * @return True if ETag processing is done
162 public boolean isEtags()
167 /* ------------------------------------------------------------ */
169 * @param etags True if ETag processing is done
171 public void setEtags(boolean etags)
176 /* ------------------------------------------------------------ */
178 public void doStart()
181 Context scontext = ContextHandler.getCurrentContext();
182 _context = (scontext==null?null:scontext.getContextHandler());
187 /* ------------------------------------------------------------ */
189 * @return Returns the resourceBase.
191 public Resource getBaseResource()
193 if (_baseResource==null)
195 return _baseResource;
198 /* ------------------------------------------------------------ */
200 * @return Returns the base resource as a string.
202 public String getResourceBase()
204 if (_baseResource==null)
206 return _baseResource.toString();
210 /* ------------------------------------------------------------ */
212 * @param base The resourceBase to set.
214 public void setBaseResource(Resource base)
219 /* ------------------------------------------------------------ */
221 * @param resourceBase The base resource as a string.
223 public void setResourceBase(String resourceBase)
227 setBaseResource(Resource.newResource(resourceBase));
231 LOG.warn(e.toString());
233 throw new IllegalArgumentException(resourceBase);
237 /* ------------------------------------------------------------ */
239 * @return Returns the stylesheet as a Resource.
241 public Resource getStylesheet()
243 if(_stylesheet != null)
249 if(_defaultStylesheet == null)
251 _defaultStylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
253 return _defaultStylesheet;
257 /* ------------------------------------------------------------ */
259 * @param stylesheet The location of the stylesheet to be used as a String.
261 public void setStylesheet(String stylesheet)
265 _stylesheet = Resource.newResource(stylesheet);
266 if(!_stylesheet.exists())
268 LOG.warn("unable to find custom stylesheet: " + stylesheet);
274 LOG.warn(e.toString());
276 throw new IllegalArgumentException(stylesheet);
280 /* ------------------------------------------------------------ */
282 * @return the cacheControl header to set on all static content.
284 public String getCacheControl()
286 return _cacheControl;
289 /* ------------------------------------------------------------ */
291 * @param cacheControl the cacheControl header to set on all static content.
293 public void setCacheControl(String cacheControl)
295 _cacheControl=cacheControl;
298 /* ------------------------------------------------------------ */
301 public Resource getResource(String path) throws MalformedURLException
303 if (path==null || !path.startsWith("/"))
304 throw new MalformedURLException(path);
306 if (LOG.isDebugEnabled())
307 LOG.debug("{} getResource({})",_context==null?_baseResource:_context,_baseResource,path);
309 Resource base = _baseResource;
314 return _context.getResource(path);
319 path=URIUtil.canonicalPath(path);
320 Resource r = base.addPath(path);
322 if (r!=null && r.getAlias()!=null && (_context==null || !_context.checkAlias(path, r)))
324 if (LOG.isDebugEnabled())
325 LOG.debug("resource={} alias={}",r,r.getAlias());
338 /* ------------------------------------------------------------ */
339 protected Resource getResource(HttpServletRequest request) throws MalformedURLException
343 Boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
344 if (included != null && included.booleanValue())
346 servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
347 pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
349 if (servletPath == null && pathInfo == null)
351 servletPath = request.getServletPath();
352 pathInfo = request.getPathInfo();
357 servletPath = request.getServletPath();
358 pathInfo = request.getPathInfo();
361 String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
362 return getResource(pathInContext);
366 /* ------------------------------------------------------------ */
367 public String[] getWelcomeFiles()
369 return _welcomeFiles;
372 /* ------------------------------------------------------------ */
373 public void setWelcomeFiles(String[] welcomeFiles)
375 _welcomeFiles=welcomeFiles;
378 /* ------------------------------------------------------------ */
379 protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException
381 for (int i=0;i<_welcomeFiles.length;i++)
383 Resource welcome=directory.addPath(_welcomeFiles[i]);
384 if (welcome.exists() && !welcome.isDirectory())
391 /* ------------------------------------------------------------ */
393 * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
396 public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
398 if (baseRequest.isHandled())
401 boolean skipContentBody = false;
403 if(!HttpMethod.GET.is(request.getMethod()))
405 if(!HttpMethod.HEAD.is(request.getMethod()))
407 //try another handler
408 super.handle(target, baseRequest, request, response);
411 skipContentBody = true;
414 Resource resource = getResource(request);
416 if (LOG.isDebugEnabled())
419 LOG.debug("resource=null");
421 LOG.debug("resource={} alias={} exists={}",resource,resource.getAlias(),resource.exists());
425 // If resource is not found
426 if (resource==null || !resource.exists())
428 // inject the jetty-dir.css file if it matches
429 if (target.endsWith("/jetty-dir.css"))
431 resource = getStylesheet();
434 response.setContentType("text/css");
438 //no resource - try other handlers
439 super.handle(target, baseRequest, request, response);
444 // We are going to serve something
445 baseRequest.setHandled(true);
447 // handle directories
448 if (resource.isDirectory())
450 String pathInfo = request.getPathInfo();
451 boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
454 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)));
458 Resource welcome=getWelcome(resource);
459 if (welcome!=null && welcome.exists())
463 doDirectory(request,response,resource);
464 baseRequest.setHandled(true);
470 long last_modified=resource.lastModified();
474 // simple handling of only a single etag
475 String ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
476 etag=resource.getWeakETag();
477 if (ifnm!=null && resource!=null && ifnm.equals(etag))
479 response.setStatus(HttpStatus.NOT_MODIFIED_304);
480 baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
485 // Handle if modified since
488 long if_modified=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
489 if (if_modified>0 && last_modified/1000<=if_modified/1000)
491 response.setStatus(HttpStatus.NOT_MODIFIED_304);
497 String mime=_mimeTypes.getMimeByExtension(resource.toString());
499 mime=_mimeTypes.getMimeByExtension(request.getPathInfo());
500 doResponseHeaders(response,resource,mime);
502 baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
509 OutputStream out =null;
510 try {out = response.getOutputStream();}
511 catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());}
513 // Has the output been wrapped
514 if (!(out instanceof HttpOutput))
515 // Write content via wrapped output
516 resource.writeTo(out,0,resource.length());
519 // select async by size
520 int min_async_size=_minAsyncContentLength==0?response.getBufferSize():_minAsyncContentLength;
522 if (request.isAsyncSupported() &&
524 resource.length()>=min_async_size)
526 final AsyncContext async = request.startAsync();
528 Callback callback = new Callback()
531 public void succeeded()
537 public void failed(Throwable x)
539 LOG.warn(x.toString());
545 // Can we use a memory mapped file?
546 if (_minMemoryMappedContentLength>0 &&
547 resource.length()>_minMemoryMappedContentLength &&
548 resource.length()<Integer.MAX_VALUE &&
549 resource instanceof FileResource)
551 ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
552 ((HttpOutput)out).sendContent(buffer,callback);
554 else // Do a blocking write of a channel (if available) or input stream
556 // Close of the channel/inputstream is done by the async sendContent
557 ReadableByteChannel channel= resource.getReadableByteChannel();
559 ((HttpOutput)out).sendContent(channel,callback);
561 ((HttpOutput)out).sendContent(resource.getInputStream(),callback);
566 // Can we use a memory mapped file?
567 if (_minMemoryMappedContentLength>0 &&
568 resource.length()>_minMemoryMappedContentLength &&
569 resource instanceof FileResource)
571 ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
572 ((HttpOutput)out).sendContent(buffer);
574 else // Do a blocking write of a channel (if available) or input stream
576 ReadableByteChannel channel= resource.getReadableByteChannel();
578 ((HttpOutput)out).sendContent(channel);
580 ((HttpOutput)out).sendContent(resource.getInputStream());
586 /* ------------------------------------------------------------ */
587 protected void doDirectory(HttpServletRequest request,HttpServletResponse response, Resource resource)
592 String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
593 response.setContentType("text/html; charset=UTF-8");
594 response.getWriter().println(listing);
597 response.sendError(HttpStatus.FORBIDDEN_403);
600 /* ------------------------------------------------------------ */
601 /** Set the response headers.
602 * This method is called to set the response headers such as content type and content length.
603 * May be extended to add additional headers.
608 protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType)
611 response.setContentType(mimeType);
613 long length=resource.length();
615 if (response instanceof Response)
617 HttpFields fields = ((Response)response).getHttpFields();
620 ((Response)response).setLongContentLength(length);
622 if (_cacheControl!=null)
623 fields.put(HttpHeader.CACHE_CONTROL,_cacheControl);
627 if (length>Integer.MAX_VALUE)
628 response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(length));
630 response.setContentLength((int)length);
632 if (_cacheControl!=null)
633 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl);