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