]> WPIA git - gigi.git/blob - lib/jetty/org/eclipse/jetty/server/Response.java
updating jetty to jetty-9.2.16.v2016040
[gigi.git] / lib / jetty / org / eclipse / jetty / server / Response.java
1 //
2 //  ========================================================================
3 //  Copyright (c) 1995-2016 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.OK_200;
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.OK_200;
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         sendError(sc, null);
545     }
546
547     @Override
548     public void sendError(int code, String message) throws IOException
549     {
550         if (isIncluding())
551             return;
552
553         switch(code)
554         {
555             case -1:
556                 _channel.abort();
557                 return;
558             case 102:
559                 sendProcessing();
560                 return;
561             default:
562         }
563
564         if (isCommitted())
565             LOG.warn("Committed before "+code+" "+message);
566
567         resetBuffer();
568         _characterEncoding=null;
569         setHeader(HttpHeader.EXPIRES,null);
570         setHeader(HttpHeader.LAST_MODIFIED,null);
571         setHeader(HttpHeader.CACHE_CONTROL,null);
572         setHeader(HttpHeader.CONTENT_TYPE,null);
573         setHeader(HttpHeader.CONTENT_LENGTH,null);
574
575         _outputType = OutputType.NONE;
576         setStatus(code);
577         _reason=message;
578
579         Request request = _channel.getRequest();
580         Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
581         if (message==null)
582             message=cause==null?HttpStatus.getMessage(code):cause.toString();
583
584         // If we are allowed to have a body
585         if (code!=SC_NO_CONTENT &&
586             code!=SC_NOT_MODIFIED &&
587             code!=SC_PARTIAL_CONTENT &&
588             code>=SC_OK)
589         {
590             ErrorHandler error_handler = ErrorHandler.getErrorHandler(_channel.getServer(),request.getContext()==null?null:request.getContext().getContextHandler());
591             if (error_handler!=null)
592             {
593                 request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,new Integer(code));
594                 request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
595                 request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
596                 request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,request.getServletName());
597                 error_handler.handle(null,_channel.getRequest(),_channel.getRequest(),this );
598             }
599             else
600             {
601                 setHeader(HttpHeader.CACHE_CONTROL, "must-revalidate,no-cache,no-store");
602                 setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString());
603                 try (ByteArrayISO8859Writer writer= new ByteArrayISO8859Writer(2048);)
604                 {
605                     message=StringUtil.sanitizeXmlString(message);
606                     String uri= request.getRequestURI();
607                     uri=StringUtil.sanitizeXmlString(uri);
608
609                     writer.write("<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\"/>\n");
610                     writer.write("<title>Error ");
611                     writer.write(Integer.toString(code));
612                     writer.write(' ');
613                     if (message==null)
614                         writer.write(message);
615                     writer.write("</title>\n</head>\n<body>\n<h2>HTTP ERROR: ");
616                     writer.write(Integer.toString(code));
617                     writer.write("</h2>\n<p>Problem accessing ");
618                     writer.write(uri);
619                     writer.write(". Reason:\n<pre>    ");
620                     writer.write(message);
621                     writer.write("</pre>");
622                     writer.write("</p>\n<hr /><i><small>Powered by Jetty://</small></i>");
623                     writer.write("\n</body>\n</html>\n");
624
625                     writer.flush();
626                     setContentLength(writer.size());
627                     try (ServletOutputStream outputStream = getOutputStream())
628                     {
629                         writer.writeTo(outputStream);
630                         writer.destroy();
631                     }
632                 }
633             }
634         }
635         else if (code!=SC_PARTIAL_CONTENT)
636         {
637             // TODO work out why this is required?
638             _channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_TYPE);
639             _channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_LENGTH);
640             _characterEncoding=null;
641             _mimeType=null;
642         }
643
644         closeOutput();
645     }
646
647     /**
648      * Sends a 102-Processing response.
649      * If the connection is a HTTP connection, the version is 1.1 and the
650      * request has a Expect header starting with 102, then a 102 response is
651      * sent. This indicates that the request still be processed and real response
652      * can still be sent.   This method is called by sendError if it is passed 102.
653      * @see javax.servlet.http.HttpServletResponse#sendError(int)
654      */
655     public void sendProcessing() throws IOException
656     {
657         if (_channel.isExpecting102Processing() && !isCommitted())
658         {
659             _channel.sendResponse(HttpGenerator.PROGRESS_102_INFO, null, true);
660         }
661     }
662     
663     /**
664      * Sends a response with one of the 300 series redirection codes.
665      * @param code
666      * @param location
667      * @throws IOException
668      */
669     public void sendRedirect(int code, String location) throws IOException
670     {
671         if ((code < HttpServletResponse.SC_MULTIPLE_CHOICES) || (code >= HttpServletResponse.SC_BAD_REQUEST))
672             throw new IllegalArgumentException("Not a 3xx redirect code");
673         
674         if (isIncluding())
675             return;
676
677         if (location == null)
678             throw new IllegalArgumentException();
679
680         if (!URIUtil.hasScheme(location))
681         {
682             StringBuilder buf = _channel.getRequest().getRootURL();
683             if (location.startsWith("/"))
684             {
685                 // absolute in context
686                 location=URIUtil.canonicalPath(location);
687             }
688             else
689             {
690                 // relative to request
691                 String path=_channel.getRequest().getRequestURI();
692                 String parent=(path.endsWith("/"))?path:URIUtil.parentPath(path);
693                 location=URIUtil.canonicalPath(URIUtil.addPaths(parent,location));
694                 if (!location.startsWith("/"))
695                     buf.append('/');
696             }
697             
698             if(location==null)
699                 throw new IllegalStateException("path cannot be above root");
700             buf.append(location);
701             
702             location=buf.toString();
703         }
704
705         resetBuffer();
706         setHeader(HttpHeader.LOCATION, location);
707         setStatus(code);
708         closeOutput();
709     }
710
711     @Override
712     public void sendRedirect(String location) throws IOException
713     {
714         sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location);
715     }
716
717     @Override
718     public void setDateHeader(String name, long date)
719     {
720         if (!isIncluding())
721             _fields.putDateField(name, date);
722     }
723
724     @Override
725     public void addDateHeader(String name, long date)
726     {
727         if (!isIncluding())
728             _fields.addDateField(name, date);
729     }
730
731     public void setHeader(HttpHeader name, String value)
732     {
733         if (HttpHeader.CONTENT_TYPE == name)
734             setContentType(value);
735         else
736         {
737             if (isIncluding())
738                 return;
739
740             _fields.put(name, value);
741
742             if (HttpHeader.CONTENT_LENGTH == name)
743             {
744                 if (value == null)
745                     _contentLength = -1l;
746                 else
747                     _contentLength = Long.parseLong(value);
748             }
749         }
750     }
751
752     @Override
753     public void setHeader(String name, String value)
754     {
755         if (HttpHeader.CONTENT_TYPE.is(name))
756             setContentType(value);
757         else
758         {
759             if (isIncluding())
760             {
761                 if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
762                     name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
763                 else
764                     return;
765             }
766             _fields.put(name, value);
767             if (HttpHeader.CONTENT_LENGTH.is(name))
768             {
769                 if (value == null)
770                     _contentLength = -1l;
771                 else
772                     _contentLength = Long.parseLong(value);
773             }
774         }
775     }
776
777     @Override
778     public Collection<String> getHeaderNames()
779     {
780         final HttpFields fields = _fields;
781         return fields.getFieldNamesCollection();
782     }
783
784     @Override
785     public String getHeader(String name)
786     {
787         return _fields.getStringField(name);
788     }
789
790     @Override
791     public Collection<String> getHeaders(String name)
792     {
793         final HttpFields fields = _fields;
794         Collection<String> i = fields.getValuesList(name);
795         if (i == null)
796             return Collections.emptyList();
797         return i;
798     }
799
800     @Override
801     public void addHeader(String name, String value)
802     {
803         if (isIncluding())
804         {
805             if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
806                 name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
807             else
808                 return;
809         }
810
811         if (HttpHeader.CONTENT_TYPE.is(name))
812         {
813             setContentType(value);
814             return;
815         }
816         
817         if (HttpHeader.CONTENT_LENGTH.is(name))
818         {
819             setHeader(name,value);
820             return;
821         }
822         
823         _fields.add(name, value);
824     }
825
826     @Override
827     public void setIntHeader(String name, int value)
828     {
829         if (!isIncluding())
830         {
831             _fields.putLongField(name, value);
832             if (HttpHeader.CONTENT_LENGTH.is(name))
833                 _contentLength = value;
834         }
835     }
836
837     @Override
838     public void addIntHeader(String name, int value)
839     {
840         if (!isIncluding())
841         {
842             _fields.add(name, Integer.toString(value));
843             if (HttpHeader.CONTENT_LENGTH.is(name))
844                 _contentLength = value;
845         }
846     }
847
848     @Override
849     public void setStatus(int sc)
850     {
851         if (sc <= 0)
852             throw new IllegalArgumentException();
853         if (!isIncluding())
854         {
855             _status = sc;
856             _reason = null;
857         }
858     }
859
860     @Override
861     @Deprecated
862     public void setStatus(int sc, String sm)
863     {
864         setStatusWithReason(sc,sm);
865     }
866     
867     public void setStatusWithReason(int sc, String sm)
868     {
869         if (sc <= 0)
870             throw new IllegalArgumentException();
871         if (!isIncluding())
872         {
873             _status = sc;
874             _reason = sm;
875         }
876     }
877
878     @Override
879     public String getCharacterEncoding()
880     {
881         if (_characterEncoding == null)
882             _characterEncoding = StringUtil.__ISO_8859_1;
883         return _characterEncoding;
884     }
885
886     @Override
887     public String getContentType()
888     {
889         return _contentType;
890     }
891
892     @Override
893     public ServletOutputStream getOutputStream() throws IOException
894     {
895         if (_outputType == OutputType.WRITER)
896             throw new IllegalStateException("WRITER");
897         _outputType = OutputType.STREAM;
898         return _out;
899     }
900
901     public boolean isWriting()
902     {
903         return _outputType == OutputType.WRITER;
904     }
905
906     @Override
907     public PrintWriter getWriter() throws IOException
908     {
909         if (_outputType == OutputType.STREAM)
910             throw new IllegalStateException("STREAM");
911
912         if (_outputType == OutputType.NONE)
913         {
914             /* get encoding from Content-Type header */
915             String encoding = _characterEncoding;
916             if (encoding == null)
917             {
918                 if (_mimeType!=null && _mimeType.isCharsetAssumed())
919                     encoding=_mimeType.getCharset().toString();
920                 else
921                 {
922                     encoding = MimeTypes.inferCharsetFromContentType(_contentType);
923                     if (encoding == null)
924                         encoding = StringUtil.__ISO_8859_1;
925                     setCharacterEncoding(encoding,false);
926                 }
927             }
928             
929             if (_writer != null && _writer.isFor(encoding))
930                 _writer.reopen();
931             else
932             {
933                 if (StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
934                     _writer = new ResponseWriter(new Iso88591HttpWriter(_out),encoding);
935                 else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
936                     _writer = new ResponseWriter(new Utf8HttpWriter(_out),encoding);
937                 else
938                     _writer = new ResponseWriter(new EncodingHttpWriter(_out, encoding),encoding);
939             }
940             
941             // Set the output type at the end, because setCharacterEncoding() checks for it
942             _outputType = OutputType.WRITER;
943         }
944         return _writer;
945     }
946
947     @Override
948     public void setContentLength(int len)
949     {
950         // Protect from setting after committed as default handling
951         // of a servlet HEAD request ALWAYS sets _content length, even
952         // if the getHandling committed the response!
953         if (isCommitted() || isIncluding())
954             return;
955
956         _contentLength = len;
957         if (_contentLength > 0)
958         {
959             long written = _out.getWritten();
960             if (written > len)
961                 throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written);
962             
963             _fields.putLongField(HttpHeader.CONTENT_LENGTH, len);
964             if (isAllContentWritten(written))
965             {
966                 try
967                 {
968                     closeOutput();
969                 }
970                 catch(IOException e)
971                 {
972                     throw new RuntimeIOException(e);
973                 }
974             }
975         }
976         else if (_contentLength==0)
977         {
978             long written = _out.getWritten();
979             if (written > 0)
980                 throw new IllegalArgumentException("setContentLength(0) when already written " + written);
981             _fields.put(HttpHeader.CONTENT_LENGTH, "0");
982         }
983         else
984             _fields.remove(HttpHeader.CONTENT_LENGTH);
985     }
986     
987     public long getContentLength()
988     {
989         return _contentLength;
990     }
991
992     public boolean isAllContentWritten(long written)
993     {
994         return (_contentLength >= 0 && written >= _contentLength);
995     }
996
997     public void closeOutput() throws IOException
998     {
999         switch (_outputType)
1000         {
1001             case WRITER:
1002                 _writer.close();
1003                 if (!_out.isClosed())
1004                     _out.close();
1005                 break;
1006             case STREAM:
1007                 getOutputStream().close();
1008                 break;
1009             default:
1010                 _out.close();
1011         }
1012     }
1013
1014     public long getLongContentLength()
1015     {
1016         return _contentLength;
1017     }
1018
1019     public void setLongContentLength(long len)
1020     {
1021         // Protect from setting after committed as default handling
1022         // of a servlet HEAD request ALWAYS sets _content length, even
1023         // if the getHandling committed the response!
1024         if (isCommitted() || isIncluding())
1025             return;
1026         _contentLength = len;
1027         _fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
1028     }
1029     
1030     @Override
1031     public void setContentLengthLong(long length)
1032     {
1033         setLongContentLength(length);
1034     }
1035
1036     @Override
1037     public void setCharacterEncoding(String encoding)
1038     {
1039         setCharacterEncoding(encoding,true);
1040     }
1041     
1042     private void setCharacterEncoding(String encoding, boolean explicit)
1043     {
1044         if (isIncluding() || isWriting())
1045             return;
1046
1047         if (_outputType == OutputType.NONE && !isCommitted())
1048         {
1049             if (encoding == null)
1050             {
1051                 _explicitEncoding=false;
1052                 
1053                 // Clear any encoding.
1054                 if (_characterEncoding != null)
1055                 {
1056                     _characterEncoding = null;
1057                     
1058                     if (_mimeType!=null)
1059                     {
1060                         _mimeType=_mimeType.getBaseType();
1061                         _contentType=_mimeType.asString();
1062                         _fields.put(_mimeType.getContentTypeField());
1063                     }
1064                     else if (_contentType != null)
1065                     {
1066                         _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
1067                         _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1068                     }
1069                 }
1070             }
1071             else
1072             {
1073                 // No, so just add this one to the mimetype
1074                 _explicitEncoding = explicit;
1075                 _characterEncoding = HttpGenerator.__STRICT?encoding:StringUtil.normalizeCharset(encoding);
1076                 if (_mimeType!=null)
1077                 {
1078                     _contentType=_mimeType.getBaseType().asString()+ "; charset=" + _characterEncoding;
1079                     _mimeType = MimeTypes.CACHE.get(_contentType);
1080                     if (_mimeType==null || HttpGenerator.__STRICT)
1081                         _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1082                     else
1083                         _fields.put(_mimeType.getContentTypeField());
1084                 }
1085                 else if (_contentType != null)
1086                 {
1087                     _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType) + "; charset=" + _characterEncoding;
1088                     _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1089                 }
1090             }
1091         }
1092     }
1093
1094     @Override
1095     public void setContentType(String contentType)
1096     {
1097         if (isCommitted() || isIncluding())
1098             return;
1099
1100         if (contentType == null)
1101         {
1102             if (isWriting() && _characterEncoding != null)
1103                 throw new IllegalSelectorException();
1104
1105             if (_locale == null)
1106                 _characterEncoding = null;
1107             _mimeType = null;
1108             _contentType = null;
1109             _fields.remove(HttpHeader.CONTENT_TYPE);
1110         }
1111         else
1112         {
1113             _contentType = contentType;
1114             _mimeType = MimeTypes.CACHE.get(contentType);
1115             
1116             String charset;
1117             if (_mimeType!=null && _mimeType.getCharset()!=null && !_mimeType.isCharsetAssumed())
1118                 charset=_mimeType.getCharset().toString();
1119             else
1120                 charset = MimeTypes.getCharsetFromContentType(contentType);
1121
1122             if (charset == null)
1123             {
1124                 if (_characterEncoding != null)
1125                 {
1126                     _contentType = contentType + "; charset=" + _characterEncoding;
1127                     _mimeType = null;
1128                 }
1129             }
1130             else if (isWriting() && !charset.equals(_characterEncoding))
1131             {
1132                 // too late to change the character encoding;
1133                 _mimeType = null;
1134                 _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
1135                 if (_characterEncoding != null)
1136                     _contentType = _contentType + "; charset=" + _characterEncoding;
1137             }
1138             else
1139             {
1140                 _characterEncoding = charset;
1141                 _explicitEncoding = true;
1142             }
1143
1144             if (HttpGenerator.__STRICT || _mimeType==null)
1145                 _fields.put(HttpHeader.CONTENT_TYPE, _contentType);
1146             else
1147             {
1148                 _contentType=_mimeType.asString();
1149                 _fields.put(_mimeType.getContentTypeField());
1150             }
1151         }
1152         
1153     }
1154
1155     @Override
1156     public void setBufferSize(int size)
1157     {
1158         if (isCommitted() || getContentCount() > 0)
1159             throw new IllegalStateException("Committed or content written");
1160         if (size <= 0)
1161             size = __MIN_BUFFER_SIZE;
1162         _out.setBufferSize(size);
1163     }
1164
1165     @Override
1166     public int getBufferSize()
1167     {
1168         return _out.getBufferSize();
1169     }
1170
1171     @Override
1172     public void flushBuffer() throws IOException
1173     {
1174         if (!_out.isClosed())
1175             _out.flush();
1176     }
1177
1178     @Override
1179     public void reset()
1180     {
1181         resetForForward();
1182         _status = 200;
1183         _reason = null;
1184         _contentLength = -1;
1185         _fields.clear();
1186
1187         String connection = _channel.getRequest().getHttpFields().getStringField(HttpHeader.CONNECTION);
1188         if (connection != null)
1189         {
1190             for (String value: StringUtil.csvSplit(null,connection,0,connection.length()))
1191             {
1192                 HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value);
1193
1194                 if (cb != null)
1195                 {
1196                     switch (cb)
1197                     {
1198                         case CLOSE:
1199                             _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString());
1200                             break;
1201
1202                         case KEEP_ALIVE:
1203                             if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol()))
1204                                 _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString());
1205                             break;
1206                         case TE:
1207                             _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString());
1208                             break;
1209                         default:
1210                     }
1211                 }
1212             }
1213         }
1214     }
1215
1216     public void reset(boolean preserveCookies)
1217     { 
1218         if (!preserveCookies)
1219             reset();
1220         else
1221         {
1222             ArrayList<String> cookieValues = new ArrayList<String>(5);
1223             Enumeration<String> vals = _fields.getValues(HttpHeader.SET_COOKIE.asString());
1224             while (vals.hasMoreElements())
1225                 cookieValues.add(vals.nextElement());
1226             reset();
1227             for (String v:cookieValues)
1228                 _fields.add(HttpHeader.SET_COOKIE, v);
1229         }
1230     }
1231
1232     public void resetForForward()
1233     {
1234         resetBuffer();
1235         _outputType = OutputType.NONE;
1236     }
1237
1238     @Override
1239     public void resetBuffer()
1240     {
1241         if (isCommitted())
1242             throw new IllegalStateException("Committed");
1243
1244         switch (_outputType)
1245         {
1246             case STREAM:
1247             case WRITER:
1248                 _out.reset();
1249                 break;
1250             default:
1251         }
1252
1253         _out.resetBuffer();
1254     }
1255
1256     protected ResponseInfo newResponseInfo()
1257     {
1258         return new ResponseInfo(_channel.getRequest().getHttpVersion(), _fields, getLongContentLength(), getStatus(), getReason(), _channel.getRequest().isHead());
1259     }
1260
1261     @Override
1262     public boolean isCommitted()
1263     {
1264         return _channel.isCommitted();
1265     }
1266
1267     @Override
1268     public void setLocale(Locale locale)
1269     {
1270         if (locale == null || isCommitted() || isIncluding())
1271             return;
1272
1273         _locale = locale;
1274         _fields.put(HttpHeader.CONTENT_LANGUAGE, locale.toString().replace('_', '-'));
1275
1276         if (_outputType != OutputType.NONE)
1277             return;
1278
1279         if (_channel.getRequest().getContext() == null)
1280             return;
1281
1282         String charset = _channel.getRequest().getContext().getContextHandler().getLocaleEncoding(locale);
1283
1284         if (charset != null && charset.length() > 0 && !_explicitEncoding)
1285             setCharacterEncoding(charset,false);
1286     }
1287
1288     @Override
1289     public Locale getLocale()
1290     {
1291         if (_locale == null)
1292             return Locale.getDefault();
1293         return _locale;
1294     }
1295
1296     @Override
1297     public int getStatus()
1298     {
1299         return _status;
1300     }
1301
1302     public String getReason()
1303     {
1304         return _reason;
1305     }
1306
1307     public HttpFields getHttpFields()
1308     {
1309         return _fields;
1310     }
1311
1312     public long getContentCount()
1313     {
1314         return _out.getWritten();
1315     }
1316
1317     @Override
1318     public String toString()
1319     {
1320         return String.format("%s %d %s%n%s", _channel.getRequest().getHttpVersion(), _status, _reason == null ? "" : _reason, _fields);
1321     }
1322     
1323
1324     private static class ResponseWriter extends PrintWriter
1325     {
1326         private final String _encoding;
1327         private final HttpWriter _httpWriter;
1328         
1329         public ResponseWriter(HttpWriter httpWriter,String encoding)
1330         {
1331             super(httpWriter);
1332             _httpWriter=httpWriter;
1333             _encoding=encoding;
1334         }
1335
1336         public boolean isFor(String encoding)
1337         {
1338             return _encoding.equalsIgnoreCase(encoding);
1339         }
1340         
1341         protected void reopen()
1342         {
1343             super.clearError();
1344             out=_httpWriter;
1345         }
1346     }
1347 }