View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.servlet;
20  
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.net.MalformedURLException;
26  import java.net.URL;
27  import java.nio.ByteBuffer;
28  import java.util.Enumeration;
29  import java.util.List;
30  
31  import javax.servlet.AsyncContext;
32  import javax.servlet.RequestDispatcher;
33  import javax.servlet.ServletContext;
34  import javax.servlet.ServletException;
35  import javax.servlet.UnavailableException;
36  import javax.servlet.http.HttpServlet;
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  
40  import org.eclipse.jetty.http.HttpContent;
41  import org.eclipse.jetty.http.HttpField;
42  import org.eclipse.jetty.http.HttpFields;
43  import org.eclipse.jetty.http.HttpGenerator.CachedHttpField;
44  import org.eclipse.jetty.http.HttpHeader;
45  import org.eclipse.jetty.http.HttpMethod;
46  import org.eclipse.jetty.http.MimeTypes;
47  import org.eclipse.jetty.http.PathMap.MappedEntry;
48  import org.eclipse.jetty.io.WriterOutputStream;
49  import org.eclipse.jetty.server.HttpOutput;
50  import org.eclipse.jetty.server.InclusiveByteRange;
51  import org.eclipse.jetty.server.ResourceCache;
52  import org.eclipse.jetty.server.Response;
53  import org.eclipse.jetty.server.handler.ContextHandler;
54  import org.eclipse.jetty.util.BufferUtil;
55  import org.eclipse.jetty.util.Callback;
56  import org.eclipse.jetty.util.IO;
57  import org.eclipse.jetty.util.MultiPartOutputStream;
58  import org.eclipse.jetty.util.QuotedStringTokenizer;
59  import org.eclipse.jetty.util.URIUtil;
60  import org.eclipse.jetty.util.log.Log;
61  import org.eclipse.jetty.util.log.Logger;
62  import org.eclipse.jetty.util.resource.Resource;
63  import org.eclipse.jetty.util.resource.ResourceCollection;
64  import org.eclipse.jetty.util.resource.ResourceFactory;
65  
66  
67  
68  /* ------------------------------------------------------------ */
69  /** The default servlet.
70   * This servlet, normally mapped to /, provides the handling for static
71   * content, OPTION and TRACE methods for the context.
72   * The following initParameters are supported, these can be set either
73   * on the servlet itself or as ServletContext initParameters with a prefix
74   * of org.eclipse.jetty.servlet.Default. :
75   * <PRE>
76   *  acceptRanges      If true, range requests and responses are
77   *                    supported
78   *
79   *  dirAllowed        If true, directory listings are returned if no
80   *                    welcome file is found. Else 403 Forbidden.
81   *
82   *  welcomeServlets   If true, attempt to dispatch to welcome files
83   *                    that are servlets, but only after no matching static
84   *                    resources could be found. If false, then a welcome
85   *                    file must exist on disk. If "exact", then exact
86   *                    servlet matches are supported without an existing file.
87   *                    Default is true.
88   *
89   *                    This must be false if you want directory listings,
90   *                    but have index.jsp in your welcome file list.
91   *
92   *  redirectWelcome   If true, welcome files are redirected rather than
93   *                    forwarded to.
94   *
95   *  gzip              If set to true, then static content will be served as
96   *                    gzip content encoded if a matching resource is
97   *                    found ending with ".gz"
98   *
99   *  resourceBase      Set to replace the context resource base
100  *
101  *  resourceCache     If set, this is a context attribute name, which the servlet
102  *                    will use to look for a shared ResourceCache instance.
103  *
104  *  relativeResourceBase
105  *                    Set with a pathname relative to the base of the
106  *                    servlet context root. Useful for only serving static content out
107  *                    of only specific subdirectories.
108  *
109  *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
110  *
111  *  stylesheet	      Set with the location of an optional stylesheet that will be used
112  *                    to decorate the directory listing html.
113  *
114  *  etags             If True, weak etags will be generated and handled.
115  *
116  *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
117  *  maxCachedFileSize The maximum size of a file to cache
118  *  maxCachedFiles    The maximum number of files to cache
119  *
120  *  useFileMappedBuffer
121  *                    If set to true, it will use mapped file buffer to serve static content
122  *                    when using NIO connector. Setting this value to false means that
123  *                    a direct buffer will be used instead of a mapped file buffer.
124  *                    By default, this is set to true.
125  *
126  *  cacheControl      If set, all static content will have this value set as the cache-control
127  *                    header.
128  *
129  *
130  * </PRE>
131  *
132  *
133  *
134  *
135  */
136 public class DefaultServlet extends HttpServlet implements ResourceFactory
137 {
138     private static final Logger LOG = Log.getLogger(DefaultServlet.class);
139 
140     private static final long serialVersionUID = 4930458713846881193L;
141     
142     private static final CachedHttpField ACCEPT_RANGES = new CachedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
143     
144     private ServletContext _servletContext;
145     private ContextHandler _contextHandler;
146 
147     private boolean _acceptRanges=true;
148     private boolean _dirAllowed=true;
149     private boolean _welcomeServlets=false;
150     private boolean _welcomeExactServlets=false;
151     private boolean _redirectWelcome=false;
152     private boolean _gzip=false;
153     private boolean _pathInfoOnly=false;
154     private boolean _etags=false;
155 
156     private Resource _resourceBase;
157     private ResourceCache _cache;
158 
159     private MimeTypes _mimeTypes;
160     private String[] _welcomes;
161     private Resource _stylesheet;
162     private boolean _useFileMappedBuffer=false;
163     private HttpField _cacheControl;
164     private String _relativeResourceBase;
165     private ServletHandler _servletHandler;
166     private ServletHolder _defaultHolder;
167 
168     /* ------------------------------------------------------------ */
169     @Override
170     public void init()
171     throws UnavailableException
172     {
173         _servletContext=getServletContext();
174         _contextHandler = initContextHandler(_servletContext);
175 
176         _mimeTypes = _contextHandler.getMimeTypes();
177 
178         _welcomes = _contextHandler.getWelcomeFiles();
179         if (_welcomes==null)
180             _welcomes=new String[] {"index.html","index.jsp"};
181 
182         _acceptRanges=getInitBoolean("acceptRanges",_acceptRanges);
183         _dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
184         _redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
185         _gzip=getInitBoolean("gzip",_gzip);
186         _pathInfoOnly=getInitBoolean("pathInfoOnly",_pathInfoOnly);
187 
188         if ("exact".equals(getInitParameter("welcomeServlets")))
189         {
190             _welcomeExactServlets=true;
191             _welcomeServlets=false;
192         }
193         else
194             _welcomeServlets=getInitBoolean("welcomeServlets", _welcomeServlets);
195 
196         _useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);
197 
198         _relativeResourceBase = getInitParameter("relativeResourceBase");
199 
200         String rb=getInitParameter("resourceBase");
201         if (rb!=null)
202         {
203             if (_relativeResourceBase!=null)
204                 throw new  UnavailableException("resourceBase & relativeResourceBase");
205             try{_resourceBase=_contextHandler.newResource(rb);}
206             catch (Exception e)
207             {
208                 LOG.warn(Log.EXCEPTION,e);
209                 throw new UnavailableException(e.toString());
210             }
211         }
212 
213         String css=getInitParameter("stylesheet");
214         try
215         {
216             if(css!=null)
217             {
218                 _stylesheet = Resource.newResource(css);
219                 if(!_stylesheet.exists())
220                 {
221                     LOG.warn("!" + css);
222                     _stylesheet = null;
223                 }
224             }
225             if(_stylesheet == null)
226             {
227                 _stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
228             }
229         }
230         catch(Exception e)
231         {
232             LOG.warn(e.toString());
233             LOG.debug(e);
234         }
235 
236         String cc=getInitParameter("cacheControl");
237         if (cc!=null)
238             _cacheControl=new CachedHttpField(HttpHeader.CACHE_CONTROL, cc);
239         
240         String resourceCache = getInitParameter("resourceCache");
241         int max_cache_size=getInitInt("maxCacheSize", -2);
242         int max_cached_file_size=getInitInt("maxCachedFileSize", -2);
243         int max_cached_files=getInitInt("maxCachedFiles", -2);
244         if (resourceCache!=null)
245         {
246             if (max_cache_size!=-1 || max_cached_file_size!= -2 || max_cached_files!=-2)
247                 LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
248             if (_relativeResourceBase!=null || _resourceBase!=null)
249                 throw new UnavailableException("resourceCache specified with resource bases");
250             _cache=(ResourceCache)_servletContext.getAttribute(resourceCache);
251 
252             LOG.debug("Cache {}={}",resourceCache,_cache);
253         }
254 
255         _etags = getInitBoolean("etags",_etags);
256         
257         try
258         {
259             if (_cache==null && max_cached_files>0)
260             {
261                 _cache= new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags);
262 
263                 if (max_cache_size>0)
264                     _cache.setMaxCacheSize(max_cache_size);
265                 if (max_cached_file_size>=-1)
266                     _cache.setMaxCachedFileSize(max_cached_file_size);
267                 if (max_cached_files>=-1)
268                     _cache.setMaxCachedFiles(max_cached_files);
269             }
270         }
271         catch (Exception e)
272         {
273             LOG.warn(Log.EXCEPTION,e);
274             throw new UnavailableException(e.toString());
275         }
276 
277         _servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class);
278         for (ServletHolder h :_servletHandler.getServlets())
279             if (h.getServletInstance()==this)
280                 _defaultHolder=h;
281 
282         
283         if (LOG.isDebugEnabled())
284             LOG.debug("resource base = "+_resourceBase);
285     }
286 
287     /**
288      * Compute the field _contextHandler.<br/>
289      * In the case where the DefaultServlet is deployed on the HttpService it is likely that
290      * this method needs to be overwritten to unwrap the ServletContext facade until we reach
291      * the original jetty's ContextHandler.
292      * @param servletContext The servletContext of this servlet.
293      * @return the jetty's ContextHandler for this servletContext.
294      */
295     protected ContextHandler initContextHandler(ServletContext servletContext)
296     {
297         ContextHandler.Context scontext=ContextHandler.getCurrentContext();
298         if (scontext==null)
299         {
300             if (servletContext instanceof ContextHandler.Context)
301                 return ((ContextHandler.Context)servletContext).getContextHandler();
302             else
303                 throw new IllegalArgumentException("The servletContext " + servletContext + " " +
304                     servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
305         }
306         else
307             return ContextHandler.getCurrentContext().getContextHandler();
308     }
309 
310     /* ------------------------------------------------------------ */
311     @Override
312     public String getInitParameter(String name)
313     {
314         String value=getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default."+name);
315         if (value==null)
316             value=super.getInitParameter(name);
317         return value;
318     }
319 
320     /* ------------------------------------------------------------ */
321     private boolean getInitBoolean(String name, boolean dft)
322     {
323         String value=getInitParameter(name);
324         if (value==null || value.length()==0)
325             return dft;
326         return (value.startsWith("t")||
327                 value.startsWith("T")||
328                 value.startsWith("y")||
329                 value.startsWith("Y")||
330                 value.startsWith("1"));
331     }
332 
333     /* ------------------------------------------------------------ */
334     private int getInitInt(String name, int dft)
335     {
336         String value=getInitParameter(name);
337         if (value==null)
338             value=getInitParameter(name);
339         if (value!=null && value.length()>0)
340             return Integer.parseInt(value);
341         return dft;
342     }
343 
344     /* ------------------------------------------------------------ */
345     /** get Resource to serve.
346      * Map a path to a resource. The default implementation calls
347      * HttpContext.getResource but derived servlets may provide
348      * their own mapping.
349      * @param pathInContext The path to find a resource for.
350      * @return The resource to serve.
351      */
352     @Override
353     public Resource getResource(String pathInContext)
354     {
355         Resource r=null;
356         if (_relativeResourceBase!=null)
357             pathInContext=URIUtil.addPaths(_relativeResourceBase,pathInContext);
358 
359         try
360         {
361             if (_resourceBase!=null)
362             {
363                 r = _resourceBase.addPath(pathInContext);
364                 if (!_contextHandler.checkAlias(pathInContext,r))
365                     r=null;
366             }
367             else if (_servletContext instanceof ContextHandler.Context)
368             {
369                 r = _contextHandler.getResource(pathInContext);
370             }
371             else
372             {
373                 URL u = _servletContext.getResource(pathInContext);
374                 r = _contextHandler.newResource(u);
375             }
376 
377             if (LOG.isDebugEnabled())
378                 LOG.debug("Resource "+pathInContext+"="+r);
379         }
380         catch (IOException e)
381         {
382             LOG.ignore(e);
383         }
384 
385         if((r==null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
386             r=_stylesheet;
387 
388         return r;
389     }
390 
391     /* ------------------------------------------------------------ */
392     @Override
393     protected void doGet(HttpServletRequest request, HttpServletResponse response)
394     throws ServletException, IOException
395     {
396         String servletPath=null;
397         String pathInfo=null;
398         Enumeration<String> reqRanges = null;
399         Boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null;
400         if (included!=null && included.booleanValue())
401         {
402             servletPath=(String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
403             pathInfo=(String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
404             if (servletPath==null)
405             {
406                 servletPath=request.getServletPath();
407                 pathInfo=request.getPathInfo();
408             }
409         }
410         else
411         {
412             included = Boolean.FALSE;
413             servletPath = _pathInfoOnly?"/":request.getServletPath();
414             pathInfo = request.getPathInfo();
415 
416             // Is this a Range request?
417             reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
418             if (!hasDefinedRange(reqRanges))
419                 reqRanges = null;
420         }
421 
422         String pathInContext=URIUtil.addPaths(servletPath,pathInfo);
423         boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH);
424 
425 
426         // Find the resource and content
427         Resource resource=null;
428         HttpContent content=null;
429         try
430         {
431             // is gzip enabled?
432             String pathInContextGz=null;
433             boolean gzip=false;
434             if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash )
435             {
436                 // Look for a gzip resource
437                 pathInContextGz=pathInContext+".gz";
438                 if (_cache==null)
439                     resource=getResource(pathInContextGz);
440                 else
441                 {
442                     content=_cache.lookup(pathInContextGz);
443                     resource=(content==null)?null:content.getResource();
444                 }
445 
446                 // Does a gzip resource exist?
447                 if (resource!=null && resource.exists() && !resource.isDirectory())
448                 {
449                     // Tell caches that response may vary by accept-encoding
450                     response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString());
451                     
452                     // Does the client accept gzip?
453                     String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
454                     if (accept!=null && accept.indexOf("gzip")>=0)
455                         gzip=true;
456                 }
457             }
458 
459             // find resource
460             if (!gzip)
461             {
462                 if (_cache==null)
463                     resource=getResource(pathInContext);
464                 else
465                 {
466                     content=_cache.lookup(pathInContext);
467                     resource=content==null?null:content.getResource();
468                 }
469             }
470 
471             if (LOG.isDebugEnabled())
472                 LOG.debug("uri="+request.getRequestURI()+" resource="+resource+(content!=null?" content":""));
473 
474             // Handle resource
475             if (resource==null || !resource.exists())
476             {
477                 if (included)
478                     throw new FileNotFoundException("!" + pathInContext);
479                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
480             }
481             else if (!resource.isDirectory())
482             {
483                 if (endsWithSlash && pathInContext.length()>1)
484                 {
485                     String q=request.getQueryString();
486                     pathInContext=pathInContext.substring(0,pathInContext.length()-1);
487                     if (q!=null&&q.length()!=0)
488                         pathInContext+="?"+q;
489                     response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext)));
490                 }
491                 else
492                 {
493                     // ensure we have content
494                     if (content==null)
495                         content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),response.getBufferSize(),_etags);
496 
497                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
498                     {
499                         if (gzip)
500                         {
501                             response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip");
502                             String mt=_servletContext.getMimeType(pathInContext);
503                             if (mt!=null)
504                                 response.setContentType(mt);
505                         }
506                         sendData(request,response,included.booleanValue(),resource,content,reqRanges);
507                     }
508                 }
509             }
510             else
511             {
512                 String welcome=null;
513 
514                 if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null))
515                 {
516                     StringBuffer buf=request.getRequestURL();
517                     synchronized(buf)
518                     {
519                         int param=buf.lastIndexOf(";");
520                         if (param<0)
521                             buf.append('/');
522                         else
523                             buf.insert(param,'/');
524                         String q=request.getQueryString();
525                         if (q!=null&&q.length()!=0)
526                         {
527                             buf.append('?');
528                             buf.append(q);
529                         }
530                         response.setContentLength(0);
531                         response.sendRedirect(response.encodeRedirectURL(buf.toString()));
532                     }
533                 }
534                 // else look for a welcome file
535                 else if (null!=(welcome=getWelcomeFile(pathInContext)))
536                 {
537                     LOG.debug("welcome={}",welcome);
538                     if (_redirectWelcome)
539                     {
540                         // Redirect to the index
541                         response.setContentLength(0);
542                         String q=request.getQueryString();
543                         if (q!=null&&q.length()!=0)
544                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q));
545                         else
546                             response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)));
547                     }
548                     else
549                     {
550                         // Forward to the index
551                         RequestDispatcher dispatcher=request.getRequestDispatcher(welcome);
552                         if (dispatcher!=null)
553                         {
554                             if (included.booleanValue())
555                                 dispatcher.include(request,response);
556                             else
557                             {
558                                 request.setAttribute("org.eclipse.jetty.server.welcome",welcome);
559                                 dispatcher.forward(request,response);
560                             }
561                         }
562                     }
563                 }
564                 else
565                 {
566                     content=new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),_etags);
567                     if (included.booleanValue() || passConditionalHeaders(request,response, resource,content))
568                         sendDirectory(request,response,resource,pathInContext);
569                 }
570             }
571         }
572         catch(IllegalArgumentException e)
573         {
574             LOG.warn(Log.EXCEPTION,e);
575             if(!response.isCommitted())
576                 response.sendError(500, e.getMessage());
577         }
578         finally
579         {
580             if (content!=null)
581                 content.release();
582             else if (resource!=null)
583                 resource.close();
584         }
585 
586     }
587 
588     /* ------------------------------------------------------------ */
589     private boolean hasDefinedRange(Enumeration<String> reqRanges)
590     {
591         return (reqRanges!=null && reqRanges.hasMoreElements());
592     }
593 
594     /* ------------------------------------------------------------ */
595     @Override
596     protected void doPost(HttpServletRequest request, HttpServletResponse response)
597     throws ServletException, IOException
598     {
599         doGet(request,response);
600     }
601 
602     /* ------------------------------------------------------------ */
603     /* (non-Javadoc)
604      * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
605      */
606     @Override
607     protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
608     {
609         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
610     }
611 
612     /* ------------------------------------------------------------ */
613     @Override
614     protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
615     throws ServletException, IOException
616     {
617         resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
618     }
619 
620     /* ------------------------------------------------------------ */
621     /**
622      * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
623      * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
624      * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
625      * If there is none, then <code>null</code> is returned.
626      * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
627      * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
628      * @param resource
629      * @return The path of the matching welcome file in context or null.
630      * @throws IOException
631      * @throws MalformedURLException
632      */
633     private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
634     {
635         if (_welcomes==null)
636             return null;
637 
638         String welcome_servlet=null;
639         for (int i=0;i<_welcomes.length;i++)
640         {
641             String welcome_in_context=URIUtil.addPaths(pathInContext,_welcomes[i]);
642             Resource welcome=getResource(welcome_in_context);
643             if (welcome!=null && welcome.exists())
644                 return _welcomes[i];
645 
646             if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
647             {
648                 MappedEntry<?> entry=_servletHandler.getHolderEntry(welcome_in_context);
649                 if (entry!=null && entry.getValue()!=_defaultHolder &&
650                         (_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
651                     welcome_servlet=welcome_in_context;
652 
653             }
654         }
655         return welcome_servlet;
656     }
657 
658     /* ------------------------------------------------------------ */
659     /* Check modification date headers.
660      */
661     protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content)
662     throws IOException
663     {
664         try
665         {
666             if (!HttpMethod.HEAD.is(request.getMethod()))
667             {
668                 if (_etags)
669                 {
670                     String ifm=request.getHeader(HttpHeader.IF_MATCH.asString());
671                     if (ifm!=null)
672                     {
673                         boolean match=false;
674                         if (content.getETag()!=null)
675                         {
676                             QuotedStringTokenizer quoted = new QuotedStringTokenizer(ifm,", ",false,true);
677                             while (!match && quoted.hasMoreTokens())
678                             {
679                                 String tag = quoted.nextToken();
680                                 if (content.getETag().equals(tag))
681                                     match=true;
682                             }
683                         }
684 
685                         if (!match)
686                         {
687                             response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
688                             return false;
689                         }
690                     }
691                     
692                     String if_non_match_etag=request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
693                     if (if_non_match_etag!=null && content.getETag()!=null)
694                     {
695                         // Look for GzipFiltered version of etag
696                         if (content.getETag().equals(request.getAttribute("o.e.j.s.GzipFilter.ETag")))
697                         {
698                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
699                             response.setHeader(HttpHeader.ETAG.asString(),if_non_match_etag);
700                             return false;
701                         }
702                         
703                         // Handle special case of exact match.
704                         if (content.getETag().equals(if_non_match_etag))
705                         {
706                             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
707                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
708                             return false;
709                         }
710 
711                         // Handle list of tags
712                         QuotedStringTokenizer quoted = new QuotedStringTokenizer(if_non_match_etag,", ",false,true);
713                         while (quoted.hasMoreTokens())
714                         {
715                             String tag = quoted.nextToken();
716                             if (content.getETag().equals(tag))
717                             {
718                                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
719                                 response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
720                                 return false;
721                             }
722                         }
723                         
724                         // If etag requires content to be served, then do not check if-modified-since
725                         return true;
726                     }
727                 }
728                 
729                 // Handle if modified since
730                 String ifms=request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
731                 if (ifms!=null)
732                 {
733                     //Get jetty's Response impl
734                     String mdlm=content.getLastModified();
735                     if (mdlm!=null && ifms.equals(mdlm))
736                     {
737                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
738                         if (_etags)
739                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
740                         response.flushBuffer();
741                         return false;
742                     }
743 
744                     long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
745                     if (ifmsl!=-1 && resource.lastModified()/1000 <= ifmsl/1000)
746                     { 
747                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
748                         if (_etags)
749                             response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
750                         response.flushBuffer();
751                         return false;
752                     }
753                 }
754 
755                 // Parse the if[un]modified dates and compare to resource
756                 long date=request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
757                 if (date!=-1 && resource.lastModified()/1000 > date/1000)
758                 {
759                     response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
760                     return false;
761                 }
762 
763             }
764         }
765         catch(IllegalArgumentException iae)
766         {
767             if(!response.isCommitted())
768                 response.sendError(400, iae.getMessage());
769             throw iae;
770         }
771         return true;
772     }
773 
774 
775     /* ------------------------------------------------------------------- */
776     protected void sendDirectory(HttpServletRequest request,
777             HttpServletResponse response,
778             Resource resource,
779             String pathInContext)
780     throws IOException
781     {
782         if (!_dirAllowed)
783         {
784             response.sendError(HttpServletResponse.SC_FORBIDDEN);
785             return;
786         }
787 
788         byte[] data=null;
789         String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
790 
791         //If the DefaultServlet has a resource base set, use it
792         if (_resourceBase != null)
793         {
794             // handle ResourceCollection
795             if (_resourceBase instanceof ResourceCollection)
796                 resource=_resourceBase.addPath(pathInContext);
797         }
798         //Otherwise, try using the resource base of its enclosing context handler
799         else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
800             resource=_contextHandler.getBaseResource().addPath(pathInContext);
801 
802         String dir = resource.getListHTML(base,pathInContext.length()>1);
803         if (dir==null)
804         {
805             response.sendError(HttpServletResponse.SC_FORBIDDEN,
806             "No directory");
807             return;
808         }
809 
810         data=dir.getBytes("UTF-8");
811         response.setContentType("text/html; charset=UTF-8");
812         response.setContentLength(data.length);
813         response.getOutputStream().write(data);
814     }
815 
816     /* ------------------------------------------------------------ */
817     protected void sendData(HttpServletRequest request,
818             HttpServletResponse response,
819             boolean include,
820             Resource resource,
821             HttpContent content,
822             Enumeration<String> reqRanges)
823     throws IOException
824     {
825         final long content_length = (content==null)?resource.length():content.getContentLength();
826 
827         // Get the output stream (or writer)
828         OutputStream out =null;
829         boolean written;
830         try
831         {
832             out = response.getOutputStream();
833 
834             // has a filter already written to the response?
835             written = out instanceof HttpOutput
836                 ? ((HttpOutput)out).isWritten()
837                 : true;
838         }
839         catch(IllegalStateException e)
840         {
841             out = new WriterOutputStream(response.getWriter());
842             written=true; // there may be data in writer buffer, so assume written
843         }
844 
845         if ( reqRanges == null || !reqRanges.hasMoreElements() || content_length<0)
846         {
847             //  if there were no ranges, send entire entity
848             if (include)
849             {
850                 resource.writeTo(out,0,content_length);
851             }
852             // else if we can't do a bypass write because of wrapping
853             else if (content==null || written || !(out instanceof HttpOutput))
854             {
855                 // write normally
856                 writeHeaders(response,content,written?-1:content_length);
857                 ByteBuffer buffer = (content==null)?null:content.getIndirectBuffer();
858                 if (buffer!=null)
859                     BufferUtil.writeTo(buffer,out);
860                 else
861                     resource.writeTo(out,0,content_length);
862             }
863             // else do a bypass write
864             else
865             {
866                 // write the headers
867                 if (response instanceof Response)
868                 {
869                     Response r = (Response)response;
870                     writeOptionHeaders(r.getHttpFields());
871                     r.setHeaders(content);
872                 }
873                 else
874                     writeHeaders(response,content,content_length);
875 
876                 // write the content asynchronously if supported
877                 if (request.isAsyncSupported())
878                 {
879                     final AsyncContext context = request.startAsync();
880 
881                     ((HttpOutput)out).sendContent(content,new Callback()
882                     {
883                         @Override
884                         public void succeeded()
885                         {   
886                             context.complete();
887                         }
888 
889                         @Override
890                         public void failed(Throwable x)
891                         {
892                             if (x instanceof IOException)
893                                 LOG.debug(x);
894                             else
895                                 LOG.warn(x);
896                             context.complete();
897                         }
898                     });
899                 }
900                 // otherwise write content blocking
901                 else
902                 {
903                     ((HttpOutput)out).sendContent(content);
904                 }
905             }
906         }
907         else
908         {
909             // Parse the satisfiable ranges
910             List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
911 
912             //  if there are no satisfiable ranges, send 416 response
913             if (ranges==null || ranges.size()==0)
914             {
915                 writeHeaders(response, content, content_length);
916                 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
917                 response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
918                         InclusiveByteRange.to416HeaderRangeString(content_length));
919                 resource.writeTo(out,0,content_length);
920                 return;
921             }
922 
923             //  if there is only a single valid range (must be satisfiable
924             //  since were here now), send that range with a 216 response
925             if ( ranges.size()== 1)
926             {
927                 InclusiveByteRange singleSatisfiableRange = ranges.get(0);
928                 long singleLength = singleSatisfiableRange.getSize(content_length);
929                 writeHeaders(response,content,singleLength                     );
930                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
931                 if (!response.containsHeader(HttpHeader.DATE.asString()))
932                     response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
933                 response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
934                         singleSatisfiableRange.toHeaderRangeString(content_length));
935                 resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
936                 return;
937             }
938 
939             //  multiple non-overlapping valid ranges cause a multipart
940             //  216 response which does not require an overall
941             //  content-length header
942             //
943             writeHeaders(response,content,-1);
944             String mimetype=(content==null?null:content.getContentType());
945             if (mimetype==null)
946                 LOG.warn("Unknown mimetype for "+request.getRequestURI());
947             MultiPartOutputStream multi = new MultiPartOutputStream(out);
948             response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
949             if (!response.containsHeader(HttpHeader.DATE.asString()))
950                 response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
951 
952             // If the request has a "Request-Range" header then we need to
953             // send an old style multipart/x-byteranges Content-Type. This
954             // keeps Netscape and acrobat happy. This is what Apache does.
955             String ctp;
956             if (request.getHeader(HttpHeader.REQUEST_RANGE.asString())!=null)
957                 ctp = "multipart/x-byteranges; boundary=";
958             else
959                 ctp = "multipart/byteranges; boundary=";
960             response.setContentType(ctp+multi.getBoundary());
961 
962             InputStream in=resource.getInputStream();
963             long pos=0;
964 
965             // calculate the content-length
966             int length=0;
967             String[] header = new String[ranges.size()];
968             for (int i=0;i<ranges.size();i++)
969             {
970                 InclusiveByteRange ibr = ranges.get(i);
971                 header[i]=ibr.toHeaderRangeString(content_length);
972                 length+=
973                     ((i>0)?2:0)+
974                     2+multi.getBoundary().length()+2+
975                     (mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+
976                     HttpHeader.CONTENT_RANGE.asString().length()+2+header[i].length()+2+
977                     2+
978                     (ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
979             }
980             length+=2+2+multi.getBoundary().length()+2+2;
981             response.setContentLength(length);
982 
983             for (int i=0;i<ranges.size();i++)
984             {
985                 InclusiveByteRange ibr =  ranges.get(i);
986                 multi.startPart(mimetype,new String[]{HttpHeader.CONTENT_RANGE+": "+header[i]});
987 
988                 long start=ibr.getFirst(content_length);
989                 long size=ibr.getSize(content_length);
990                 if (in!=null)
991                 {
992                     // Handle non cached resource
993                     if (start<pos)
994                     {
995                         in.close();
996                         in=resource.getInputStream();
997                         pos=0;
998                     }
999                     if (pos<start)
1000                     {
1001                         in.skip(start-pos);
1002                         pos=start;
1003                     }
1004                     
1005                     IO.copy(in,multi,size);
1006                     pos+=size;
1007                 }
1008                 else
1009                     // Handle cached resource
1010                     (resource).writeTo(multi,start,size);
1011             }
1012             if (in!=null)
1013                 in.close();
1014             multi.close();
1015         }
1016         return;
1017     }
1018 
1019     /* ------------------------------------------------------------ */
1020     protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
1021     {        
1022         if (content.getContentType()!=null && response.getContentType()==null)
1023             response.setContentType(content.getContentType().toString());
1024 
1025         if (response instanceof Response)
1026         {
1027             Response r=(Response)response;
1028             HttpFields fields = r.getHttpFields();
1029 
1030             if (content.getLastModified()!=null)
1031                 fields.put(HttpHeader.LAST_MODIFIED,content.getLastModified());
1032             else if (content.getResource()!=null)
1033             {
1034                 long lml=content.getResource().lastModified();
1035                 if (lml!=-1)
1036                     fields.putDateField(HttpHeader.LAST_MODIFIED,lml);
1037             }
1038 
1039             if (count != -1)
1040                 r.setLongContentLength(count);
1041 
1042             writeOptionHeaders(fields);
1043             
1044             if (_etags)
1045                 fields.put(HttpHeader.ETAG,content.getETag());
1046         }
1047         else
1048         {
1049             long lml=content.getResource().lastModified();
1050             if (lml>=0)
1051                 response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(),lml);
1052 
1053             if (count != -1)
1054             {
1055                 if (count<Integer.MAX_VALUE)
1056                     response.setContentLength((int)count);
1057                 else
1058                     response.setHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(count));
1059             }
1060 
1061             writeOptionHeaders(response);
1062 
1063             if (_etags)
1064                 response.setHeader(HttpHeader.ETAG.asString(),content.getETag());
1065         }
1066     }
1067 
1068     /* ------------------------------------------------------------ */
1069     protected void writeOptionHeaders(HttpFields fields)
1070     {
1071         if (_acceptRanges)
1072             fields.put(ACCEPT_RANGES);
1073 
1074         if (_cacheControl!=null)
1075             fields.put(_cacheControl);
1076     }
1077 
1078     /* ------------------------------------------------------------ */
1079     protected void writeOptionHeaders(HttpServletResponse response)
1080     {
1081         if (_acceptRanges)
1082             response.setHeader(HttpHeader.ACCEPT_RANGES.asString(),"bytes");
1083 
1084         if (_cacheControl!=null)
1085             response.setHeader(HttpHeader.CACHE_CONTROL.asString(),_cacheControl.getValue());
1086     }
1087 
1088     /* ------------------------------------------------------------ */
1089     /*
1090      * @see javax.servlet.Servlet#destroy()
1091      */
1092     @Override
1093     public void destroy()
1094     {
1095         if (_cache!=null)
1096             _cache.flushCache();
1097         super.destroy();
1098     }
1099 
1100 }