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