View Javadoc

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