View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server.handler;
20  
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;
26  
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;
32  
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;
50  
51  
52  /* ------------------------------------------------------------ */
53  /** Resource Handler.
54   *
55   * This handle will serve static content and handle If-Modified-Since headers.
56   * No caching is done.
57   * Requests for resources that do not exist are let pass (Eg no 404's).
58   *
59   *
60   * @org.apache.xbean.XBean
61   */
62  public class ResourceHandler extends HandlerWrapper
63  {
64      private static final Logger LOG = Log.getLogger(ResourceHandler.class);
65  
66      ContextHandler _context;
67      Resource _baseResource;
68      Resource _defaultStylesheet;
69      Resource _stylesheet;
70      String[] _welcomeFiles={"index.html"};
71      MimeTypes _mimeTypes = new MimeTypes();
72      String _cacheControl;
73      boolean _directory;
74      boolean _etags;
75      int _minMemoryMappedContentLength=-1;
76      int _minAsyncContentLength=0;
77  
78      /* ------------------------------------------------------------ */
79      public ResourceHandler()
80      {
81  
82      }
83  
84      /* ------------------------------------------------------------ */
85      public MimeTypes getMimeTypes()
86      {
87          return _mimeTypes;
88      }
89  
90      /* ------------------------------------------------------------ */
91      public void setMimeTypes(MimeTypes mimeTypes)
92      {
93          _mimeTypes = mimeTypes;
94      }
95  
96      /* ------------------------------------------------------------ */
97      /** Get the directory option.
98       * @return true if directories are listed.
99       */
100     public boolean isDirectoriesListed()
101     {
102         return _directory;
103     }
104 
105     /* ------------------------------------------------------------ */
106     /** Set the directory.
107      * @param directory true if directories are listed.
108      */
109     public void setDirectoriesListed(boolean directory)
110     {
111         _directory = directory;
112     }
113 
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
118      * buffers.
119      */
120     public int getMinMemoryMappedContentLength()
121     {
122         return _minMemoryMappedContentLength;
123     }
124 
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
129      * buffers.
130      */
131     public void setMinMemoryMappedContentLength(int minMemoryMappedFileSize)
132     {
133         _minMemoryMappedContentLength = minMemoryMappedFileSize;
134     }
135 
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.
141      */
142     public int getMinAsyncContentLength()
143     {
144         return _minAsyncContentLength;
145     }
146 
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.
152      */
153     public void setMinAsyncContentLength(int minAsyncContentLength)
154     {
155         _minAsyncContentLength = minAsyncContentLength;
156     }
157 
158     /* ------------------------------------------------------------ */
159     /**
160      * @return True if ETag processing is done
161      */
162     public boolean isEtags()
163     {
164         return _etags;
165     }
166 
167     /* ------------------------------------------------------------ */
168     /**
169      * @param etags True if ETag processing is done
170      */
171     public void setEtags(boolean etags)
172     {
173         _etags = etags;
174     }
175 
176     /* ------------------------------------------------------------ */
177     @Override
178     public void doStart()
179     throws Exception
180     {
181         Context scontext = ContextHandler.getCurrentContext();
182         _context = (scontext==null?null:scontext.getContextHandler());
183 
184         super.doStart();
185     }
186 
187     /* ------------------------------------------------------------ */
188     /**
189      * @return Returns the resourceBase.
190      */
191     public Resource getBaseResource()
192     {
193         if (_baseResource==null)
194             return null;
195         return _baseResource;
196     }
197 
198     /* ------------------------------------------------------------ */
199     /**
200      * @return Returns the base resource as a string.
201      */
202     public String getResourceBase()
203     {
204         if (_baseResource==null)
205             return null;
206         return _baseResource.toString();
207     }
208 
209 
210     /* ------------------------------------------------------------ */
211     /**
212      * @param base The resourceBase to set.
213      */
214     public void setBaseResource(Resource base)
215     {
216         _baseResource=base;
217     }
218 
219     /* ------------------------------------------------------------ */
220     /**
221      * @param resourceBase The base resource as a string.
222      */
223     public void setResourceBase(String resourceBase)
224     {
225         try
226         {
227             setBaseResource(Resource.newResource(resourceBase));
228         }
229         catch (Exception e)
230         {
231             LOG.warn(e.toString());
232             LOG.debug(e);
233             throw new IllegalArgumentException(resourceBase);
234         }
235     }
236 
237     /* ------------------------------------------------------------ */
238     /**
239      * @return Returns the stylesheet as a Resource.
240      */
241     public Resource getStylesheet()
242     {
243     	if(_stylesheet != null)
244     	{
245     	    return _stylesheet;
246     	}
247     	else
248     	{
249     	    if(_defaultStylesheet == null)
250     	    {
251     	        try
252     	        {
253     	            _defaultStylesheet =  Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
254     	        }
255     	        catch(IOException e)
256     	        {
257     	            LOG.warn(e.toString());
258     	            LOG.debug(e);
259     	        }
260     	    }
261     	    return _defaultStylesheet;
262     	}
263     }
264 
265     /* ------------------------------------------------------------ */
266     /**
267      * @param stylesheet The location of the stylesheet to be used as a String.
268      */
269     public void setStylesheet(String stylesheet)
270     {
271         try
272         {
273             _stylesheet = Resource.newResource(stylesheet);
274             if(!_stylesheet.exists())
275             {
276                 LOG.warn("unable to find custom stylesheet: " + stylesheet);
277                 _stylesheet = null;
278             }
279         }
280     	catch(Exception e)
281     	{
282     		LOG.warn(e.toString());
283             LOG.debug(e);
284             throw new IllegalArgumentException(stylesheet);
285     	}
286     }
287 
288     /* ------------------------------------------------------------ */
289     /**
290      * @return the cacheControl header to set on all static content.
291      */
292     public String getCacheControl()
293     {
294         return _cacheControl;
295     }
296 
297     /* ------------------------------------------------------------ */
298     /**
299      * @param cacheControl the cacheControl header to set on all static content.
300      */
301     public void setCacheControl(String cacheControl)
302     {
303         _cacheControl=cacheControl;
304     }
305 
306     /* ------------------------------------------------------------ */
307     /*
308      */
309     public Resource getResource(String path) throws MalformedURLException
310     {
311         if (path==null || !path.startsWith("/"))
312             throw new MalformedURLException(path);
313 
314         Resource base = _baseResource;
315         if (base==null)
316         {
317             if (_context==null)
318                 return null;
319             base=_context.getBaseResource();
320             if (base==null)
321                 return null;
322         }
323 
324         try
325         {
326             path=URIUtil.canonicalPath(path);
327             return base.addPath(path);
328         }
329         catch(Exception e)
330         {
331             LOG.ignore(e);
332         }
333 
334         return null;
335     }
336 
337     /* ------------------------------------------------------------ */
338     protected Resource getResource(HttpServletRequest request) throws MalformedURLException
339     {
340         String servletPath;
341         String pathInfo;
342         Boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
343         if (included != null && included.booleanValue())
344         {
345             servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
346             pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
347 
348             if (servletPath == null && pathInfo == null)
349             {
350                 servletPath = request.getServletPath();
351                 pathInfo = request.getPathInfo();
352             }
353         }
354         else
355         {
356             servletPath = request.getServletPath();
357             pathInfo = request.getPathInfo();
358         }
359 
360         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
361         return getResource(pathInContext);
362     }
363 
364 
365     /* ------------------------------------------------------------ */
366     public String[] getWelcomeFiles()
367     {
368         return _welcomeFiles;
369     }
370 
371     /* ------------------------------------------------------------ */
372     public void setWelcomeFiles(String[] welcomeFiles)
373     {
374         _welcomeFiles=welcomeFiles;
375     }
376 
377     /* ------------------------------------------------------------ */
378     protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException
379     {
380         for (int i=0;i<_welcomeFiles.length;i++)
381         {
382             Resource welcome=directory.addPath(_welcomeFiles[i]);
383             if (welcome.exists() && !welcome.isDirectory())
384                 return welcome;
385         }
386 
387         return null;
388     }
389 
390     /* ------------------------------------------------------------ */
391     /*
392      * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
393      */
394     @Override
395     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
396     {
397         if (baseRequest.isHandled())
398             return;
399 
400         boolean skipContentBody = false;
401 
402         if(!HttpMethod.GET.is(request.getMethod()))
403         {
404             if(!HttpMethod.HEAD.is(request.getMethod()))
405             {
406                 //try another handler
407                 super.handle(target, baseRequest, request, response);
408                 return;
409             }
410             skipContentBody = true;
411         }
412 
413         Resource resource = getResource(request);
414         // If resource is not found
415         if (resource==null || !resource.exists())
416         {
417             // inject the jetty-dir.css file if it matches
418             if (target.endsWith("/jetty-dir.css"))
419             {
420                 resource = getStylesheet();
421                 if (resource==null)
422                     return;
423                 response.setContentType("text/css");
424             }
425             else
426             {
427                 //no resource - try other handlers
428                 super.handle(target, baseRequest, request, response);
429                 return;
430             }
431         }
432 
433         // We are going to serve something
434         baseRequest.setHandled(true);
435 
436         // handle directories
437         if (resource.isDirectory())
438         {
439             if (!request.getPathInfo().endsWith(URIUtil.SLASH))
440             {
441                 response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)));
442                 return;
443             }
444 
445             Resource welcome=getWelcome(resource);
446             if (welcome!=null && welcome.exists())
447                 resource=welcome;
448             else
449             {
450                 doDirectory(request,response,resource);
451                 baseRequest.setHandled(true);
452                 return;
453             }
454         }
455 
456         // Handle ETAGS
457         long last_modified=resource.lastModified();
458         String etag=null;
459         if (_etags)
460         {
461             // simple handling of only a single etag
462             String ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
463             etag=resource.getWeakETag();
464             if (ifnm!=null && resource!=null && ifnm.equals(etag))
465             {
466                 response.setStatus(HttpStatus.NOT_MODIFIED_304);
467                 baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
468                 return;
469             }
470         }
471         
472         // Handle if modified since 
473         if (last_modified>0)
474         {
475             long if_modified=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
476             if (if_modified>0 && last_modified/1000<=if_modified/1000)
477             {
478                 response.setStatus(HttpStatus.NOT_MODIFIED_304);
479                 return;
480             }
481         }
482 
483         // set the headers
484         String mime=_mimeTypes.getMimeByExtension(resource.toString());
485         if (mime==null)
486             mime=_mimeTypes.getMimeByExtension(request.getPathInfo());
487         doResponseHeaders(response,resource,mime);
488         if (_etags)
489             baseRequest.getResponse().getHttpFields().put(HttpHeader.ETAG,etag);
490         
491         if(skipContentBody)
492             return;
493         
494         
495         // Send the content
496         OutputStream out =null;
497         try {out = response.getOutputStream();}
498         catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());}
499 
500         // Has the output been wrapped
501         if (!(out instanceof HttpOutput))
502             // Write content via wrapped output
503             resource.writeTo(out,0,resource.length());
504         else
505         {
506             // select async by size
507             int min_async_size=_minAsyncContentLength==0?response.getBufferSize():_minAsyncContentLength;
508             
509             if (request.isAsyncSupported() && 
510                 min_async_size>0 &&
511                 resource.length()>=min_async_size)
512             {
513                 final AsyncContext async = request.startAsync();
514                 Callback callback = new Callback()
515                 {
516                     @Override
517                     public void succeeded()
518                     {
519                         async.complete();
520                     }
521 
522                     @Override
523                     public void failed(Throwable x)
524                     {
525                         LOG.warn(x.toString());
526                         LOG.debug(x);
527                         async.complete();
528                     }   
529                 };
530 
531                 // Can we use a memory mapped file?
532                 if (_minMemoryMappedContentLength>0 && 
533                     resource.length()>_minMemoryMappedContentLength &&
534                     resource instanceof FileResource)
535                 {
536                     ByteBuffer buffer = BufferUtil.toBuffer(resource.getFile());
537                     ((HttpOutput)out).sendContent(buffer,callback);
538                 }
539                 else  // Do a blocking write of a channel (if available) or input stream
540                 {
541                     // Close of the channel/inputstream is done by the async sendContent
542                     ReadableByteChannel channel= resource.getReadableByteChannel();
543                     if (channel!=null)
544                         ((HttpOutput)out).sendContent(channel,callback);
545                     else
546                         ((HttpOutput)out).sendContent(resource.getInputStream(),callback);
547                 }
548             }
549             else
550             {
551                 // Can we use a memory mapped file?
552                 if (_minMemoryMappedContentLength>0 && 
553                     resource.length()>_minMemoryMappedContentLength &&
554                     resource instanceof FileResource)
555                 {
556                     ByteBuffer buffer = BufferUtil.toBuffer(resource.getFile());
557                     ((HttpOutput)out).sendContent(buffer);
558                 }
559                 else  // Do a blocking write of a channel (if available) or input stream
560                 {
561                     ReadableByteChannel channel= resource.getReadableByteChannel();
562                     if (channel!=null)
563                         ((HttpOutput)out).sendContent(channel);
564                     else
565                         ((HttpOutput)out).sendContent(resource.getInputStream());
566                 }
567             }
568         }
569     }
570 
571     /* ------------------------------------------------------------ */
572     protected void doDirectory(HttpServletRequest request,HttpServletResponse response, Resource resource)
573         throws IOException
574     {
575         if (_directory)
576         {
577             String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
578             response.setContentType("text/html; charset=UTF-8");
579             response.getWriter().println(listing);
580         }
581         else
582             response.sendError(HttpStatus.FORBIDDEN_403);
583     }
584 
585     /* ------------------------------------------------------------ */
586     /** Set the response headers.
587      * This method is called to set the response headers such as content type and content length.
588      * May be extended to add additional headers.
589      * @param response
590      * @param resource
591      * @param mimeType
592      */
593     protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType)
594     {
595         if (mimeType!=null)
596             response.setContentType(mimeType);
597 
598         long length=resource.length();
599 
600         if (response instanceof Response)
601         {
602             HttpFields fields = ((Response)response).getHttpFields();
603 
604             if (length>0)
605                 ((Response)response).setLongContentLength(length);
606 
607             if (_cacheControl!=null)
608                 fields.put(HttpHeader.CACHE_CONTROL,_cacheControl);
609         }
610         else
611         {
612             if (length>Integer.MAX_VALUE)
613                 response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(length));
614             else if (length>0)
615                 response.setContentLength((int)length);
616 
617             if (_cacheControl!=null)
618                 response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl);
619         }
620     }
621 }