blob: 526c662a5c462e4e98b36f08e47ab81d5c38cc73 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import static org.eclipse.jetty.util.QuotedStringTokenizer.isQuoted;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.IllegalSelectorException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpGenerator.ResponseInfo;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.ByteArrayISO8859Writer;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>{@link Response} provides the implementation for {@link HttpServletResponse}.</p>
*/
public class Response implements HttpServletResponse
{
private static final Logger LOG = Log.getLogger(Response.class);
private static final String __COOKIE_DELIM="\",;\\ \t";
private final static String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim();
private final static int __MIN_BUFFER_SIZE = 1;
// Cookie building buffer. Reduce garbage for cookie using applications
private static final ThreadLocal<StringBuilder> __cookieBuilder = new ThreadLocal<StringBuilder>()
{
@Override
protected StringBuilder initialValue()
{
return new StringBuilder(128);
}
};
/* ------------------------------------------------------------ */
public static Response getResponse(HttpServletResponse response)
{
if (response instanceof Response)
return (Response)response;
return HttpChannel.getCurrentHttpChannel().getResponse();
}
public enum OutputType
{
NONE, STREAM, WRITER
}
/**
* If a header name starts with this string, the header (stripped of the prefix)
* can be set during include using only {@link #setHeader(String, String)} or
* {@link #addHeader(String, String)}.
*/
public final static String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include.";
/**
* If this string is found within the comment of a cookie added with {@link #addCookie(Cookie)}, then the cookie
* will be set as HTTP ONLY.
*/
public final static String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
private final HttpChannel<?> _channel;
private final HttpFields _fields = new HttpFields();
private final AtomicInteger _include = new AtomicInteger();
private HttpOutput _out;
private int _status = HttpStatus.OK_200;
private String _reason;
private Locale _locale;
private MimeTypes.Type _mimeType;
private String _characterEncoding;
private boolean _explicitEncoding;
private String _contentType;
private OutputType _outputType = OutputType.NONE;
private ResponseWriter _writer;
private long _contentLength = -1;
public Response(HttpChannel<?> channel, HttpOutput out)
{
_channel = channel;
_out = out;
}
protected HttpChannel<?> getHttpChannel()
{
return _channel;
}
protected void recycle()
{
_status = HttpStatus.OK_200;
_reason = null;
_locale = null;
_mimeType = null;
_characterEncoding = null;
_contentType = null;
_outputType = OutputType.NONE;
_contentLength = -1;
_out.reset();
_fields.clear();
_explicitEncoding=false;
}
public void setHeaders(HttpContent httpContent)
{
Response response = _channel.getResponse();
String contentType = httpContent.getContentType();
if (contentType != null && !response.getHttpFields().containsKey(HttpHeader.CONTENT_TYPE.asString()))
setContentType(contentType);
if (httpContent.getContentLength() > 0)
setLongContentLength(httpContent.getContentLength());
String lm = httpContent.getLastModified();
if (lm != null)
response.getHttpFields().put(HttpHeader.LAST_MODIFIED, lm);
else if (httpContent.getResource() != null)
{
long lml = httpContent.getResource().lastModified();
if (lml != -1)
response.getHttpFields().putDateField(HttpHeader.LAST_MODIFIED, lml);
}
String etag=httpContent.getETag();
if (etag!=null)
response.getHttpFields().put(HttpHeader.ETAG,etag);
}
public HttpOutput getHttpOutput()
{
return _out;
}
public void setHttpOutput(HttpOutput out)
{
_out=out;
}
public boolean isIncluding()
{
return _include.get() > 0;
}
public void include()
{
_include.incrementAndGet();
}
public void included()
{
_include.decrementAndGet();
if (_outputType == OutputType.WRITER)
{
_writer.reopen();
}
_out.reopen();
}
public void addCookie(HttpCookie cookie)
{
addSetCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.getComment(),
cookie.isSecure(),
cookie.isHttpOnly(),
cookie.getVersion());;
}
@Override
public void addCookie(Cookie cookie)
{
String comment = cookie.getComment();
boolean httpOnly = false;
if (comment != null)
{
int i = comment.indexOf(HTTP_ONLY_COMMENT);
if (i >= 0)
{
httpOnly = true;
comment = comment.replace(HTTP_ONLY_COMMENT, "").trim();
if (comment.length() == 0)
comment = null;
}
}
addSetCookie(cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
comment,
cookie.getSecure(),
httpOnly || cookie.isHttpOnly(),
cookie.getVersion());
}
/**
* Format a set cookie value
*
* @param name the name
* @param value the value
* @param domain the domain
* @param path the path
* @param maxAge the maximum age
* @param comment the comment (only present on versions > 0)
* @param isSecure true if secure cookie
* @param isHttpOnly true if for http only
* @param version version of cookie logic to use (0 == default behavior)
*/
public void addSetCookie(
final String name,
final String value,
final String domain,
final String path,
final long maxAge,
final String comment,
final boolean isSecure,
final boolean isHttpOnly,
int version)
{
// Check arguments
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
// Format value and params
StringBuilder buf = __cookieBuilder.get();
buf.setLength(0);
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
boolean quote_name=isQuoteNeededForCookie(name);
quoteOnlyOrAppend(buf,name,quote_name);
buf.append('=');
// Remember name= part to look for other matching set-cookie
String name_equals=buf.toString();
// Append the value
boolean quote_value=isQuoteNeededForCookie(value);
quoteOnlyOrAppend(buf,value,quote_value);
// Look for domain and path fields and check if they need to be quoted
boolean has_domain = domain!=null && domain.length()>0;
boolean quote_domain = has_domain && isQuoteNeededForCookie(domain);
boolean has_path = path!=null && path.length()>0;
boolean quote_path = has_path && isQuoteNeededForCookie(path);
// Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
if (version==0 && ( comment!=null || quote_name || quote_value || quote_domain || quote_path || isQuoted(name) || isQuoted(value) || isQuoted(path) || isQuoted(domain)))
version=1;
// Append version
if (version==1)
buf.append (";Version=1");
else if (version>1)
buf.append (";Version=").append(version);
// Append path
if (has_path)
{
buf.append(";Path=");
quoteOnlyOrAppend(buf,path,quote_path);
}
// Append domain
if (has_domain)
{
buf.append(";Domain=");
quoteOnlyOrAppend(buf,domain,quote_domain);
}
// Handle max-age and/or expires
if (maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
buf.append(";Expires=");
if (maxAge == 0)
buf.append(__01Jan1970_COOKIE);
else
DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);
// for v1 cookies, also send max-age
if (version>=1)
{
buf.append(";Max-Age=");
buf.append(maxAge);
}
}
// add the other fields
if (isSecure)
buf.append(";Secure");
if (isHttpOnly)
buf.append(";HttpOnly");
if (comment != null)
{
buf.append(";Comment=");
quoteOnlyOrAppend(buf,comment,isQuoteNeededForCookie(comment));
}
// remove any existing set-cookie fields of same name
Iterator<HttpField> i=_fields.iterator();
while (i.hasNext())
{
HttpField field=i.next();
if (field.getHeader()==HttpHeader.SET_COOKIE)
{
String val = field.getValue();
if (val!=null && val.startsWith(name_equals))
{
//existing cookie has same name, does it also match domain and path?
if (((!has_domain && !val.contains("Domain")) || (has_domain && val.contains(domain))) &&
((!has_path && !val.contains("Path")) || (has_path && val.contains(path))))
{
i.remove();
}
}
}
}
// add the set cookie
_fields.add(HttpHeader.SET_COOKIE.toString(), buf.toString());
// Expire responses with set-cookie headers so they do not get cached.
_fields.put(HttpHeader.EXPIRES.toString(), DateGenerator.__01Jan1970);
}
/* ------------------------------------------------------------ */
/** Does a cookie value need to be quoted?
* @param s value string
* @return true if quoted;
* @throws IllegalArgumentException If there a control characters in the string
*/
private static boolean isQuoteNeededForCookie(String s)
{
if (s==null || s.length()==0)
return true;
if (QuotedStringTokenizer.isQuoted(s))
return false;
for (int i=0;i<s.length();i++)
{
char c = s.charAt(i);
if (__COOKIE_DELIM.indexOf(c)>=0)
return true;
if (c<0x20 || c>=0x7f)
throw new IllegalArgumentException("Illegal character in cookie value");
}
return false;
}
private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
{
if (quote)
QuotedStringTokenizer.quoteOnly(buf,s);
else
buf.append(s);
}
@Override
public boolean containsHeader(String name)
{
return _fields.containsKey(name);
}
@Override
public String encodeURL(String url)
{
final Request request = _channel.getRequest();
SessionManager sessionManager = request.getSessionManager();
if (sessionManager == null)
return url;
HttpURI uri = null;
if (sessionManager.isCheckingRemoteSessionIdEncoding() && URIUtil.hasScheme(url))
{
uri = new HttpURI(url);
String path = uri.getPath();
path = (path == null ? "" : path);
int port = uri.getPort();
if (port < 0)
port = HttpScheme.HTTPS.asString().equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
if (!request.getServerName().equalsIgnoreCase(uri.getHost()) ||
request.getServerPort() != port ||
!path.startsWith(request.getContextPath())) //TODO the root context path is "", with which every non null string starts
return url;
}
String sessionURLPrefix = sessionManager.getSessionIdPathParameterNamePrefix();
if (sessionURLPrefix == null)
return url;
if (url == null)
return null;
// should not encode if cookies in evidence
if ((sessionManager.isUsingCookies() && request.isRequestedSessionIdFromCookie()) || !sessionManager.isUsingURLs())
{
int prefix = url.indexOf(sessionURLPrefix);
if (prefix != -1)
{
int suffix = url.indexOf("?", prefix);
if (suffix < 0)
suffix = url.indexOf("#", prefix);
if (suffix <= prefix)
return url.substring(0, prefix);
return url.substring(0, prefix) + url.substring(suffix);
}
return url;
}
// get session;
HttpSession session = request.getSession(false);
// no session
if (session == null)
return url;
// invalid session
if (!sessionManager.isValid(session))
return url;
String id = sessionManager.getNodeId(session);
if (uri == null)
uri = new HttpURI(url);
// Already encoded
int prefix = url.indexOf(sessionURLPrefix);
if (prefix != -1)
{
int suffix = url.indexOf("?", prefix);
if (suffix < 0)
suffix = url.indexOf("#", prefix);
if (suffix <= prefix)
return url.substring(0, prefix + sessionURLPrefix.length()) + id;
return url.substring(0, prefix + sessionURLPrefix.length()) + id +
url.substring(suffix);
}
// edit the session
int suffix = url.indexOf('?');
if (suffix < 0)
suffix = url.indexOf('#');
if (suffix < 0)
{
return url +
((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path, insert the root path
sessionURLPrefix + id;
}
return url.substring(0, suffix) +
((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && uri.getPath() == null ? "/" : "") + //if no path so insert the root path
sessionURLPrefix + id + url.substring(suffix);
}
@Override
public String encodeRedirectURL(String url)
{
return encodeURL(url);
}
@Override
@Deprecated
public String encodeUrl(String url)
{
return encodeURL(url);
}
@Override
@Deprecated
public String encodeRedirectUrl(String url)
{
return encodeRedirectURL(url);
}
@Override
public void sendError(int sc) throws IOException
{
sendError(sc, null);
}
@Override
public void sendError(int code, String message) throws IOException
{
if (isIncluding())
return;
switch(code)
{
case -1:
_channel.abort();
return;
case 102:
sendProcessing();
return;
default:
}
if (isCommitted())
LOG.warn("Committed before "+code+" "+message);
resetBuffer();
_characterEncoding=null;
setHeader(HttpHeader.EXPIRES,null);
setHeader(HttpHeader.LAST_MODIFIED,null);
setHeader(HttpHeader.CACHE_CONTROL,null);
setHeader(HttpHeader.CONTENT_TYPE,null);
setHeader(HttpHeader.CONTENT_LENGTH,null);
_outputType = OutputType.NONE;
setStatus(code);
_reason=message;
Request request = _channel.getRequest();
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
if (message==null)
message=cause==null?HttpStatus.getMessage(code):cause.toString();
// If we are allowed to have a body
if (code!=SC_NO_CONTENT &&
code!=SC_NOT_MODIFIED &&
code!=SC_PARTIAL_CONTENT &&
code>=SC_OK)
{
ErrorHandler error_handler = ErrorHandler.getErrorHandler(_channel.getServer(),request.getContext()==null?null:request.getContext().getContextHandler());
if (error_handler!=null)
{
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,new Integer(code));
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,request.getServletName());
error_handler.handle(null,_channel.getRequest(),_channel.getRequest(),this );
}
else
{
setHeader(HttpHeader.CACHE_CONTROL, "must-revalidate,no-cache,no-store");
setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString());
try (ByteArrayISO8859Writer writer= new ByteArrayISO8859Writer(2048);)
{
message=StringUtil.sanitizeXmlString(message);
String uri= request.getRequestURI();
uri=StringUtil.sanitizeXmlString(uri);
writer.write("<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\"/>\n");
writer.write("<title>Error ");
writer.write(Integer.toString(code));
writer.write(' ');
if (message==null)
writer.write(message);
writer.write("</title>\n</head>\n<body>\n<h2>HTTP ERROR: ");
writer.write(Integer.toString(code));
writer.write("</h2>\n<p>Problem accessing ");
writer.write(uri);
writer.write(". Reason:\n<pre> ");
writer.write(message);
writer.write("</pre>");
writer.write("</p>\n<hr /><i><small>Powered by Jetty://</small></i>");
writer.write("\n</body>\n</html>\n");
writer.flush();
setContentLength(writer.size());
try (ServletOutputStream outputStream = getOutputStream())
{
writer.writeTo(outputStream);
writer.destroy();
}
}
}
}
else if (code!=SC_PARTIAL_CONTENT)
{
// TODO work out why this is required?
_channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_TYPE);
_channel.getRequest().getHttpFields().remove(HttpHeader.CONTENT_LENGTH);
_characterEncoding=null;
_mimeType=null;
}
closeOutput();
}
/**
* Sends a 102-Processing response.
* If the connection is a HTTP connection, the version is 1.1 and the
* request has a Expect header starting with 102, then a 102 response is
* sent. This indicates that the request still be processed and real response
* can still be sent. This method is called by sendError if it is passed 102.
* @see javax.servlet.http.HttpServletResponse#sendError(int)
*/
public void sendProcessing() throws IOException
{
if (_channel.isExpecting102Processing() && !isCommitted())
{
_channel.sendResponse(HttpGenerator.PROGRESS_102_INFO, null, true);
}
}
/**
* Sends a response with one of the 300 series redirection codes.
* @param code
* @param location
* @throws IOException
*/
public void sendRedirect(int code, String location) throws IOException
{
if ((code < HttpServletResponse.SC_MULTIPLE_CHOICES) || (code >= HttpServletResponse.SC_BAD_REQUEST))
throw new IllegalArgumentException("Not a 3xx redirect code");
if (isIncluding())
return;
if (location == null)
throw new IllegalArgumentException();
if (!URIUtil.hasScheme(location))
{
StringBuilder buf = _channel.getRequest().getRootURL();
if (location.startsWith("/"))
{
// absolute in context
location=URIUtil.canonicalPath(location);
}
else
{
// relative to request
String path=_channel.getRequest().getRequestURI();
String parent=(path.endsWith("/"))?path:URIUtil.parentPath(path);
location=URIUtil.canonicalPath(URIUtil.addPaths(parent,location));
if (!location.startsWith("/"))
buf.append('/');
}
if(location==null)
throw new IllegalStateException("path cannot be above root");
buf.append(location);
location=buf.toString();
}
resetBuffer();
setHeader(HttpHeader.LOCATION, location);
setStatus(code);
closeOutput();
}
@Override
public void sendRedirect(String location) throws IOException
{
sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location);
}
@Override
public void setDateHeader(String name, long date)
{
if (!isIncluding())
_fields.putDateField(name, date);
}
@Override
public void addDateHeader(String name, long date)
{
if (!isIncluding())
_fields.addDateField(name, date);
}
public void setHeader(HttpHeader name, String value)
{
if (HttpHeader.CONTENT_TYPE == name)
setContentType(value);
else
{
if (isIncluding())
return;
_fields.put(name, value);
if (HttpHeader.CONTENT_LENGTH == name)
{
if (value == null)
_contentLength = -1l;
else
_contentLength = Long.parseLong(value);
}
}
}
@Override
public void setHeader(String name, String value)
{
if (HttpHeader.CONTENT_TYPE.is(name))
setContentType(value);
else
{
if (isIncluding())
{
if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
else
return;
}
_fields.put(name, value);
if (HttpHeader.CONTENT_LENGTH.is(name))
{
if (value == null)
_contentLength = -1l;
else
_contentLength = Long.parseLong(value);
}
}
}
@Override
public Collection<String> getHeaderNames()
{
final HttpFields fields = _fields;
return fields.getFieldNamesCollection();
}
@Override
public String getHeader(String name)
{
return _fields.getStringField(name);
}
@Override
public Collection<String> getHeaders(String name)
{
final HttpFields fields = _fields;
Collection<String> i = fields.getValuesList(name);
if (i == null)
return Collections.emptyList();
return i;
}
@Override
public void addHeader(String name, String value)
{
if (isIncluding())
{
if (name.startsWith(SET_INCLUDE_HEADER_PREFIX))
name = name.substring(SET_INCLUDE_HEADER_PREFIX.length());
else
return;
}
if (HttpHeader.CONTENT_TYPE.is(name))
{
setContentType(value);
return;
}
if (HttpHeader.CONTENT_LENGTH.is(name))
{
setHeader(name,value);
return;
}
_fields.add(name, value);
}
@Override
public void setIntHeader(String name, int value)
{
if (!isIncluding())
{
_fields.putLongField(name, value);
if (HttpHeader.CONTENT_LENGTH.is(name))
_contentLength = value;
}
}
@Override
public void addIntHeader(String name, int value)
{
if (!isIncluding())
{
_fields.add(name, Integer.toString(value));
if (HttpHeader.CONTENT_LENGTH.is(name))
_contentLength = value;
}
}
@Override
public void setStatus(int sc)
{
if (sc <= 0)
throw new IllegalArgumentException();
if (!isIncluding())
{
_status = sc;
_reason = null;
}
}
@Override
@Deprecated
public void setStatus(int sc, String sm)
{
setStatusWithReason(sc,sm);
}
public void setStatusWithReason(int sc, String sm)
{
if (sc <= 0)
throw new IllegalArgumentException();
if (!isIncluding())
{
_status = sc;
_reason = sm;
}
}
@Override
public String getCharacterEncoding()
{
if (_characterEncoding == null)
_characterEncoding = StringUtil.__ISO_8859_1;
return _characterEncoding;
}
@Override
public String getContentType()
{
return _contentType;
}
@Override
public ServletOutputStream getOutputStream() throws IOException
{
if (_outputType == OutputType.WRITER)
throw new IllegalStateException("WRITER");
_outputType = OutputType.STREAM;
return _out;
}
public boolean isWriting()
{
return _outputType == OutputType.WRITER;
}
@Override
public PrintWriter getWriter() throws IOException
{
if (_outputType == OutputType.STREAM)
throw new IllegalStateException("STREAM");
if (_outputType == OutputType.NONE)
{
/* get encoding from Content-Type header */
String encoding = _characterEncoding;
if (encoding == null)
{
if (_mimeType!=null && _mimeType.isCharsetAssumed())
encoding=_mimeType.getCharset().toString();
else
{
encoding = MimeTypes.inferCharsetFromContentType(_contentType);
if (encoding == null)
encoding = StringUtil.__ISO_8859_1;
setCharacterEncoding(encoding,false);
}
}
if (_writer != null && _writer.isFor(encoding))
_writer.reopen();
else
{
if (StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
_writer = new ResponseWriter(new Iso88591HttpWriter(_out),encoding);
else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
_writer = new ResponseWriter(new Utf8HttpWriter(_out),encoding);
else
_writer = new ResponseWriter(new EncodingHttpWriter(_out, encoding),encoding);
}
// Set the output type at the end, because setCharacterEncoding() checks for it
_outputType = OutputType.WRITER;
}
return _writer;
}
@Override
public void setContentLength(int len)
{
// Protect from setting after committed as default handling
// of a servlet HEAD request ALWAYS sets _content length, even
// if the getHandling committed the response!
if (isCommitted() || isIncluding())
return;
_contentLength = len;
if (_contentLength > 0)
{
long written = _out.getWritten();
if (written > len)
throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written);
_fields.putLongField(HttpHeader.CONTENT_LENGTH, len);
if (isAllContentWritten(written))
{
try
{
closeOutput();
}
catch(IOException e)
{
throw new RuntimeIOException(e);
}
}
}
else if (_contentLength==0)
{
long written = _out.getWritten();
if (written > 0)
throw new IllegalArgumentException("setContentLength(0) when already written " + written);
_fields.put(HttpHeader.CONTENT_LENGTH, "0");
}
else
_fields.remove(HttpHeader.CONTENT_LENGTH);
}
public long getContentLength()
{
return _contentLength;
}
public boolean isAllContentWritten(long written)
{
return (_contentLength >= 0 && written >= _contentLength);
}
public void closeOutput() throws IOException
{
switch (_outputType)
{
case WRITER:
_writer.close();
if (!_out.isClosed())
_out.close();
break;
case STREAM:
getOutputStream().close();
break;
default:
_out.close();
}
}
public long getLongContentLength()
{
return _contentLength;
}
public void setLongContentLength(long len)
{
// Protect from setting after committed as default handling
// of a servlet HEAD request ALWAYS sets _content length, even
// if the getHandling committed the response!
if (isCommitted() || isIncluding())
return;
_contentLength = len;
_fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
}
@Override
public void setContentLengthLong(long length)
{
setLongContentLength(length);
}
@Override
public void setCharacterEncoding(String encoding)
{
setCharacterEncoding(encoding,true);
}
private void setCharacterEncoding(String encoding, boolean explicit)
{
if (isIncluding() || isWriting())
return;
if (_outputType == OutputType.NONE && !isCommitted())
{
if (encoding == null)
{
_explicitEncoding=false;
// Clear any encoding.
if (_characterEncoding != null)
{
_characterEncoding = null;
if (_mimeType!=null)
{
_mimeType=_mimeType.getBaseType();
_contentType=_mimeType.asString();
_fields.put(_mimeType.getContentTypeField());
}
else if (_contentType != null)
{
_contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
_fields.put(HttpHeader.CONTENT_TYPE, _contentType);
}
}
}
else
{
// No, so just add this one to the mimetype
_explicitEncoding = explicit;
_characterEncoding = HttpGenerator.__STRICT?encoding:StringUtil.normalizeCharset(encoding);
if (_mimeType!=null)
{
_contentType=_mimeType.getBaseType().asString()+ "; charset=" + _characterEncoding;
_mimeType = MimeTypes.CACHE.get(_contentType);
if (_mimeType==null || HttpGenerator.__STRICT)
_fields.put(HttpHeader.CONTENT_TYPE, _contentType);
else
_fields.put(_mimeType.getContentTypeField());
}
else if (_contentType != null)
{
_contentType = MimeTypes.getContentTypeWithoutCharset(_contentType) + "; charset=" + _characterEncoding;
_fields.put(HttpHeader.CONTENT_TYPE, _contentType);
}
}
}
}
@Override
public void setContentType(String contentType)
{
if (isCommitted() || isIncluding())
return;
if (contentType == null)
{
if (isWriting() && _characterEncoding != null)
throw new IllegalSelectorException();
if (_locale == null)
_characterEncoding = null;
_mimeType = null;
_contentType = null;
_fields.remove(HttpHeader.CONTENT_TYPE);
}
else
{
_contentType = contentType;
_mimeType = MimeTypes.CACHE.get(contentType);
String charset;
if (_mimeType!=null && _mimeType.getCharset()!=null && !_mimeType.isCharsetAssumed())
charset=_mimeType.getCharset().toString();
else
charset = MimeTypes.getCharsetFromContentType(contentType);
if (charset == null)
{
if (_characterEncoding != null)
{
_contentType = contentType + "; charset=" + _characterEncoding;
_mimeType = null;
}
}
else if (isWriting() && !charset.equals(_characterEncoding))
{
// too late to change the character encoding;
_mimeType = null;
_contentType = MimeTypes.getContentTypeWithoutCharset(_contentType);
if (_characterEncoding != null)
_contentType = _contentType + "; charset=" + _characterEncoding;
}
else
{
_characterEncoding = charset;
_explicitEncoding = true;
}
if (HttpGenerator.__STRICT || _mimeType==null)
_fields.put(HttpHeader.CONTENT_TYPE, _contentType);
else
{
_contentType=_mimeType.asString();
_fields.put(_mimeType.getContentTypeField());
}
}
}
@Override
public void setBufferSize(int size)
{
if (isCommitted() || getContentCount() > 0)
throw new IllegalStateException("Committed or content written");
if (size <= 0)
size = __MIN_BUFFER_SIZE;
_out.setBufferSize(size);
}
@Override
public int getBufferSize()
{
return _out.getBufferSize();
}
@Override
public void flushBuffer() throws IOException
{
if (!_out.isClosed())
_out.flush();
}
@Override
public void reset()
{
resetForForward();
_status = 200;
_reason = null;
_contentLength = -1;
_fields.clear();
String connection = _channel.getRequest().getHttpFields().getStringField(HttpHeader.CONNECTION);
if (connection != null)
{
for (String value: StringUtil.csvSplit(null,connection,0,connection.length()))
{
HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value);
if (cb != null)
{
switch (cb)
{
case CLOSE:
_fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString());
break;
case KEEP_ALIVE:
if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol()))
_fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString());
break;
case TE:
_fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString());
break;
default:
}
}
}
}
}
public void reset(boolean preserveCookies)
{
if (!preserveCookies)
reset();
else
{
ArrayList<String> cookieValues = new ArrayList<String>(5);
Enumeration<String> vals = _fields.getValues(HttpHeader.SET_COOKIE.asString());
while (vals.hasMoreElements())
cookieValues.add(vals.nextElement());
reset();
for (String v:cookieValues)
_fields.add(HttpHeader.SET_COOKIE, v);
}
}
public void resetForForward()
{
resetBuffer();
_outputType = OutputType.NONE;
}
@Override
public void resetBuffer()
{
if (isCommitted())
throw new IllegalStateException("Committed");
switch (_outputType)
{
case STREAM:
case WRITER:
_out.reset();
break;
default:
}
_out.resetBuffer();
}
protected ResponseInfo newResponseInfo()
{
return new ResponseInfo(_channel.getRequest().getHttpVersion(), _fields, getLongContentLength(), getStatus(), getReason(), _channel.getRequest().isHead());
}
@Override
public boolean isCommitted()
{
return _channel.isCommitted();
}
@Override
public void setLocale(Locale locale)
{
if (locale == null || isCommitted() || isIncluding())
return;
_locale = locale;
_fields.put(HttpHeader.CONTENT_LANGUAGE, locale.toString().replace('_', '-'));
if (_outputType != OutputType.NONE)
return;
if (_channel.getRequest().getContext() == null)
return;
String charset = _channel.getRequest().getContext().getContextHandler().getLocaleEncoding(locale);
if (charset != null && charset.length() > 0 && !_explicitEncoding)
setCharacterEncoding(charset,false);
}
@Override
public Locale getLocale()
{
if (_locale == null)
return Locale.getDefault();
return _locale;
}
@Override
public int getStatus()
{
return _status;
}
public String getReason()
{
return _reason;
}
public HttpFields getHttpFields()
{
return _fields;
}
public long getContentCount()
{
return _out.getWritten();
}
@Override
public String toString()
{
return String.format("%s %d %s%n%s", _channel.getRequest().getHttpVersion(), _status, _reason == null ? "" : _reason, _fields);
}
private static class ResponseWriter extends PrintWriter
{
private final String _encoding;
private final HttpWriter _httpWriter;
public ResponseWriter(HttpWriter httpWriter,String encoding)
{
super(httpWriter);
_httpWriter=httpWriter;
_encoding=encoding;
}
public boolean isFor(String encoding)
{
return _encoding.equalsIgnoreCase(encoding);
}
protected void reopen()
{
super.clearError();
out=_httpWriter;
}
}
}