View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server;
20  
21  import static org.eclipse.jetty.util.QuotedStringTokenizer.isQuoted;
22  
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.nio.channels.IllegalSelectorException;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.Enumeration;
30  import java.util.Iterator;
31  import java.util.Locale;
32  import java.util.concurrent.atomic.AtomicInteger;
33  
34  import javax.servlet.RequestDispatcher;
35  import javax.servlet.ServletOutputStream;
36  import javax.servlet.http.Cookie;
37  import javax.servlet.http.HttpServletResponse;
38  import javax.servlet.http.HttpSession;
39  
40  import org.eclipse.jetty.http.DateGenerator;
41  import org.eclipse.jetty.http.HttpContent;
42  import org.eclipse.jetty.http.HttpCookie;
43  import org.eclipse.jetty.http.HttpField;
44  import org.eclipse.jetty.http.HttpFields;
45  import org.eclipse.jetty.http.HttpGenerator;
46  import org.eclipse.jetty.http.HttpGenerator.ResponseInfo;
47  import org.eclipse.jetty.http.HttpHeader;
48  import org.eclipse.jetty.http.HttpHeaderValue;
49  import org.eclipse.jetty.http.HttpParser;
50  import org.eclipse.jetty.http.HttpScheme;
51  import org.eclipse.jetty.http.HttpStatus;
52  import org.eclipse.jetty.http.HttpURI;
53  import org.eclipse.jetty.http.HttpVersion;
54  import org.eclipse.jetty.http.MimeTypes;
55  import org.eclipse.jetty.io.RuntimeIOException;
56  import org.eclipse.jetty.server.handler.ContextHandler;
57  import org.eclipse.jetty.server.handler.ErrorHandler;
58  import org.eclipse.jetty.util.ByteArrayISO8859Writer;
59  import org.eclipse.jetty.util.QuotedStringTokenizer;
60  import org.eclipse.jetty.util.StringUtil;
61  import org.eclipse.jetty.util.URIUtil;
62  import org.eclipse.jetty.util.log.Log;
63  import org.eclipse.jetty.util.log.Logger;
64  
65  /**
66   * <p>{@link Response} provides the implementation for {@link HttpServletResponse}.</p>
67   */
68  public class Response implements HttpServletResponse
69  {
70      private static final Logger LOG = Log.getLogger(Response.class);    
71      private static final String __COOKIE_DELIM="\",;\\ \t";
72      private final static String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim();
73  
74      // Cookie building buffer. Reduce garbage for cookie using applications
75      private static final ThreadLocal<StringBuilder> __cookieBuilder = new ThreadLocal<StringBuilder>()
76      {
77         @Override
78         protected StringBuilder initialValue()
79         {
80            return new StringBuilder(128);
81         }
82      };
83  
84      /* ------------------------------------------------------------ */
85      public static Response getResponse(HttpServletResponse response)
86      {
87          if (response instanceof Response)
88              return (Response)response;
89          return HttpChannel.getCurrentHttpChannel().getResponse();
90      }
91      
92      
93      public enum OutputType
94      {
95          NONE, STREAM, WRITER
96      }
97  
98      /**
99       * If a header name starts with this string,  the header (stripped of the prefix)
100      * can be set during include using only {@link #setHeader(String, String)} or
101      * {@link #addHeader(String, String)}.
102      */
103     public final static String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include.";
104 
105     /**
106      * If this string is found within the comment of a cookie added with {@link #addCookie(Cookie)}, then the cookie
107      * will be set as HTTP ONLY.
108      */
109     public final static String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
110 
111     private final HttpChannel<?> _channel;
112     private final HttpOutput _out;
113     private final HttpFields _fields = new HttpFields();
114     private final AtomicInteger _include = new AtomicInteger();
115     private int _status = HttpStatus.NOT_SET_000;
116     private String _reason;
117     private Locale _locale;
118     private MimeTypes.Type _mimeType;
119     private String _characterEncoding;
120     private boolean _explicitEncoding;
121     private String _contentType;
122     private OutputType _outputType = OutputType.NONE;
123     private ResponseWriter _writer;
124     private long _contentLength = -1;
125     
126 
127     public Response(HttpChannel<?> channel, HttpOutput out)
128     {
129         _channel = channel;
130         _out = out;
131     }
132 
133     protected HttpChannel<?> getHttpChannel()
134     {
135         return _channel;
136     }
137 
138     protected void recycle()
139     {
140         _status = HttpStatus.NOT_SET_000;
141         _reason = null;
142         _locale = null;
143         _mimeType = null;
144         _characterEncoding = null;
145         _contentType = null;
146         _outputType = OutputType.NONE;
147         _contentLength = -1;
148         _out.reset();
149         _fields.clear();
150         _explicitEncoding=false;
151     }
152 
153     public void setHeaders(HttpContent httpContent)
154     {
155         Response response = _channel.getResponse();
156         String contentType = httpContent.getContentType();
157         if (contentType != null && !response.getHttpFields().containsKey(HttpHeader.CONTENT_TYPE.asString()))
158             setContentType(contentType);
159         
160         if (httpContent.getContentLength() > 0)
161             setLongContentLength(httpContent.getContentLength());
162 
163         String lm = httpContent.getLastModified();
164         if (lm != null)
165             response.getHttpFields().put(HttpHeader.LAST_MODIFIED, lm);
166         else if (httpContent.getResource() != null)
167         {
168             long lml = httpContent.getResource().lastModified();
169             if (lml != -1)
170                 response.getHttpFields().putDateField(HttpHeader.LAST_MODIFIED, lml);
171         }
172 
173         String etag=httpContent.getETag();
174         if (etag!=null)
175             response.getHttpFields().put(HttpHeader.ETAG,etag);
176     }
177     
178     public HttpOutput getHttpOutput()
179     {
180         return _out;
181     }
182 
183     public boolean isIncluding()
184     {
185         return _include.get() > 0;
186     }
187 
188     public void include()
189     {
190         _include.incrementAndGet();
191     }
192 
193     public void included()
194     {
195         _include.decrementAndGet();
196         if (_outputType == OutputType.WRITER)
197         {
198             _writer.reopen();
199         }
200         _out.reopen();
201     }
202 
203     public void addCookie(HttpCookie cookie)
204     {
205         addSetCookie(
206                 cookie.getName(),
207                 cookie.getValue(),
208                 cookie.getDomain(),
209                 cookie.getPath(),
210                 cookie.getMaxAge(),
211                 cookie.getComment(),
212                 cookie.isSecure(),
213                 cookie.isHttpOnly(),
214                 cookie.getVersion());;
215     }
216 
217     @Override
218     public void addCookie(Cookie cookie)
219     {
220         String comment = cookie.getComment();
221         boolean httpOnly = false;
222 
223         if (comment != null)
224         {
225             int i = comment.indexOf(HTTP_ONLY_COMMENT);
226             if (i >= 0)
227             {
228                 httpOnly = true;
229                 comment = comment.replace(HTTP_ONLY_COMMENT, "").trim();
230                 if (comment.length() == 0)
231                     comment = null;
232             }
233         }
234         addSetCookie(cookie.getName(),
235                 cookie.getValue(),
236                 cookie.getDomain(),
237                 cookie.getPath(),
238                 cookie.getMaxAge(),
239                 comment,
240                 cookie.getSecure(),
241                 httpOnly || cookie.isHttpOnly(),
242                 cookie.getVersion());
243     }
244 
245 
246     /**
247      * Format a set cookie value
248      *
249      * @param name the name
250      * @param value the value
251      * @param domain the domain
252      * @param path the path
253      * @param maxAge the maximum age
254      * @param comment the comment (only present on versions > 0)
255      * @param isSecure true if secure cookie
256      * @param isHttpOnly true if for http only
257      * @param version version of cookie logic to use (0 == default behavior)
258      */
259     public void addSetCookie(
260             final String name,
261             final String value,
262             final String domain,
263             final String path,
264             final long maxAge,
265             final String comment,
266             final boolean isSecure,
267             final boolean isHttpOnly,
268             int version)
269     {
270         // Check arguments
271         if (name == null || name.length() == 0)
272             throw new IllegalArgumentException("Bad cookie name");
273 
274         // Format value and params
275         StringBuilder buf = __cookieBuilder.get();
276         buf.setLength(0);
277         
278         // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
279         boolean quote_name=isQuoteNeededForCookie(name);
280         quoteOnlyOrAppend(buf,name,quote_name);
281         
282         buf.append('=');
283         
284         // Remember name= part to look for other matching set-cookie
285         String name_equals=buf.toString();
286 
287         // Append the value
288         boolean quote_value=isQuoteNeededForCookie(value);
289         quoteOnlyOrAppend(buf,value,quote_value);
290 
291         // Look for domain and path fields and check if they need to be quoted
292         boolean has_domain = domain!=null && domain.length()>0;
293         boolean quote_domain = has_domain && isQuoteNeededForCookie(domain);
294         boolean has_path = path!=null && path.length()>0;
295         boolean quote_path = has_path && isQuoteNeededForCookie(path);
296         
297         // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
298         if (version==0 && ( comment!=null || quote_name || quote_value || quote_domain || quote_path || isQuoted(name) || isQuoted(value) || isQuoted(path) || isQuoted(domain)))
299             version=1;
300 
301         // Append version
302         if (version==1)
303             buf.append (";Version=1");
304         else if (version>1)
305             buf.append (";Version=").append(version);
306         
307         // Append path
308         if (has_path)
309         {
310             buf.append(";Path=");
311             quoteOnlyOrAppend(buf,path,quote_path);
312         }
313         
314         // Append domain
315         if (has_domain)
316         {
317             buf.append(";Domain=");
318             quoteOnlyOrAppend(buf,domain,quote_domain);
319         }
320 
321         // Handle max-age and/or expires
322         if (maxAge >= 0)
323         {
324             // Always use expires
325             // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
326             buf.append(";Expires=");
327             if (maxAge == 0)
328                 buf.append(__01Jan1970_COOKIE);
329             else
330                 DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);
331             
332             // for v1 cookies, also send max-age
333             if (version>=1)
334             {
335                 buf.append(";Max-Age=");
336                 buf.append(maxAge);
337             }
338         }
339 
340         // add the other fields
341         if (isSecure)
342             buf.append(";Secure");
343         if (isHttpOnly)
344             buf.append(";HttpOnly");
345         if (comment != null)
346         {
347             buf.append(";Comment=");
348             quoteOnlyOrAppend(buf,comment,isQuoteNeededForCookie(comment));
349         }
350 
351         // remove any existing set-cookie fields of same name
352         Iterator<HttpField> i=_fields.iterator();
353         while (i.hasNext())
354         {
355             HttpField field=i.next();
356             if (field.getHeader()==HttpHeader.SET_COOKIE)
357             {
358                 String val = field.getValue();
359                 if (val!=null && val.startsWith(name_equals))
360                 {
361                     //existing cookie has same name, does it also match domain and path?
362                     if (((!has_domain && !val.contains("Domain")) || (has_domain && val.contains(domain))) &&
363                         ((!has_path && !val.contains("Path")) || (has_path && val.contains(path))))
364                     {
365                         i.remove();
366                     }
367                 }
368             }
369         }
370         
371         // add the set cookie
372         _fields.add(HttpHeader.SET_COOKIE.toString(), buf.toString());
373 
374         // Expire responses with set-cookie headers so they do not get cached.
375         _fields.put(HttpHeader.EXPIRES.toString(), DateGenerator.__01Jan1970);
376     }
377 
378 
379     /* ------------------------------------------------------------ */
380     /** Does a cookie value need to be quoted?
381      * @param s value string
382      * @return true if quoted;
383      * @throws IllegalArgumentException If there a control characters in the string
384      */
385     private static boolean isQuoteNeededForCookie(String s)
386     {
387         if (s==null || s.length()==0)
388             return true;
389         
390         if (QuotedStringTokenizer.isQuoted(s))
391             return false;
392 
393         for (int i=0;i<s.length();i++)
394         {
395             char c = s.charAt(i);
396             if (__COOKIE_DELIM.indexOf(c)>=0)
397                 return true;
398             
399             if (c<0x20 || c>=0x7f)
400                 throw new IllegalArgumentException("Illegal character in cookie value");
401         }
402 
403         return false;
404     }
405     
406     
407     private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
408     {
409         if (quote)
410             QuotedStringTokenizer.quoteOnly(buf,s);
411         else
412             buf.append(s);
413     }
414     
415     @Override
416     public boolean containsHeader(String name)
417     {
418         return _fields.containsKey(name);
419     }
420 
421     @Override
422     public String encodeURL(String url)
423     {
424         final Request request = _channel.getRequest();
425         SessionManager sessionManager = request.getSessionManager();
426         if (sessionManager == null)
427             return url;
428 
429         HttpURI uri = null;
430         if (sessionManager.isCheckingRemoteSessionIdEncoding() && URIUtil.hasScheme(url))
431         {
432             uri = new HttpURI(url);
433             String path = uri.getPath();
434             path = (path == null ? "" : path);
435             int port = uri.getPort();
436             if (port < 0)
437                 port = HttpScheme.HTTPS.asString().equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
438             if (!request.getServerName().equalsIgnoreCase(uri.getHost()) ||
439                     request.getServerPort() != port ||
440                     !path.startsWith(request.getContextPath())) //TODO the root context path is "", with which every non null string starts
441                 return url;
442         }
443 
444         String sessionURLPrefix = sessionManager.getSessionIdPathParameterNamePrefix();
445         if (sessionURLPrefix == null)
446             return url;
447 
448         if (url == null)
449             return null;
450 
451         // should not encode if cookies in evidence
452         if ((sessionManager.isUsingCookies() && request.isRequestedSessionIdFromCookie()) || !sessionManager.isUsingURLs()) 
453         {
454             int prefix = url.indexOf(sessionURLPrefix);
455             if (prefix != -1)
456             {
457                 int suffix = url.indexOf("?", prefix);
458                 if (suffix < 0)
459                     suffix = url.indexOf("#", prefix);
460 
461                 if (suffix <= prefix)
462                     return url.substring(0, prefix);
463                 return url.substring(0, prefix) + url.substring(suffix);
464             }
465             return url;
466         }
467 
468         // get session;
469         HttpSession session = request.getSession(false);
470 
471         // no session
472         if (session == null)
473             return url;
474 
475         // invalid session
476         if (!sessionManager.isValid(session))
477             return url;
478 
479         String id = sessionManager.getNodeId(session);
480 
481         if (uri == null)
482             uri = new HttpURI(url);
483 
484 
485         // Already encoded
486         int prefix = url.indexOf(sessionURLPrefix);
487         if (prefix != -1)
488         {
489             int suffix = url.indexOf("?", prefix);
490             if (suffix < 0)
491                 suffix = url.indexOf("#", prefix);
492 
493             if (suffix <= prefix)
494                 return url.substring(0, prefix + sessionURLPrefix.length()) + id;
495             return url.substring(0, prefix + sessionURLPrefix.length()) + id +
496                     url.substring(suffix);
497         }
498 
499         // edit the session
500         int suffix = url.indexOf('?');
501         if (suffix < 0)
502             suffix = url.indexOf('#');
503         if (suffix < 0)
504         {
505             return url +
506                     ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path, insert the root path
507                     sessionURLPrefix + id;
508         }
509 
510 
511         return url.substring(0, suffix) +
512                 ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path so insert the root path
513                 sessionURLPrefix + id + url.substring(suffix);
514     }
515 
516     @Override
517     public String encodeRedirectURL(String url)
518     {
519         return encodeURL(url);
520     }
521 
522     @Override
523     @Deprecated
524     public String encodeUrl(String url)
525     {
526         return encodeURL(url);
527     }
528 
529     @Override
530     @Deprecated
531     public String encodeRedirectUrl(String url)
532     {
533         return encodeRedirectURL(url);
534     }
535 
536     @Override
537     public void sendError(int sc) throws IOException
538     {
539         if (sc == 102)
540             sendProcessing();
541         else
542             sendError(sc, null);
543     }
544 
545     @Override
546     public void sendError(int code, String message) throws IOException
547     {
548         if (isIncluding())
549             return;
550 
551         if (isCommitted())
552             LOG.warn("Committed before "+code+" "+message);
553 
554         resetBuffer();
555         _characterEncoding=null;
556         setHeader(HttpHeader.EXPIRES,null);
557         setHeader(HttpHeader.LAST_MODIFIED,null);
558         setHeader(HttpHeader.CACHE_CONTROL,null);
559         setHeader(HttpHeader.CONTENT_TYPE,null);
560         setHeader(HttpHeader.CONTENT_LENGTH,null);
561 
562         _outputType = OutputType.NONE;
563         setStatus(code);
564         _reason=message;
565 
566         Request request = _channel.getRequest();
567         Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
568         if (message==null)
569             message=cause==null?HttpStatus.getMessage(code):cause.toString();
570 
571         // If we are allowed to have a body
572         if (code!=SC_NO_CONTENT &&
573             code!=SC_NOT_MODIFIED &&
574             code!=SC_PARTIAL_CONTENT &&
575             code>=SC_OK)
576         {
577 
578             ErrorHandler error_handler = null;
579             ContextHandler.Context context = request.getContext();
580             if (context!=null)
581                 error_handler=context.getContextHandler().getErrorHandler();
582             if (error_handler==null)
583                 error_handler = _channel.getServer().getBean(ErrorHandler.class);
584             if (error_handler!=null)
585             {
586                 request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,new Integer(code));
587                 request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
588                 request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
589                 request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,request.getServletName());
590                 error_handler.handle(null,_channel.getRequest(),_channel.getRequest(),this );
591             }
592             else
593             {
594                 setHeader(HttpHeader.CACHE_CONTROL, "must-revalidate,no-cache,no-store");
595                 setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString());
596                 try (ByteArrayISO8859Writer writer= new ByteArrayISO8859Writer(2048);)
597                 {
598                     if (message != null)
599                     {
600                         message= StringUtil.replace(message, "&", "&amp;");
601                         message= StringUtil.replace(message, "<", "&lt;");
602                         message= StringUtil.replace(message, ">", "&gt;");
603                     }
604                     String uri= request.getRequestURI();
605                     if (uri!=null)
606                     {
607                         uri= StringUtil.replace(uri, "&", "&amp;");
608                         uri= StringUtil.replace(uri, "<", "&lt;");
609                         uri= StringUtil.replace(uri, ">", "&gt;");
610                     }
611 
612                     writer.write("<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\"/>\n");
613                     writer.write("<title>Error ");
614                     writer.write(Integer.toString(code));
615                     writer.write(' ');
616                     if (message==null)
617                         writer.write(message);
618                     writer.write("</title>\n</head>\n<body>\n<h2>HTTP ERROR: ");
619                     writer.write(Integer.toString(code));
620                     writer.write("</h2>\n<p>Problem accessing ");
621                     writer.write(uri);
622                     writer.write(". Reason:\n<pre>    ");
623                     writer.write(message);
624                     writer.write("</pre>");
625                     writer.write("</p>\n<hr /><i><small>Powered by Jetty://</small></i>");
626                     writer.write("\n</body>\n</html>\n");
627 
628                     writer.flush();
629                     setContentLength(writer.size());
630                     try (ServletOutputStream outputStream = getOutputStream())
631                     {
632                         writer.writeTo(outputStream);
633                         writer.destroy();
634                     }
635                 }
636             }
637         }
638         else if (code!=SC_PARTIAL_CONTENT)
639         {
640             // TODO work out why this is required?
641             _channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_TYPE);
642             _channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_LENGTH);
643             _characterEncoding=null;
644             _mimeType=null;
645         }
646 
647         closeOutput();
648     }
649 
650     /**
651      * Sends a 102-Processing response.
652      * If the connection is a HTTP connection, the version is 1.1 and the
653      * request has a Expect header starting with 102, then a 102 response is
654      * sent. This indicates that the request still be processed and real response
655      * can still be sent.   This method is called by sendError if it is passed 102.
656      * @see javax.servlet.http.HttpServletResponse#sendError(int)
657      */
658     public void sendProcessing() throws IOException
659     {
660         if (_channel.isExpecting102Processing() && !isCommitted())
661         {
662             _channel.sendResponse(HttpGenerator.PROGRESS_102_INFO, null, true);
663         }
664     }
665     
666     /**
667      * Sends a response with one of the 300 series redirection codes.
668      * @param code
669      * @param location
670      * @throws IOException
671      */
672     public void sendRedirect(int code, String location) throws IOException
673     {
674         if ((code < HttpServletResponse.SC_MULTIPLE_CHOICES) || (code >= HttpServletResponse.SC_BAD_REQUEST))
675             throw new IllegalArgumentException("Not a 3xx redirect code");
676         
677         if (isIncluding())
678             return;
679 
680         if (location == null)
681             throw new IllegalArgumentException();
682 
683         if (!URIUtil.hasScheme(location))
684         {
685             StringBuilder buf = _channel.getRequest().getRootURL();
686  
687             if (location.startsWith("//"))
688             {
689                 buf.delete(0, buf.length());
690                 buf.append(_channel.getRequest().getScheme());
691                 buf.append(":");
692                 buf.append(location);
693             }
694             else if (location.startsWith("/"))
695                 buf.append(location);
696             else
697             {
698                 String path = _channel.getRequest().getRequestURI();
699                 String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
700                 location = URIUtil.addPaths(parent, location);
701                 if (location == null)
702                     throw new IllegalStateException("path cannot be above root");
703                 if (!location.startsWith("/"))
704                     buf.append('/');
705                 buf.append(location);
706             }
707 
708             location = buf.toString();
709             HttpURI uri = new HttpURI(location);
710             String path = uri.getDecodedPath();
711             String canonical = URIUtil.canonicalPath(path);
712             if (canonical == null)
713                 throw new IllegalArgumentException();
714             if (!canonical.equals(path))
715             {
716                 buf = _channel.getRequest().getRootURL();
717                 buf.append(URIUtil.encodePath(canonical));
718                 String param=uri.getParam();
719                 if (param!=null)
720                 {
721                     buf.append(';');
722                     buf.append(param);
723                 }
724                 String query=uri.getQuery();
725                 if (query!=null)
726                 {
727                     buf.append('?');
728                     buf.append(query);
729                 }
730                 String fragment=uri.getFragment();
731                 if (fragment!=null)
732                 {
733                     buf.append('#');
734                     buf.append(fragment);
735                 }
736                 location = buf.toString();
737             }
738         }
739 
740         resetBuffer();
741         setHeader(HttpHeader.LOCATION, location);
742         setStatus(code);
743         closeOutput();
744     }
745 
746     @Override
747     public void sendRedirect(String location) throws IOException
748     {
749         sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location);
750     }
751 
752     @Override
753     public void setDateHeader(String name, long date)
754     {
755         if (!isIncluding())
756             _fields.putDateField(name, date);
757     }
758 
759     @Override
760     public void addDateHeader(String name, long date)
761     {
762         if (!isIncluding())
763             _fields.addDateField(name, date);
764     }
765 
766     public void setHeader(HttpHeader name, String value)
767     {
768         if (HttpHeader.CONTENT_TYPE == name)
769             setContentType(value);
770         else
771         {
772             if (isIncluding())
773                 return;
774 
775             _fields.put(name, value);
776 
777             if (HttpHeader.CONTENT_LENGTH == name)
778             {
779                 if (value == null)
780                     _contentLength = -1l;
781                 else
782                     _contentLength = Long.parseLong(value);
783             }
784         }
785     }
786 
787     @Override
788     public void setHeader(String name, String value)
789     {
790         if (HttpHeader.CONTENT_TYPE.is(name))
791             setContentType(value);
792         else
793         {
794             if (isIncluding())
795             {
796                 if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
797                     name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
798                 else
799                     return;
800             }
801             _fields.put(name, value);
802             if (HttpHeader.CONTENT_LENGTH.is(name))
803             {
804                 if (value == null)
805                     _contentLength = -1l;
806                 else
807                     _contentLength = Long.parseLong(value);
808             }
809         }
810     }
811 
812     @Override
813     public Collection<String> getHeaderNames()
814     {
815         final HttpFields fields = _fields;
816         return fields.getFieldNamesCollection();
817     }
818 
819     @Override
820     public String getHeader(String name)
821     {
822         return _fields.getStringField(name);
823     }
824 
825     @Override
826     public Collection<String> getHeaders(String name)
827     {
828         final HttpFields fields = _fields;
829         Collection<String> i = fields.getValuesList(name);
830         if (i == null)
831             return Collections.emptyList();
832         return i;
833     }
834 
835     @Override
836     public void addHeader(String name, String value)
837     {
838         if (isIncluding())
839         {
840             if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
841                 name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
842             else
843                 return;
844         }
845 
846         if (HttpHeader.CONTENT_TYPE.is(name))
847         {
848             setContentType(value);
849             return;
850         }
851         
852         if (HttpHeader.CONTENT_LENGTH.is(name))
853         {
854             setHeader(name,value);
855             return;
856         }
857         
858         _fields.add(name, value);
859     }
860 
861     @Override
862     public void setIntHeader(String name, int value)
863     {
864         if (!isIncluding())
865         {
866             _fields.putLongField(name, value);
867             if (HttpHeader.CONTENT_LENGTH.is(name))
868                 _contentLength = value;
869         }
870     }
871 
872     @Override
873     public void addIntHeader(String name, int value)
874     {
875         if (!isIncluding())
876         {
877             _fields.add(name, Integer.toString(value));
878             if (HttpHeader.CONTENT_LENGTH.is(name))
879                 _contentLength = value;
880         }
881     }
882 
883     @Override
884     public void setStatus(int sc)
885     {
886         if (sc <= 0)
887             throw new IllegalArgumentException();
888         if (!isIncluding())
889         {
890             _status = sc;
891             _reason = null;
892         }
893     }
894 
895     @Override
896     @Deprecated
897     public void setStatus(int sc, String sm)
898     {
899         setStatusWithReason(sc,sm);
900     }
901     
902     public void setStatusWithReason(int sc, String sm)
903     {
904         if (sc <= 0)
905             throw new IllegalArgumentException();
906         if (!isIncluding())
907         {
908             _status = sc;
909             _reason = sm;
910         }
911     }
912 
913     @Override
914     public String getCharacterEncoding()
915     {
916         if (_characterEncoding == null)
917             _characterEncoding = StringUtil.__ISO_8859_1;
918         return _characterEncoding;
919     }
920 
921     @Override
922     public String getContentType()
923     {
924         return _contentType;
925     }
926 
927     @Override
928     public ServletOutputStream getOutputStream() throws IOException
929     {
930         if (_outputType == OutputType.WRITER)
931             throw new IllegalStateException("WRITER");
932         _outputType = OutputType.STREAM;
933         return _out;
934     }
935 
936     public boolean isWriting()
937     {
938         return _outputType == OutputType.WRITER;
939     }
940 
941     @Override
942     public PrintWriter getWriter() throws IOException
943     {
944         if (_outputType == OutputType.STREAM)
945             throw new IllegalStateException("STREAM");
946 
947         if (_outputType == OutputType.NONE)
948         {
949             /* get encoding from Content-Type header */
950             String encoding = _characterEncoding;
951             if (encoding == null)
952             {
953                 encoding = MimeTypes.inferCharsetFromContentType(_contentType);
954                 if (encoding == null)
955                     encoding = StringUtil.__ISO_8859_1;
956                 setCharacterEncoding(encoding,false);
957             }
958             
959             if (_writer != null && _writer.isFor(encoding))
960                 _writer.reopen();
961             else
962             {
963                 if (StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
964                     _writer = new ResponseWriter(new Iso88591HttpWriter(_out),encoding);
965                 else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
966                     _writer = new ResponseWriter(new Utf8HttpWriter(_out),encoding);
967                 else
968                     _writer = new ResponseWriter(new EncodingHttpWriter(_out, encoding),encoding);
969             }
970             
971             // Set the output type at the end, because setCharacterEncoding() checks for it
972             _outputType = OutputType.WRITER;
973         }
974         return _writer;
975     }
976 
977     @Override
978     public void setContentLength(int len)
979     {
980         // Protect from setting after committed as default handling
981         // of a servlet HEAD request ALWAYS sets _content length, even
982         // if the getHandling committed the response!
983         if (isCommitted() || isIncluding())
984             return;
985 
986         long written = _out.getWritten();
987         if (written > len)
988             throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written);
989 
990         _contentLength = len;
991         _fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
992 
993         if (_contentLength > 0)
994         {
995             if (isAllContentWritten(written))
996             {
997                 try
998                 {
999                     closeOutput();
1000                 }
1001                 catch(IOException e)
1002                 {
1003                     throw new RuntimeIOException(e);
1004                 }
1005             }
1006         }
1007     }
1008 
1009     public boolean isAllContentWritten(long written)
1010     {
1011         return (_contentLength >= 0 && written >= _contentLength);
1012     }
1013 
1014     public void closeOutput() throws IOException
1015     {
1016         switch (_outputType)
1017         {
1018             case WRITER:
1019                 _writer.close();
1020                 if (!_out.isClosed())
1021                     _out.close();
1022                 break;
1023             case STREAM:
1024                 getOutputStream().close();
1025                 break;
1026             default:
1027                 _out.close();
1028         }
1029     }
1030 
1031     public long getLongContentLength()
1032     {
1033         return _contentLength;
1034     }
1035 
1036     public void setLongContentLength(long len)
1037     {
1038         // Protect from setting after committed as default handling
1039         // of a servlet HEAD request ALWAYS sets _content length, even
1040         // if the getHandling committed the response!
1041         if (isCommitted() || isIncluding())
1042             return;
1043         _contentLength = len;
1044         _fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
1045     }
1046     
1047     @Override
1048     public void setContentLengthLong(long length)
1049     {
1050         setLongContentLength(length);
1051     }
1052 
1053     @Override
1054     public void setCharacterEncoding(String encoding)
1055     {
1056         setCharacterEncoding(encoding,true);
1057     }
1058     
1059     private void setCharacterEncoding(String encoding, boolean explicit)
1060     {
1061         if (isIncluding())
1062             return;
1063 
1064         if (_outputType == OutputType.NONE && !isCommitted())
1065         {
1066             if (encoding == null)
1067             {
1068                 _explicitEncoding=false;
1069                 
1070                 // Clear any encoding.
1071                 if (_characterEncoding != null)
1072                 {
1073                     _characterEncoding = null;
1074                     if (_contentType != null)
1075                     {
1076                         _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
1077                         HttpField field = HttpParser.CONTENT_TYPE.get(_contentType);
1078                         if (field!=null)
1079                             _fields.put(field);
1080                         else
1081                             _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1082                     }
1083                 }
1084             }
1085             else
1086             {
1087                 // No, so just add this one to the mimetype
1088                 _explicitEncoding=explicit;
1089                 _characterEncoding = StringUtil.normalizeCharset(encoding);
1090                 if (_contentType != null)
1091                 {
1092                     _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType) + ";charset=" + _characterEncoding;
1093                     HttpField field = HttpParser.CONTENT_TYPE.get(_contentType);
1094                     if (field!=null)
1095                         _fields.put(field);
1096                     else
1097                         _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1098                 }
1099             }
1100         }
1101     }
1102 
1103     @Override
1104     public void setContentType(String contentType)
1105     {
1106         if (isCommitted() || isIncluding())
1107             return;
1108 
1109         if (contentType == null)
1110         {
1111             if (isWriting() && _characterEncoding != null)
1112                 throw new IllegalSelectorException();
1113 
1114             if (_locale == null)
1115                 _characterEncoding = null;
1116             _mimeType = null;
1117             _contentType = null;
1118             _fields.remove(HttpHeader.CONTENT_TYPE);
1119         }
1120         else
1121         {
1122             _contentType = contentType;
1123             _mimeType = MimeTypes.CACHE.get(contentType);
1124             String charset;
1125             if (_mimeType != null && _mimeType.getCharset() != null)
1126                 charset = _mimeType.getCharset().toString();
1127             else
1128                 charset = MimeTypes.getCharsetFromContentType(contentType);
1129 
1130             if (charset == null)
1131             {
1132                 if (_characterEncoding != null)
1133                 {
1134                     _contentType = contentType + ";charset=" + _characterEncoding;
1135                     _mimeType = null;
1136                 }
1137             }
1138             else if (isWriting() && !charset.equals(_characterEncoding))
1139             {
1140                 // too late to change the character encoding;
1141                 _mimeType = null;
1142                 _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
1143                 if (_characterEncoding != null)
1144                     _contentType = _contentType + ";charset=" + _characterEncoding;
1145             }
1146             else
1147             {
1148                 _characterEncoding = charset;
1149                 _explicitEncoding = true;
1150             }
1151 
1152             HttpField field = HttpParser.CONTENT_TYPE.get(_contentType);
1153             if (field!=null)
1154                 _fields.put(field);
1155             else
1156                 _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1157         }
1158     }
1159 
1160     @Override
1161     public void setBufferSize(int size)
1162     {
1163         if (isCommitted() || getContentCount() > 0)
1164             throw new IllegalStateException("Committed or content written");
1165         _out.setBufferSize(size);
1166     }
1167 
1168     @Override
1169     public int getBufferSize()
1170     {
1171         return _out.getBufferSize();
1172     }
1173 
1174     @Override
1175     public void flushBuffer() throws IOException
1176     {
1177         if (!_out.isClosed())
1178             _out.flush();
1179     }
1180 
1181     @Override
1182     public void reset()
1183     {
1184         resetForForward();
1185         _status = 200;
1186         _reason = null;
1187         _contentLength = -1;
1188         _fields.clear();
1189 
1190         String connection = _channel.getRequest().getHttpFields().getStringField(HttpHeader.CONNECTION);
1191         if (connection != null)
1192         {
1193             String[] values = connection.split(",");
1194             for (int i = 0; values != null && i < values.length; i++)
1195             {
1196                 HttpHeaderValue cb = HttpHeaderValue.CACHE.get(values[0].trim());
1197 
1198                 if (cb != null)
1199                 {
1200                     switch (cb)
1201                     {
1202                         case CLOSE:
1203                             _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString());
1204                             break;
1205 
1206                         case KEEP_ALIVE:
1207                             if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol()))
1208                                 _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString());
1209                             break;
1210                         case TE:
1211                             _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString());
1212                             break;
1213                         default:
1214                     }
1215                 }
1216             }
1217         }
1218     }
1219 
1220     public void reset(boolean preserveCookies)
1221     { 
1222         if (!preserveCookies)
1223             reset();
1224         else
1225         {
1226             ArrayList<String> cookieValues = new ArrayList<String>(5);
1227             Enumeration<String> vals = _fields.getValues(HttpHeader.SET_COOKIE.asString());
1228             while (vals.hasMoreElements())
1229                 cookieValues.add(vals.nextElement());
1230             reset();
1231             for (String v:cookieValues)
1232                 _fields.add(HttpHeader.SET_COOKIE, v);
1233         }
1234     }
1235 
1236     public void resetForForward()
1237     {
1238         resetBuffer();
1239         _outputType = OutputType.NONE;
1240     }
1241 
1242     @Override
1243     public void resetBuffer()
1244     {
1245         if (isCommitted())
1246             throw new IllegalStateException("Committed");
1247 
1248         switch (_outputType)
1249         {
1250             case STREAM:
1251             case WRITER:
1252                 _out.reset();
1253                 break;
1254             default:
1255         }
1256 
1257         _out.resetBuffer();
1258     }
1259 
1260     protected ResponseInfo newResponseInfo()
1261     {
1262         if (_status == HttpStatus.NOT_SET_000)
1263             _status = HttpStatus.OK_200;
1264         return new ResponseInfo(_channel.getRequest().getHttpVersion(), _fields, getLongContentLength(), getStatus(), getReason(), _channel.getRequest().isHead());
1265     }
1266 
1267     @Override
1268     public boolean isCommitted()
1269     {
1270         return _channel.isCommitted();
1271     }
1272 
1273     @Override
1274     public void setLocale(Locale locale)
1275     {
1276         if (locale == null || isCommitted() || isIncluding())
1277             return;
1278 
1279         _locale = locale;
1280         _fields.put(HttpHeader.CONTENT_LANGUAGE, locale.toString().replace('_', '-'));
1281 
1282         if (_outputType != OutputType.NONE)
1283             return;
1284 
1285         if (_channel.getRequest().getContext() == null)
1286             return;
1287 
1288         String charset = _channel.getRequest().getContext().getContextHandler().getLocaleEncoding(locale);
1289 
1290         if (charset != null && charset.length() > 0 && !_explicitEncoding)
1291             setCharacterEncoding(charset,false);
1292     }
1293 
1294     @Override
1295     public Locale getLocale()
1296     {
1297         if (_locale == null)
1298             return Locale.getDefault();
1299         return _locale;
1300     }
1301 
1302     @Override
1303     public int getStatus()
1304     {
1305         return _status;
1306     }
1307 
1308     public String getReason()
1309     {
1310         return _reason;
1311     }
1312 
1313     public HttpFields getHttpFields()
1314     {
1315         return _fields;
1316     }
1317 
1318     public long getContentCount()
1319     {
1320         return _out.getWritten();
1321     }
1322 
1323     @Override
1324     public String toString()
1325     {
1326         return String.format("%s %d %s%n%s", _channel.getRequest().getHttpVersion(), _status, _reason == null ? "" : _reason, _fields);
1327     }
1328     
1329 
1330     private static class ResponseWriter extends PrintWriter
1331     {
1332         private final String _encoding;
1333         private final HttpWriter _httpWriter;
1334         
1335         public ResponseWriter(HttpWriter httpWriter,String encoding)
1336         {
1337             super(httpWriter);
1338             _httpWriter=httpWriter;
1339             _encoding=encoding;
1340         }
1341 
1342         public boolean isFor(String encoding)
1343         {
1344             return _encoding.equalsIgnoreCase(encoding);
1345         }
1346         
1347         protected void reopen()
1348         {
1349             super.clearError();
1350             out=_httpWriter;
1351         }
1352     }
1353 }