View Javadoc

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