/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.connector;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.ServletOutputStream;
import javax.servlet.SessionTrackingMode;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Session;
import org.apache.catalina.Wrapper;
import org.apache.catalina.security.SecurityUtil;
import org.apache.catalina.util.DateTool;
import org.apache.catalina.util.RequestUtil;
import org.apache.catalina.util.SessionConfig;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.CharChunk;
import org.apache.tomcat.util.buf.UEncoder;
import org.apache.tomcat.util.buf.UEncoder.SafeCharsSet;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.ServerCookie;
import org.apache.tomcat.util.http.parser.MediaTypeCache;
import org.apache.tomcat.util.net.URL;
import org.apache.tomcat.util.res.StringManager;

/**
 * Wrapper object for the Coyote response.
 *
 * @author Remy Maucherat
 * @author Craig R. McClanahan
 */
public class Response implements HttpServletResponse {

	private static final Log log = LogFactory.getLog(Response.class);
	protected static final StringManager sm = StringManager.getManager(Response.class);

	private static final MediaTypeCache MEDIA_TYPE_CACHE = new MediaTypeCache(100);

	/**
	 * Compliance with SRV.15.2.22.1. A call to Response.getWriter() if no
	 * character encoding has been specified will result in subsequent calls to
	 * Response.getCharacterEncoding() returning ISO-8859-1 and the Content-Type
	 * response header will include a charset=ISO-8859-1 component.
	 */
	private static final boolean ENFORCE_ENCODING_IN_GET_WRITER;

	static {
		// Ensure that URL is loaded for SM
		URL.isSchemeChar('c');

		ENFORCE_ENCODING_IN_GET_WRITER = Boolean.parseBoolean(
				System.getProperty("org.apache.catalina.connector.Response.ENFORCE_ENCODING_IN_GET_WRITER", "true"));
	}

	/**
	 * Descriptive information about this Response implementation.
	 */
	protected static final String info = "org.apache.coyote.catalina.CoyoteResponse/1.0";

	// ----------------------------------------------------- Instance Variables

	/**
	 * The date format we will use for creating date headers.
	 */
	protected SimpleDateFormat format = null;

	// ------------------------------------------------------------- Properties

	/**
	 * Associated Catalina connector.
	 * 
	 * @deprecated Unused
	 */
	@Deprecated
	protected Connector connector;

	/**
	 * Return the Connector through which this Request was received.
	 */
	@Deprecated
	public Connector getConnector() {
		return (this.connector);
	}

	/**
	 * Set the Connector through which this Request was received.
	 *
	 * @param connector
	 *            The new connector
	 */
	public void setConnector(Connector connector) {
		this.connector = connector;
		if ("AJP/1.3".equals(connector.getProtocol())) {
			// default size to size of one ajp-packet
			outputBuffer = new OutputBuffer(8184);
		} else {
			outputBuffer = new OutputBuffer();
		}
		outputStream = new CoyoteOutputStream(outputBuffer);
		writer = new CoyoteWriter(outputBuffer);
	}

	/**
	 * Coyote response.
	 */
	protected org.apache.coyote.Response coyoteResponse;

	/**
	 * Set the Coyote response.
	 *
	 * @param coyoteResponse
	 *            The Coyote response
	 */
	public void setCoyoteResponse(org.apache.coyote.Response coyoteResponse) {
		this.coyoteResponse = coyoteResponse;
		outputBuffer.setResponse(coyoteResponse);
	}

	/**
	 * Get the Coyote response.
	 */
	public org.apache.coyote.Response getCoyoteResponse() {
		return (coyoteResponse);
	}

	/**
	 * Return the Context within which this Request is being processed.
	 */
	public Context getContext() {
		return (request.getContext());
	}

	/**
	 * Set the Context within which this Request is being processed. This must
	 * be called as soon as the appropriate Context is identified, because it
	 * identifies the value to be returned by <code>getContextPath()</code>, and
	 * thus enables parsing of the request URI.
	 *
	 * @param context
	 *            The newly associated Context
	 */
	@Deprecated
	public void setContext(Context context) {
		request.setContext(context);
	}

	/**
	 * The associated output buffer.
	 */
	protected OutputBuffer outputBuffer;

	/**
	 * The associated output stream.
	 */
	protected CoyoteOutputStream outputStream;

	/**
	 * The associated writer.
	 */
	protected CoyoteWriter writer;

	/**
	 * The application commit flag.
	 */
	protected boolean appCommitted = false;

	/**
	 * The included flag.
	 */
	protected boolean included = false;

	/**
	 * The characterEncoding flag
	 */
	private boolean isCharacterEncodingSet = false;

	/**
	 * With the introduction of async processing and the possibility of
	 * non-container threads calling sendError() tracking the current error
	 * state and ensuring that the correct error page is called becomes more
	 * complicated. This state attribute helps by tracking the current error
	 * state and informing callers that attempt to change state if the change
	 * was successful or if another thread got there first.
	 *
	 * <pre>
	 * The state machine is very simple:
	 *
	 * 0 - NONE
	 * 1 - NOT_REPORTED
	 * 2 - REPORTED
	 *
	 *
	 *   -->---->-- >NONE
	 *   |   |        |
	 *   |   |        | setError()
	 *   ^   ^        |
	 *   |   |       \|/
	 *   |   |-<-NOT_REPORTED
	 *   |            |
	 *   ^            | report()
	 *   |            |
	 *   |           \|/
	 *   |----<----REPORTED
	 * </pre>
	 */
	private final AtomicInteger errorState = new AtomicInteger(0);

