View Javadoc

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