View Javadoc

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