	/**
	 * Using output stream flag.
	 */
	protected boolean usingOutputStream = false;

	/**
	 * Using writer flag.
	 */
	protected boolean usingWriter = false;

	/**
	 * URL encoder.
	 */
	protected final UEncoder urlEncoder = new UEncoder(SafeCharsSet.WITH_SLASH);

	/**
	 * Recyclable buffer to hold the redirect URL.
	 */
	protected CharChunk redirectURLCC = new CharChunk();

	// --------------------------------------------------------- Public Methods

	/**
	 * Release all object references, and initialize instance variables, in
	 * preparation for reuse of this object.
	 */
	public void recycle() {

		outputBuffer.recycle();
		usingOutputStream = false;
		usingWriter = false;
		appCommitted = false;
		included = false;
		errorState.set(0);
		isCharacterEncodingSet = false;

		if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
			if (facade != null) {
				facade.clear();
				facade = null;
			}
			if (outputStream != null) {
				outputStream.clear();
				outputStream = null;
			}
			if (writer != null) {
				writer.clear();
				writer = null;
			}
		} else {
			writer.recycle();
		}

	}

	/**
	 * Clear cached encoders (to save memory for Comet requests).
	 */
	public void clearEncoders() {
		outputBuffer.clearEncoders();
	}

	// ------------------------------------------------------- Response Methods

	/**
	 * Return the number of bytes the application has actually written to the
	 * output stream. This excludes chunking, compression, etc. as well as
	 * headers.
	 */
	public long getContentWritten() {
		return outputBuffer.getContentWritten();
	}

	/**
	 * Return the number of bytes the actually written to the socket. This
	 * includes chunking, compression, etc. but excludes headers.
	 */
	public long getBytesWritten(boolean flush) {
		if (flush) {
			try {
				outputBuffer.flush();
			} catch (IOException ioe) {
				// Ignore - the client has probably closed the connection
			}
		}
		return getCoyoteResponse().getBytesWritten(flush);
	}

	/**
	 * Set the application commit flag.
	 *
	 * @param appCommitted
	 *            The new application committed flag value
	 */
	public void setAppCommitted(boolean appCommitted) {
		this.appCommitted = appCommitted;
	}

	/**
	 * Application commit flag accessor.
	 */
	public boolean isAppCommitted() {
		return (this.appCommitted || isCommitted() || isSuspended()
				|| ((getContentLength() > 0) && (getContentWritten() >= getContentLength())));
	}

	/**
	 * Return the "processing inside an include" flag.
	 */
	@Deprecated
	public boolean getIncluded() {
		return included;
	}

	/**
	 * Set the "processing inside an include" flag.
	 *
	 * @param included
	 *            <code>true</code> if we are currently inside a
	 *            RequestDispatcher.include(), else <code>false</code>
	 */
	@Deprecated
	public void setIncluded(boolean included) {
		this.included = included;
	}

	/**
	 * Return descriptive information about this Response implementation and the
	 * corresponding version number, in the format
	 * <code>&lt;description&gt;/&lt;version&gt;</code>.
	 */
	public String getInfo() {
		return (info);
	}

	/**
	 * The request with which this response is associated.
	 */
	protected Request request = null;

	/**
	 * Return the Request with which this Response is associated.
	 */
	public org.apache.catalina.connector.Request getRequest() {
		return (this.request);
	}

	/**
	 * Set the Request with which this Response is associated.
	 *
	 * @param request
	 *            The new associated request
	 */
	public void setRequest(org.apache.catalina.connector.Request request) {
		this.request = request;
	}

	/**
	 * The facade associated with this response.
	 */
	protected ResponseFacade facade = null;

	/**
	 * Return the <code>ServletResponse</code> for which this object is the
	 * facade.
	 */
	public HttpServletResponse getResponse() {
		if (facade == null) {
			facade = new ResponseFacade(this);
		}
		return (facade);
	}

	/**
	 * Return the output stream associated with this Response.
	 */
	@Deprecated
	public OutputStream getStream() {
		if (outputStream == null) {
			outputStream = new CoyoteOutputStream(outputBuffer);
		}
		return outputStream;
	}

	/**
	 * Set the suspended flag.
	 *
	 * @param suspended
	 *            The new suspended flag value
	 */
	public void setSuspended(boolean suspended) {
		outputBuffer.setSuspended(suspended);
	}

	/**
	 * Suspended flag accessor.
	 */
	public boolean isSuspended() {
		return outputBuffer.isSuspended();
	}

	/**
	 * Closed flag accessor.
	 */
	public boolean isClosed() {
		return outputBuffer.isClosed();
	}

	/**
	 * Set the error flag.
	 */
	public boolean setError() {
		boolean result = errorState.compareAndSet(0, 1);
		if (result) {
			Wrapper wrapper = getRequest().getWrapper();
			if (wrapper != null) {
				wrapper.incrementErrorCount();
			}
		}
		return result;
	}

	/**
	 * Error flag accessor.
	 */
	public boolean isError() {
		return errorState.get() > 0;
	}

	public boolean isErrorReportRequired() {
		return errorState.get() == 1;
	}

	public boolean setErrorReported() {
		return errorState.compareAndSet(1, 2);
	}

	/**
	 * Create and return a ServletOutputStream to write the content associated
	 * with this Response.
	 *
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Deprecated
	public ServletOutputStream createOutputStream() throws IOException {
		// Probably useless
		if (outputStream == null) {
			outputStream = new CoyoteOutputStream(outputBuffer);
		}
		return outputStream;
	}

	/**
	 * Perform whatever actions are required to flush and close the output
	 * stream or writer, in a single operation.
	 *
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	public void finishResponse() throws IOException {
		// Writing leftover bytes
		outputBuffer.close();
	}

	/**
	 * Return the content length that was set or calculated for this Response.
	 */
	public int getContentLength() {
		return getCoyoteResponse().getContentLength();
	}

	/**
	 * Return the content type that was set or calculated for this response, or
	 * <code>null</code> if no content type was set.
	 */
	@Override
	public String getContentType() {
		return getCoyoteResponse().getContentType();
	}

	/**
	 * Return a PrintWriter that can be used to render error messages,
	 * regardless of whether a stream or writer has already been acquired.
	 *
	 * @return Writer which can be used for error reports. If the response is
	 *         not an error report returned using sendError or triggered by an
	 *         unexpected exception thrown during the servlet processing (and
	 *         only in that case), null will be returned if the response stream
	 *         has already been used.
	 *
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	public PrintWriter getReporter() throws IOException {
		if (outputBuffer.isNew()) {
			outputBuffer.checkConverter();
			if (writer == null) {
				writer = new CoyoteWriter(outputBuffer);
			}
			return writer;
		} else {
			return null;
		}
	}

	// ------------------------------------------------ ServletResponse Methods

	/**
	 * Flush the buffer and commit this response.
	 *
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public void flushBuffer() throws IOException {
		outputBuffer.flush();
	}

	/**
	 * Return the actual buffer size used for this Response.
	 */
	@Override
	public int getBufferSize() {
		return outputBuffer.getBufferSize();
	}

	/**
	 * Return the character encoding used for this Response.
	 */
	@Override
	public String getCharacterEncoding() {
		return (getCoyoteResponse().getCharacterEncoding());
	}

	/**
	 * Return the servlet output stream associated with this Response.
	 *
	 * @exception IllegalStateException
	 *                if <code>getWriter</code> has already been called for this
	 *                response
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public ServletOutputStream getOutputStream() throws IOException {

		if (usingWriter) {
			throw new IllegalStateException(sm.getString("coyoteResponse.getOutputStream.ise"));
		}

		usingOutputStream = true;
		if (outputStream == null) {
			outputStream = new CoyoteOutputStream(outputBuffer);
		}
		return outputStream;

	}

	/**
	 * Return the Locale assigned to this response.
	 */
	@Override
	public Locale getLocale() {
		return (getCoyoteResponse().getLocale());
	}

	/**
	 * Return the writer associated with this Response.
	 *
	 * @exception IllegalStateException
	 *                if <code>getOutputStream</code> has already been called
	 *                for this response
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public PrintWriter getWriter() throws IOException {

		if (usingOutputStream) {
			throw new IllegalStateException(sm.getString("coyoteResponse.getWriter.ise"));
		}

		if (ENFORCE_ENCODING_IN_GET_WRITER) {
			/*
			 * If the response's character encoding has not been specified as
			 * described in <code>getCharacterEncoding</code> (i.e., the method
			 * just returns the default value <code>ISO-8859-1</code>),
			 * <code>getWriter</code> updates it to <code>ISO-8859-1</code>
			 * (with the effect that a subsequent call to getContentType() will
			 * include a charset=ISO-8859-1 component which will also be
			 * reflected in the Content-Type response header, thereby satisfying
			 * the Servlet spec requirement that containers must communicate the
			 * character encoding used for the servlet response's writer to the
			 * client).
			 */
			setCharacterEncoding(getCharacterEncoding());
		}

		usingWriter = true;
		outputBuffer.checkConverter();
		if (writer == null) {
			writer = new CoyoteWriter(outputBuffer);
		}
		return writer;
	}

	/**
	 * Has the output of this response already been committed?
	 */
	@Override
	public boolean isCommitted() {
		return getCoyoteResponse().isCommitted();
	}

	/**
	 * Clear any content written to the buffer.
	 *
	 * @exception IllegalStateException
	 *                if this response has already been committed
	 */
	@Override
	public void reset() {
		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		getCoyoteResponse().reset();
		outputBuffer.reset();
		usingOutputStream = false;
		usingWriter = false;
		isCharacterEncodingSet = false;
	}

	/**
	 * Reset the data buffer but not any status or header information.
	 *
	 * @exception IllegalStateException
	 *                if the response has already been committed
	 */
	@Override
	public void resetBuffer() {
		resetBuffer(false);
	}

	/**
	 * Reset the data buffer and the using Writer/Stream flags but not any
	 * status or header information.
	 *
	 * @param resetWriterStreamFlags
	 *            <code>true</code> if the internal <code>usingWriter</code>,
	 *            <code>usingOutputStream</code>,
	 *            <code>isCharacterEncodingSet</code> flags should also be reset
	 *
	 * @exception IllegalStateException
	 *                if the response has already been committed
	 */
	public void resetBuffer(boolean resetWriterStreamFlags) {

		if (isCommitted()) {
			throw new IllegalStateException(sm.getString("coyoteResponse.resetBuffer.ise"));
		}

		outputBuffer.reset(resetWriterStreamFlags);

		if (resetWriterStreamFlags) {
			usingOutputStream = false;
			usingWriter = false;
			isCharacterEncodingSet = false;
		}

	}

	/**
	 * Set the buffer size to be used for this Response.
	 *
	 * @param size
	 *            The new buffer size
	 *
	 * @exception IllegalStateException
	 *                if this method is called after output has been committed
	 *                for this response
	 */
	@Override
	public void setBufferSize(int size) {

		if (isCommitted() || !outputBuffer.isNew()) {
			throw new IllegalStateException(sm.getString("coyoteResponse.setBufferSize.ise"));
		}

		outputBuffer.setBufferSize(size);

	}

	/**
	 * Set the content length (in bytes) for this Response.
	 *
	 * @param length
	 *            The new content length
	 */
	@Override
	public void setContentLength(int length) {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		getCoyoteResponse().setContentLength(length);

	}

	/**
	 * Set the content type for this Response.
	 *
	 * @param type
	 *            The new content type
	 */
	@Override
	public void setContentType(String type) {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		if (type == null) {
			getCoyoteResponse().setContentType(null);
			return;
		}

		String[] m = MEDIA_TYPE_CACHE.parse(type);
		if (m == null) {
			// Invalid - Assume no charset and just pass through whatever
			// the user provided.
			getCoyoteResponse().setContentTypeNoCharset(type);
			return;
		}

		getCoyoteResponse().setContentTypeNoCharset(m[0]);

		if (m[1] != null) {
			// Ignore charset if getWriter() has already been called
			if (!usingWriter) {
				getCoyoteResponse().setCharacterEncoding(m[1]);
				isCharacterEncodingSet = true;
			}
		}
	}

	/*
	 * Overrides the name of the character encoding used in the body of the
	 * request. This method must be called prior to reading request parameters
	 * or reading input using getReader().
	 *
	 * @param charset String containing the name of the character encoding.
	 */
	@Override
	public void setCharacterEncoding(String charset) {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		// Ignore any call made after the getWriter has been invoked
		// The default should be used
		if (usingWriter) {
			return;
		}

		getCoyoteResponse().setCharacterEncoding(charset);
		isCharacterEncodingSet = true;
	}

	/**
	 * Set the Locale that is appropriate for this response, including setting
	 * the appropriate character encoding.
	 *
	 * @param locale
	 *            The new locale
	 */
	@Override
	public void setLocale(Locale locale) {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		getCoyoteResponse().setLocale(locale);

		// Ignore any call made after the getWriter has been invoked.
		// The default should be used
		if (usingWriter) {
			return;
		}

		if (isCharacterEncodingSet) {
			return;
		}

		String charset = getContext().getCharset(locale);
		if (charset != null) {
			getCoyoteResponse().setCharacterEncoding(charset);
		}
	}

	// --------------------------------------------------- HttpResponse Methods

	@Override
	public String getHeader(String name) {
		return getCoyoteResponse().getMimeHeaders().getHeader(name);
	}

	@Override
	public Collection<String> getHeaderNames() {

		MimeHeaders headers = getCoyoteResponse().getMimeHeaders();
		int n = headers.size();
		List<String> result = new ArrayList<String>(n);
		for (int i = 0; i < n; i++) {
			result.add(headers.getName(i).toString());
		}
		return result;

	}

	@Override
	public Collection<String> getHeaders(String name) {

		Enumeration<String> enumeration = getCoyoteResponse().getMimeHeaders().values(name);
		Vector<String> result = new Vector<String>();
		while (enumeration.hasMoreElements()) {
			result.addElement(enumeration.nextElement());
		}
		return result;
	}

	/**
	 * Return the error message that was set with <code>sendError()</code> for
	 * this Response.
	 */
	public String getMessage() {
		return getCoyoteResponse().getMessage();
	}

	@Override
	public int getStatus() {
		return getCoyoteResponse().getStatus();
	}

	/**
	 * Reset this response, and specify the values for the HTTP status code and
	 * corresponding message.
	 *
	 * @exception IllegalStateException
	 *                if this response has already been committed
	 */
	@Deprecated
	public void reset(int status, String message) {
		reset();
		setStatus(status, message);
	}

	// -------------------------------------------- HttpServletResponse Methods

	/**
	 * Add the specified Cookie to those that will be included with this
	 * Response.
	 *
	 * @param cookie
	 *            Cookie to be added
	 */
	@Override
	public void addCookie(final Cookie cookie) {

		// Ignore any call from an included servlet
		if (included || isCommitted()) {
			return;
		}

		final StringBuffer sb = generateCookieString(cookie);
		// if we reached here, no exception, cookie is valid
		// the header name is Set-Cookie for both "old" and v.1 ( RFC2109 )
		// RFC2965 is not supported by browsers and the Servlet spec
		// asks for 2109.
		addHeader("Set-Cookie", sb.toString());
	}

	/**
	 * Special method for adding a session cookie as we should be overriding any
	 * previous
	 * 
	 * @param cookie
	 */
	public void addSessionCookieInternal(final Cookie cookie) {
		if (isCommitted()) {
			return;
		}

		String name = cookie.getName();
		final String headername = "Set-Cookie";
		final String startsWith = name + "=";
		final StringBuffer sb = generateCookieString(cookie);
		boolean set = false;
		MimeHeaders headers = getCoyoteResponse().getMimeHeaders();
		int n = headers.size();
		for (int i = 0; i < n; i++) {
			if (headers.getName(i).toString().equals(headername)) {
				if (headers.getValue(i).toString().startsWith(startsWith)) {
					headers.getValue(i).setString(sb.toString());
					set = true;
				}
			}
		}
		if (!set) {
			addHeader(headername, sb.toString());
		}

	}

	public StringBuffer generateCookieString(final Cookie cookie) {
		final StringBuffer sb = new StringBuffer();
		// web application code can receive a IllegalArgumentException
		// from the appendCookieValue invocation
		if (SecurityUtil.isPackageProtectionEnabled()) {
			AccessController.doPrivileged(new PrivilegedAction<Void>() {
				@Override
				public Void run() {
					ServerCookie.appendCookieValue(sb, cookie.getVersion(), cookie.getName(), cookie.getValue(),
							cookie.getPath(), cookie.getDomain(), cookie.getComment(), cookie.getMaxAge(),
							cookie.getSecure(), cookie.isHttpOnly());
					return null;
				}
			});
		} else {
			ServerCookie.appendCookieValue(sb, cookie.getVersion(), cookie.getName(), cookie.getValue(),
					cookie.getPath(), cookie.getDomain(), cookie.getComment(), cookie.getMaxAge(), cookie.getSecure(),
					cookie.isHttpOnly());
		}
		return sb;
	}

	/**
	 * Add the specified date header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Date value to be set
	 */
	@Override
	public void addDateHeader(String name, long value) {

		if (name == null || name.length() == 0) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		if (format == null) {
			format = new SimpleDateFormat(DateTool.HTTP_RESPONSE_DATE_HEADER, Locale.US);
			format.setTimeZone(TimeZone.getTimeZone("GMT"));
		}

		addHeader(name, FastHttpDateFormat.formatDate(value, format));

	}

	/**
	 * Add the specified header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Value to be set
	 */
	@Override
	public void addHeader(String name, String value) {

		if (name == null || name.length() == 0 || value == null) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		char cc = name.charAt(0);
		if (cc == 'C' || cc == 'c') {
			if (checkSpecialHeader(name, value))
				return;
		}

		getCoyoteResponse().addHeader(name, value);
	}

	/**
	 * An extended version of this exists in {@link org.apache.coyote.Response}.
	 * This check is required here to ensure that the usingWriter checks in
	 * {@link #setContentType(String)} are applied since usingWriter is not
	 * visible to {@link org.apache.coyote.Response}
	 *
	 * Called from set/addHeader. Return true if the header is special, no need
	 * to set the header.
	 */
	private boolean checkSpecialHeader(String name, String value) {
		if (name.equalsIgnoreCase("Content-Type")) {
			setContentType(value);
			return true;
		}
		return false;
	}

	/**
	 * Add the specified integer header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Integer value to be set
	 */
	@Override
	public void addIntHeader(String name, int value) {

		if (name == null || name.length() == 0) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		addHeader(name, "" + value);

	}

	/**
	 * Has the specified header been set already in this response?
	 *
	 * @param name
	 *            Name of the header to check
	 */
	@Override
	public boolean containsHeader(String name) {
		// Need special handling for Content-Type and Content-Length due to
		// special handling of these in coyoteResponse
		char cc = name.charAt(0);
		if (cc == 'C' || cc == 'c') {
			if (name.equalsIgnoreCase("Content-Type")) {
				// Will return null if this has not been set
				return (getCoyoteResponse().getContentType() != null);
			}
			if (name.equalsIgnoreCase("Content-Length")) {
				// -1 means not known and is not sent to client
				return (getCoyoteResponse().getContentLengthLong() != -1);
			}
		}

		return getCoyoteResponse().containsHeader(name);
	}

	/**
	 * Encode the session identifier associated with this response into the
	 * specified redirect URL, if necessary.
	 *
	 * @param url
	 *            URL to be encoded
	 */
	@Override
	public String encodeRedirectURL(String url) {

		if (isEncodeable(toAbsolute(url))) {
			return (toEncoded(url, request.getSessionInternal().getIdInternal()));
		} else {
			return (url);
		}

	}

	/**
	 * Encode the session identifier associated with this response into the
	 * specified redirect URL, if necessary.
	 *
	 * @param url
	 *            URL to be encoded
	 *
	 * @deprecated As of Version 2.1 of the Java Servlet API, use
	 *             <code>encodeRedirectURL()</code> instead.
	 */
	@Override
	@Deprecated
	public String encodeRedirectUrl(String url) {
		return (encodeRedirectURL(url));
	}

	/**
	 * Encode the session identifier associated with this response into the
	 * specified URL, if necessary.
	 *
	 * @param url
	 *            URL to be encoded
	 */
	@Override
	public String encodeURL(String url) {

		String absolute;
		try {
			absolute = toAbsolute(url);
		} catch (IllegalArgumentException iae) {
			// Relative URL
			return url;
		}

		if (isEncodeable(absolute)) {
			// W3c spec clearly said
			if (url.equalsIgnoreCase("")) {
				url = absolute;
			} else if (url.equals(absolute) && !hasPath(url)) {
				url += '/';
			}
			return (toEncoded(url, request.getSessionInternal().getIdInternal()));
		} else {
			return (url);
		}

	}

	/**
	 * Encode the session identifier associated with this response into the
	 * specified URL, if necessary.
	 *
	 * @param url
	 *            URL to be encoded
	 *
	 * @deprecated As of Version 2.1 of the Java Servlet API, use
	 *             <code>encodeURL()</code> instead.
	 */
	@Override
	@Deprecated
	public String encodeUrl(String url) {
		return (encodeURL(url));
	}

	/**
	 * Send an acknowledgment of a request.
	 *
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	public void sendAcknowledgement() throws IOException {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		getCoyoteResponse().acknowledge();
	}

	/**
	 * Send an error response with the specified status and a default message.
	 *
	 * @param status
	 *            HTTP status code to send
	 *
	 * @exception IllegalStateException
	 *                if this response has already been committed
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public void sendError(int status) throws IOException {
		sendError(status, null);
	}

	/**
	 * Send an error response with the specified status and message.
	 *
	 * @param status
	 *            HTTP status code to send
	 * @param message
	 *            Corresponding message to send
	 *
	 * @exception IllegalStateException
	 *                if this response has already been committed
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public void sendError(int status, String message) throws IOException {

		if (isCommitted()) {
			throw new IllegalStateException(sm.getString("coyoteResponse.sendError.ise"));
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		setError();

		getCoyoteResponse().setStatus(status);
		getCoyoteResponse().setMessage(message);

		// Clear any data content that has been buffered
		resetBuffer();

		// Cause the response to be finished (from the application perspective)
		setSuspended(true);
	}

	/**
	 * Send a temporary redirect to the specified redirect location URL.
	 *
	 * @param location
	 *            Location URL to redirect to
	 *
	 * @exception IllegalStateException
	 *                if this response has already been committed
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	@Override
	public void sendRedirect(String location) throws IOException {
		sendRedirect(location, SC_FOUND);
	}

	/**
	 * Internal method that allows a redirect to be sent with a status other
	 * than {@link HttpServletResponse#SC_FOUND} (302). No attempt is made to
	 * validate the status code.
	 */
	public void sendRedirect(String location, int status) throws IOException {
		if (isCommitted()) {
			throw new IllegalStateException(sm.getString("coyoteResponse.sendRedirect.ise"));
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		// Clear any data content that has been buffered
		resetBuffer(true);

		// Generate a temporary redirect to the specified location
		try {
			String locationUri;
			// Relative redirects require HTTP/1.1
			if (getRequest().getCoyoteRequest().getSupportsRelativeRedirects()
					&& getContext().getUseRelativeRedirects()) {
				locationUri = location;
			} else {
				locationUri = toAbsolute(location);
			}
			setStatus(status);
			setHeader("Location", locationUri);
			if (getContext().getSendRedirectBody()) {
				PrintWriter writer = getWriter();
				writer.print(sm.getString("coyoteResponse.sendRedirect.note", RequestUtil.filter(locationUri)));
				flushBuffer();
			}
		} catch (IllegalArgumentException e) {
			log.warn(sm.getString("response.sendRedirectFail", location), e);
			setStatus(SC_NOT_FOUND);
		}

		// Cause the response to be finished (from the application perspective)
		setSuspended(true);
	}

	/**
	 * Set the specified date header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Date value to be set
	 */
	@Override
	public void setDateHeader(String name, long value) {

		if (name == null || name.length() == 0) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		if (format == null) {
			format = new SimpleDateFormat(DateTool.HTTP_RESPONSE_DATE_HEADER, Locale.US);
			format.setTimeZone(TimeZone.getTimeZone("GMT"));
		}

		setHeader(name, FastHttpDateFormat.formatDate(value, format));
	}

	/**
	 * Set the specified header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Value to be set
	 */
	@Override
	public void setHeader(String name, String value) {

		if (name == null || name.length() == 0 || value == null) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		char cc = name.charAt(0);
		if (cc == 'C' || cc == 'c') {
			if (checkSpecialHeader(name, value))
				return;
		}

		getCoyoteResponse().setHeader(name, value);
	}

	/**
	 * Set the specified integer header to the specified value.
	 *
	 * @param name
	 *            Name of the header to set
	 * @param value
	 *            Integer value to be set
	 */
	@Override
	public void setIntHeader(String name, int value) {

		if (name == null || name.length() == 0) {
			return;
		}

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		setHeader(name, "" + value);

	}

	/**
	 * Set the HTTP status to be returned with this response.
	 *
	 * @param status
	 *            The new HTTP status
	 */
	@Override
	public void setStatus(int status) {
		setStatus(status, null);
	}

	/**
	 * Set the HTTP status and message to be returned with this response.
	 *
	 * @param status
	 *            The new HTTP status
	 * @param message
	 *            The associated text message
	 *
	 * @deprecated As of Version 2.1 of the Java Servlet API, this method has
	 *             been deprecated due to the ambiguous meaning of the message
	 *             parameter.
	 */
	@Override
	@Deprecated
	public void setStatus(int status, String message) {

		if (isCommitted()) {
			return;
		}

		// Ignore any call from an included servlet
		if (included) {
			return;
		}

		getCoyoteResponse().setStatus(status);
		getCoyoteResponse().setMessage(message);

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Return <code>true</code> if the specified URL should be encoded with a
	 * session identifier. This will be true if all of the following conditions
	 * are met:
	 * <ul>
	 * <li>The request we are responding to asked for a valid session
	 * <li>The requested session ID was not received via a cookie
	 * <li>The specified URL points back to somewhere within the web application
	 * that is responding to this request
	 * </ul>
	 *
	 * @param location
	 *            Absolute URL to be validated
	 */
	protected boolean isEncodeable(final String location) {

		if (location == null) {
			return (false);
		}

		// Is this an intra-document reference?
		if (location.startsWith("#")) {
			return (false);
		}

		// Are we in a valid session that is not using cookies?
		final Request hreq = request;
		final Session session = hreq.getSessionInternal(false);
		if (session == null) {
			return (false);
		}
		if (hreq.isRequestedSessionIdFromCookie()) {
			return (false);
		}

		// Is URL encoding permitted
		if (!hreq.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
			return false;
		}

		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (AccessController.doPrivileged(new PrivilegedAction<Boolean>() {

				@Override
				public Boolean run() {
					return Boolean.valueOf(doIsEncodeable(hreq, session, location));
				}
			})).booleanValue();
		} else {
			return doIsEncodeable(hreq, session, location);
		}
	}

	private boolean doIsEncodeable(Request hreq, Session session, String location) {
		// Is this a valid absolute URL?
		URL url = null;
		try {
			url = new URL(location);
		} catch (MalformedURLException e) {
			return (false);
		}

		// Does this URL match down to (and including) the context path?
		if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) {
			return (false);
		}
		if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) {
			return (false);
		}
		int serverPort = hreq.getServerPort();
		if (serverPort == -1) {
			if ("https".equals(hreq.getScheme())) {
				serverPort = 443;
			} else {
				serverPort = 80;
			}
		}
		int urlPort = url.getPort();
		if (urlPort == -1) {
			if ("https".equals(url.getProtocol())) {
				urlPort = 443;
			} else {
				urlPort = 80;
			}
		}
		if (serverPort != urlPort) {
			return (false);
		}

		String contextPath = getContext().getPath();
		if (contextPath != null) {
			String file = url.getFile();
			if (!file.startsWith(contextPath)) {
				return (false);
			}
			String tok = ";" + SessionConfig.getSessionUriParamName(request.getContext()) + "="
					+ session.getIdInternal();
			if (file.indexOf(tok, contextPath.length()) >= 0) {
				return (false);
			}
		}

		// This URL belongs to our web application, so it is encodeable
		return (true);

	}

	/**
	 * Convert (if necessary) and return the absolute URL that represents the
	 * resource referenced by this possibly relative URL. If this URL is already
	 * absolute, return it unchanged.
	 *
	 * @param location
	 *            URL to be (possibly) converted and then returned
	 *
	 * @exception IllegalArgumentException
	 *                if a MalformedURLException is thrown when converting the
	 *                relative URL to an absolute one
	 */
	protected String toAbsolute(String location) {

		if (location == null) {
			return (location);
		}

		boolean leadingSlash = location.startsWith("/");

		if (location.startsWith("//")) {
			// Scheme relative
			redirectURLCC.recycle();
			// Add the scheme
			String scheme = request.getScheme();
			try {
				redirectURLCC.append(scheme, 0, scheme.length());
				redirectURLCC.append(':');
				redirectURLCC.append(location, 0, location.length());
				return redirectURLCC.toString();
			} catch (IOException e) {
				IllegalArgumentException iae = new IllegalArgumentException(location);
				iae.initCause(e);
				throw iae;
			}

		} else if (leadingSlash || !hasScheme(location)) {

			redirectURLCC.recycle();

			String scheme = request.getScheme();
			String name = request.getServerName();
			int port = request.getServerPort();

			try {
				redirectURLCC.append(scheme, 0, scheme.length());
				redirectURLCC.append("://", 0, 3);
				redirectURLCC.append(name, 0, name.length());
				if ((scheme.equals("http") && port != 80) || (scheme.equals("https") && port != 443)) {
					redirectURLCC.append(':');
					String portS = port + "";
					redirectURLCC.append(portS, 0, portS.length());
				}
				if (!leadingSlash) {
					String relativePath = request.getDecodedRequestURI();
					int pos = relativePath.lastIndexOf('/');
					CharChunk encodedURI = null;
					final String frelativePath = relativePath;
					final int fend = pos;
					if (SecurityUtil.isPackageProtectionEnabled()) {
						try {
							encodedURI = AccessController.doPrivileged(new PrivilegedExceptionAction<CharChunk>() {
								@Override
								public CharChunk run() throws IOException {
									return urlEncoder.encodeURL(frelativePath, 0, fend);
								}
							});
						} catch (PrivilegedActionException pae) {
							IllegalArgumentException iae = new IllegalArgumentException(location);
							iae.initCause(pae.getException());
							throw iae;
						}
					} else {
						encodedURI = urlEncoder.encodeURL(relativePath, 0, pos);
					}
					redirectURLCC.append(encodedURI);
					encodedURI.recycle();
					redirectURLCC.append('/');
				}
				redirectURLCC.append(location, 0, location.length());

				normalize(redirectURLCC);
			} catch (IOException e) {
				IllegalArgumentException iae = new IllegalArgumentException(location);
				iae.initCause(e);
				throw iae;
			}

			return redirectURLCC.toString();

		} else {

			return (location);

		}

	}

	/*
	 * Removes /./ and /../ sequences from absolute URLs. Code borrowed heavily
	 * from CoyoteAdapter.normalize()
	 */
	private void normalize(CharChunk cc) {
		// Strip query string and/or fragment first as doing it this way makes
		// the normalization logic a lot simpler
		int truncate = cc.indexOf('?');
		if (truncate == -1) {
			truncate = cc.indexOf('#');
		}
		char[] truncateCC = null;
		if (truncate > -1) {
			truncateCC = Arrays.copyOfRange(cc.getBuffer(), cc.getStart() + truncate, cc.getEnd());
			cc.setEnd(cc.getStart() + truncate);
		}

		if (cc.endsWith("/.") || cc.endsWith("/..")) {
			try {
				cc.append('/');
			} catch (IOException e) {
				throw new IllegalArgumentException(cc.toString(), e);
			}
		}

		char[] c = cc.getChars();
		int start = cc.getStart();
		int end = cc.getEnd();
		int index = 0;
		int startIndex = 0;

		// Advance past the first three / characters (should place index just
		// scheme://host[:port]

		for (int i = 0; i < 3; i++) {
			startIndex = cc.indexOf('/', startIndex + 1);
		}

		// Remove /./
		index = startIndex;
		while (true) {
			index = cc.indexOf("/./", 0, 3, index);
			if (index < 0) {
				break;
			}
			copyChars(c, start + index, start + index + 2, end - start - index - 2);
			end = end - 2;
			cc.setEnd(end);
		}

		// Remove /../
		index = startIndex;
		int pos;
		while (true) {
			index = cc.indexOf("/../", 0, 4, index);
			if (index < 0) {
				break;
			}
			// Can't go above the server root
			if (index == startIndex) {
				throw new IllegalArgumentException();
			}
			int index2 = -1;
			for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos--) {
				if (c[pos] == (byte) '/') {
					index2 = pos;
				}
			}
			copyChars(c, start + index2, start + index + 3, end - start - index - 3);
			end = end + index2 - index - 3;
			cc.setEnd(end);
			index = index2;
		}

		// Add the query string and/or fragment (if present) back in
		if (truncateCC != null) {
			try {
				cc.append(truncateCC, 0, truncateCC.length);
			} catch (IOException ioe) {
				throw new IllegalArgumentException(ioe);
			}
		}
	}

	private void copyChars(char[] c, int dest, int src, int len) {
		for (int pos = 0; pos < len; pos++) {
			c[pos + dest] = c[pos + src];
		}
	}

	/**
	 * Determine if an absolute URL has a path component
	 */
	private boolean hasPath(String uri) {
		int pos = uri.indexOf("://");
		if (pos < 0) {
			return false;
		}
		pos = uri.indexOf('/', pos + 3);
		if (pos < 0) {
			return false;
		}
		return true;
	}

	/**
	 * Determine if a URI string has a <code>scheme</code> component.
	 */
	private boolean hasScheme(String uri) {
		int len = uri.length();
		for (int i = 0; i < len; i++) {
			char c = uri.charAt(i);
			if (c == ':') {
				return i > 0;
			} else if (!URL.isSchemeChar(c)) {
				return false;
			}
		}
		return false;
	}

	/**
	 * Return the specified URL with the specified session identifier suitably
	 * encoded.
	 *
	 * @param url
	 *            URL to be encoded with the session id
	 * @param sessionId
	 *            Session id to be included in the encoded URL
	 */
	protected String toEncoded(String url, String sessionId) {

		if ((url == null) || (sessionId == null)) {
			return (url);
		}

		String path = url;
		String query = "";
		String anchor = "";
		int question = url.indexOf('?');
		if (question >= 0) {
			path = url.substring(0, question);
			query = url.substring(question);
		}
		int pound = path.indexOf('#');
		if (pound >= 0) {
			anchor = path.substring(pound);
			path = path.substring(0, pound);
		}
		StringBuilder sb = new StringBuilder(path);
		if (sb.length() > 0) { // jsessionid can't be first.
			sb.append(";");
			sb.append(SessionConfig.getSessionUriParamName(request.getContext()));
			sb.append("=");
			sb.append(sessionId);
		}
		sb.append(anchor);
		sb.append(query);
		return (sb.toString());

	}
}