blob: 12020c016e9e71bf523eea41447408ad3cb165a6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 1999, 2009 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.http.servlet;
import java.io.*;
import java.util.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.equinox.http.*;
import org.eclipse.equinox.socket.SocketInterface;
/**
* The implementation of the HttpServletResponse interface.
*
* Per Servlet 2.2 Section 3.3.3.3, this object need not be thread-safe.
**/
public class HttpServletResponseImpl implements HttpServletResponse {
private Http http;
protected HttpServletRequestImpl request;
private SocketInterface socket;
protected ServletOutputStreamImpl servletOutputStream;
protected String charset = null;
protected static final String defaultCharset = "ISO-8859-1"; //$NON-NLS-1$
protected int contentLength = 0;
protected String contentType = null;
protected Vector cookies = null;
protected Hashtable headers = null;
protected int status = SC_OK;
protected String statusString = "OK"; //$NON-NLS-1$
private boolean gotOutputStream = false;
protected PrintWriter writer = null;
private boolean keepAlive = false;
public HttpServletResponseImpl(SocketInterface socket, Http http) throws IOException {
this.http = http;
this.socket = socket;
servletOutputStream = new ServletOutputStreamImpl(socket.getOutputStream(), this);
}
/**
* Called by the request in its constructor.
*/
protected void setRequest(HttpServletRequestImpl request) {
this.request = request;
}
/**
* Adds the specified cookie to the response. It can be called
* multiple times to set more than one cookie.
*
* @param cookie the Cookie to return to the client
*/
public void addCookie(Cookie cookie) {
if (cookies == null) {
synchronized (this) {
if (cookies == null) {
cookies = new Vector(20);
}
}
}
synchronized (cookies) {
if (!cookies.contains(cookie)) {
//System.out.println("addCookie: "+cookie.getName()+"="+cookie.getValue());
cookies.addElement(cookie);
}
}
//http.logDebug("ServletResponse::addCookie: added cookie "+cookie.getName());
}
/**
* Checks whether the response message header has a field with
* the specified name.
*
* @param name the header field name
* @return true if the response message header has a field with
* the specified name; false otherwise
*/
public boolean containsHeader(String name) {
if (headers == null) {
return (false);
}
Object value = headers.get(name);
if (value != null) {
return (true);
}
Enumeration headerEnumeration = headers.keys();
while (headerEnumeration.hasMoreElements()) {
String key = (String) headerEnumeration.nextElement();
if (name.equalsIgnoreCase(key)) {
return (true);
}
}
return (false);
}
/**
* Encodes the specified URL for use in the
* <code>sendRedirect</code> method or, if encoding is not needed,
* returns the URL unchanged. The implementation of this method
* should include the logic to determine whether the session ID
* needs to be encoded in the URL. Because the rules for making
* this determination differ from those used to decide whether to
* encode a normal link, this method is seperate from the
* <code>encodeUrl</code> method.
*
* <p>All URLs sent to the HttpServletResponse.sendRedirect
* method should be run through this method. Otherwise, URL
* rewriting cannot be used with browsers which do not support
* cookies.
*
* @param url the url to be encoded.
* @return the encoded URL if encoding is needed; the unchanged URL
* otherwise.
* deprecated
*
* @see #sendRedirect
* @see #encodeUrl
*/
public String encodeRedirectUrl(String url) {
return (encodeRedirectURL(url));
}
/**
* Encodes the specified URL for use in the
* <code>sendRedirect</code> method or, if encoding is not needed,
* returns the URL unchanged. The implementation of this method
* should include the logic to determine whether the session ID
* needs to be encoded in the URL. Because the rules for making
* this determination differ from those used to decide whether to
* encode a normal link, this method is seperate from the
* <code>encodeUrl</code> method.
*
* <p>All URLs sent to the HttpServletResponse.sendRedirect
* method should be run through this method. Otherwise, URL
* rewriting canont be used with browsers which do not support
* cookies.
*
* <p>After this method is called, the response should be considered
* to be committed and should not be written to.
*
* @param url the url to be encoded.
* @return the encoded URL if encoding is needed; the unchanged URL
* otherwise.
*
* @see #sendRedirect
* @see #encodeUrl
*/
public String encodeRedirectURL(String url) {
return (url);
}
/**
* Encodes the specified URL by including the session ID in it,
* or, if encoding is not needed, returns the URL unchanged.
* The implementation of this method should include the logic to
* determine whether the session ID needs to be encoded in the URL.
* For example, if the browser supports cookies, or session
* tracking is turned off, URL encoding is unnecessary.
*
* <p>All URLs emitted by a Servlet should be run through this
* method. Otherwise, URL rewriting cannot be used with browsers
* which do not support cookies.
*
* @param url the url to be encoded.
* @return the encoded URL if encoding is needed; the unchanged URL
* otherwise.
* deprecated
*/
public String encodeUrl(String url) {
return (encodeURL(url));
}
/**
* Encodes the specified URL by including the session ID in it,
* or, if encoding is not needed, returns the URL unchanged.
* The implementation of this method should include the logic to
* determine whether the session ID needs to be encoded in the URL.
* For example, if the browser supports cookies, or session
* tracking is turned off, URL encoding is unnecessary.
*
* <p>All URLs emitted by a Servlet should be run through this
* method. Otherwise, URL rewriting cannot be used with browsers
* which do not support cookies.
*
* @param url the url to be encoded.
* @return the encoded URL if encoding is needed; the unchanged URL
* otherwise.
*/
public String encodeURL(String url) {
return (url);
}
/**
* Close the response output.
*
*/
public void close() {
if (writer != null) {
writer.close();
} else {
try {
servletOutputStream.close();
} catch (IOException e) {
// TODO: consider logging
}
}
if (!keepAlive) /* if the no Keep-Alive, then close socket */
{
try {
socket.close();
} catch (IOException e) {
// TODO: consider logging
}
}
}
/**
* Returns the character set encoding used for this MIME body.
* The character encoding is either the one specified in the
* assigned content type, or one which the client understands
* as specified in the Accept-Charset header of the request.
* If no charset can be determined, it defaults to the
* value of the System property 'file.encoding' if that
* is a supported character set. If it is not supported, then
* we use <tt>ISO-8859-1</tt> as the character set.
*/
public String getCharacterEncoding() {
if (charset == null) {
OutputStream dummy = new ByteArrayOutputStream();
// BUGBUG Per the spec, the default charset is ISO8859_1
// Servlet 2.3 Section 5.4
/* Get the default file encoding charset */
String fileEncoding = (new SecureAction()).getProperty("file.encoding", defaultCharset); //$NON-NLS-1$
if ((fileEncoding == null) || (fileEncoding.length() < 1)) {
fileEncoding = defaultCharset;
}
String fileEncodingAlias = null;
/* We need to look at Accept-Charset from the request to
* select the encoding charset.
*/
String accept = request.getHeaderUpper("ACCEPT-CHARSET"); //$NON-NLS-1$
if (accept != null) {
/*
* We prefer the file.encoding charset if specified
* in the Accept-Charset and supported by the JRE. Otherwise,
* we simply select the first value supported by the
* JRE. We do not support the q value in Accept-Charset.
*/
Tokenizer tokenizer = new Tokenizer(accept);
parseloop: while (true) {
String acceptCharset = tokenizer.getToken(",;"); //$NON-NLS-1$
if (!acceptCharset.equals("*")) //$NON-NLS-1$
{
try /* is charset valid? */
{
String encodingAlias = new OutputStreamWriter(dummy, acceptCharset).getEncoding();
if (charset == null) /* charset has not been set */
{
charset = acceptCharset;
} else {
if (fileEncodingAlias == null) {
try {
fileEncodingAlias = new OutputStreamWriter(dummy, fileEncoding).getEncoding();
} catch (UnsupportedEncodingException ee) {
// TODO: consider logging
}
}
if (encodingAlias.equals(fileEncodingAlias)) {
charset = acceptCharset; /* prefer the file.encoding charset */
break parseloop;
}
}
} catch (UnsupportedEncodingException e) {
/* charset is not supported */
}
}
char c = tokenizer.getChar();
if (c == ';') {
tokenizer.getToken(","); /* ignore q value *///$NON-NLS-1$
c = tokenizer.getChar();
}
if (c == '\0') {
break parseloop;
}
}
}
if (charset == null) /* if Accept-Charset did not produce a valid charset */
{
charset = fileEncoding;
try /* is charset valid? */
{
new OutputStreamWriter(dummy, charset);
} catch (UnsupportedEncodingException e) {
/* charset is not supported */
charset = defaultCharset;
}
}
}
return (charset);
}
/**
* Returns an output stream for writing binary response data.
*
* @see #getWriter()
* @exception IllegalStateException if getWriter has been
* called on this same request.
*/
public ServletOutputStream getOutputStream() {
if (!gotOutputStream) {
synchronized (this) {
if (writer != null) {
throw new IllegalStateException();
}
gotOutputStream = true;
}
}
return (servletOutputStream);
}
/**
* Returns a print writer for writing formatted text responses. The
* MIME type of the response will be modified, if necessary, to reflect
* the character encoding used, through the <em>charset=...</em>
* property. This means that the content type must be set before
* calling this method.
*
* @see #getOutputStream()
* @see #setContentType(String)
*
* @exception UnsupportedEncodingException if no such charset can
* be provided
* @exception IllegalStateException if getOutputStream has been
* called on this same request.
* @exception IOException on other errors.
*/
public PrintWriter getWriter() throws IOException {
if (writer == null) {
synchronized (this) {
if (writer == null) {
if (gotOutputStream) {
throw new IllegalStateException();
}
String encoding = getCharacterEncoding();
writer = new ServletPrintWriter(servletOutputStream, encoding);
if (contentType == null) {
// BUGBUG Must not set a default content type.
// Servlet 2.3 Section 5.3
contentType = "text/plain; charset=" + encoding; //$NON-NLS-1$
setHeader("Content-Type", contentType); //$NON-NLS-1$
} else {
if (contentType.toLowerCase().indexOf("charset=") == -1) // 99372 //$NON-NLS-1$
{
contentType = contentType + "; charset=" + encoding; //$NON-NLS-1$
setHeader("Content-Type", contentType); //$NON-NLS-1$
}
}
}
}
}
return (writer);
}
boolean gotStreamOrWriter() {
return (writer != null || gotOutputStream);
}
/**
* Sends an error response to the client using the specified
* status code and a default message.
* @param statusCode the status code
* @exception IOException If an I/O error has occurred.
*/
public void sendError(int statusCode) throws IOException {
// BUGBUG Must clear buffer if response has not yet been committed
// or throw an IllegalStateException.
sendError(statusCode, http.getStatusPhrase(statusCode));
}
/**
* Sends an error response to the client using the specified status
* code and descriptive message. If setStatus has previously been
* called, it is reset to the error status code. The message is
* sent as the body of an HTML page, which is returned to the user
* to describe the problem. The page is sent with a default HTML
* header; the message is enclosed in simple body tags
* (&lt;body&gt;&lt;/body&gt;).
*
* @param sc the status code
* @param msg the detail message
* @exception IOException If an I/O error has occurred.
*/
public void sendError(int sc, String msg) throws IOException {
// BUGBUG Must clear buffer if response has not yet been committed
// or throw an IllegalStateException.
status = sc;
statusString = msg;
if (Http.DEBUG) {
http.logDebug("Error: " + sc + " - " + msg); //$NON-NLS-1$ //$NON-NLS-2$
}
servletOutputStream.print("<html><body><h3>"); //$NON-NLS-1$
servletOutputStream.print(sc);
servletOutputStream.print(" - "); //$NON-NLS-1$
servletOutputStream.print(msg);
servletOutputStream.print("</h3></body></html>"); //$NON-NLS-1$
servletOutputStream.close(); /* commit and close the response */
}
/**
* Sends a temporary redirect response to the client using the
* specified redirect location URL. The URL must be absolute (for
* example, <code><em>https://hostname/path/file.html</em></code>).
* Relative URLs are not permitted here.
*
* @param location the redirect location URL
* @exception IOException If an I/O error has occurred.
*/
public void sendRedirect(String location) throws IOException {
// BUGBUG The input URL may be relative. We must translate to
// a fully qualified URL (it should already have been encoded).
// If this cannot be done, throw an IllegalArgument exception.
// BUGBUG Must clear buffer if response has not yet been committed
// or throw an IllegalStateException.
setStatus(SC_MOVED_TEMPORARILY);
setHeader("Location", location); //$NON-NLS-1$
servletOutputStream.close(); /* commit and close the response */
}
/**
* Sets the content length for this response.
*
* @param len the content length
*/
public void setContentLength(int len) {
// BUGBUG response should be considered committed and closed
// when content length has been written.
// Not sure if content length is bytes or chars?
contentLength = len;
setIntHeader("Content-Length", len); //$NON-NLS-1$
}
/**
* Sets the content type for this response. This type may later
* be implicitly modified by addition of properties such as the MIME
* <em>charset=&lt;value&gt;</em> if the service finds it necessary,
* and the appropriate media type property has not been set.
*
* <p>This response property may only be assigned one time. If a
* writer is to be used to write a text response, this method must
* be called before the method <code>getWriter</code>. If an
* output stream will be used to write a response, this method must
* be called before the output stream is used to write response
* data.
*
* @param type the content's MIME type
* @see #getOutputStream()
* @see #getWriter()
*/
public void setContentType(String type) {
if (contentType == null) {
synchronized (this) {
if (contentType == null) {
contentType = type;
setHeader("Content-Type", type); //$NON-NLS-1$
int index = type.indexOf(';', 0);
if (index >= 0) {
Tokenizer tokenizer = new Tokenizer(type);
// TODO: verify next statement. It was String mimetype = tokenizer.getToken(";");
tokenizer.getToken(";"); //$NON-NLS-1$
tokenizer.getChar(); /* eat semicolon */
parseloop: while (true) {
String attribute = tokenizer.getToken("="); //$NON-NLS-1$
char c = tokenizer.getChar();
if (c != '=') {
break parseloop; /* invalid content type */
}
String value = tokenizer.getString(";"); //$NON-NLS-1$
c = tokenizer.getChar();
if ("charset".equalsIgnoreCase(attribute)) //$NON-NLS-1$
{
charset = value;
}
if (c == '\0') {
break parseloop;
}
}
}
}
}
}
}
/**
* This method was created in VisualAge.
*/
protected void setCookies() {
if (cookies == null) {
return;
}
int numCookies = cookies.size();
if (numCookies > 0) {
StringBuffer value = new StringBuffer(256);
for (int i = 0; i < numCookies; i++) {
Cookie cookie = (Cookie) cookies.elementAt(i);
if (i > 0) {
value.append(',');
}
value.append(cookie.getName());
value.append("=\""); //$NON-NLS-1$
value.append(cookie.getValue());
value.append('\"');
if ((cookie.getVersion()) != 0) {
value.append(";Version="); //$NON-NLS-1$
value.append(cookie.getVersion());
}
if ((cookie.getComment()) != null) {
value.append(";Comment=\""); //$NON-NLS-1$
value.append(cookie.getComment());
value.append('\"');
}
if ((cookie.getDomain()) != null) {
value.append(";Domain=\""); //$NON-NLS-1$
value.append(cookie.getDomain());
value.append('\"');
}
if ((cookie.getMaxAge()) != -1) {
value.append(";Max-Age=\""); //$NON-NLS-1$
value.append(cookie.getMaxAge());
value.append('\"');
}
if ((cookie.getPath()) != null) {
value.append(";Path=\""); //$NON-NLS-1$
value.append(cookie.getPath());
value.append('\"');
}
if (cookie.getSecure()) {
value.append(";Secure"); //$NON-NLS-1$
}
}
setHeader("Set-Cookie", value.toString()); //$NON-NLS-1$
//System.out.println("Set-Cookie: "+value.toString());
}
}
/**
*
* Adds a field to the response header with the given name and
* date-valued field. The date is specified in terms of
* milliseconds since the epoch. If the date field had already
* been set, the new value overwrites the previous one. The
* <code>containsHeader</code> method can be used to test for the
* presence of a header before setting its value.
*
* @param name the name of the header field
* @param date the header field's date value
*
* @see #containsHeader
*/
public void setDateHeader(String name, long date) {
HttpDate d = new HttpDate(date);
setHeader(name, d.toString());
}
/**
*
* Adds a field to the response header with the given name and value.
* If the field had already been set, the new value overwrites the
* previous one. The <code>containsHeader</code> method can be
* used to test for the presence of a header before setting its
* value.
*
* @param name the name of the header field
* @param value the header field's value
*
* @see #containsHeader
*/
public void setHeader(String name, String value) {
// BUGBUG Headers set after the response is committed must be ignored
// Servlet 2.2 Section 6.2
if (headers == null) {
synchronized (this) {
if (headers == null) {
headers = new Hashtable(31);
}
}
}
headers.put(name, value);
}
/**
* Adds a field to the response header with the given name and
* integer value. If the field had already been set, the new value
* overwrites the previous one. The <code>containsHeader</code>
* method can be used to test for the presence of a header before
* setting its value.
*
* @param name the name of the header field
* @param value the header field's integer value
*
* @see #containsHeader
*/
public void setIntHeader(String name, int value) {
setHeader(name, String.valueOf(value));
}
/**
* Sets the status code for this response. This method is used to
* set the return status code when there is no error (for example,
* for the status codes SC_OK or SC_MOVED_TEMPORARILY). If there
* is an error, the <code>sendError</code> method should be used
* instead.
*
* @param statusCode the status code
*
* @see #sendError
*/
public void setStatus(int statusCode) {
status = statusCode;
statusString = http.getStatusPhrase(statusCode);
}
/**
* Sets the status code and message for this response. If the
* field had already been set, the new value overwrites the
* previous one. The message is sent as the body of an HTML
* page, which is returned to the user to describe the problem.
* The page is sent with a default HTML header; the message
* is enclosed in simple body tags (&lt;body&gt;&lt;/body&gt;).
*
* @param si the status code
* @param ss the status message
* deprecated
*/
public void setStatus(int si, String ss) {
status = si;
statusString = ss;
}
/**
* Write the response headers to the ServletOutputStream.
*
* @param length Content length of the of the buffered content
* or -1 if the length is unknown.
*/
void writeHeaders(int length) throws IOException {
setCookies();
if ((length != -1) && !containsHeader("Content-Length")) //$NON-NLS-1$
{
setContentLength(length);
}
if (containsHeader("Content-Length")) //$NON-NLS-1$
{
String requestConnection = request.getHeaderUpper("CONNECTION"); //$NON-NLS-1$
if (requestConnection != null) {
if (requestConnection.toLowerCase().indexOf("keep-alive") >= 0) //$NON-NLS-1$
{
setHeader("Connection", "Keep-Alive"); //$NON-NLS-1$ //$NON-NLS-2$
keepAlive = true;
}
}
}
if (!keepAlive) {
setHeader("Connection", "close"); //$NON-NLS-1$ //$NON-NLS-2$
}
servletOutputStream.print("HTTP/1.0 "); //$NON-NLS-1$
servletOutputStream.print(status);
servletOutputStream.print(" "); //$NON-NLS-1$
servletOutputStream.println(statusString);
// System.out.print("HTTP/1.0 ");
// System.out.print(status);
// System.out.print(" ");
// System.out.println(statusString);
/* Write response headers */
if (headers != null) {
Enumeration headerEnumeration = headers.keys();
while (headerEnumeration.hasMoreElements()) {
String name = (String) headerEnumeration.nextElement();
String value = (String) headers.get(name);
servletOutputStream.print(name);
servletOutputStream.print(": "); //$NON-NLS-1$
servletOutputStream.println(value);
// System.out.print(name);
// System.out.print(": ");
// System.out.println(value);
}
}
servletOutputStream.println(); /* Terminate the headers */
// System.out.println();
}
/**
* @see javax.servlet.http.HttpServletResponse#addDateHeader(String, long)
*/
public void addDateHeader(String name, long date) throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.http.HttpServletResponse#addHeader(String, String)
*/
public void addHeader(String name, String value) throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.http.HttpServletResponse#addIntHeader(String, int)
*/
public void addIntHeader(String name, int value) throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#flushBuffer()
*/
public void flushBuffer() throws IOException, UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#getBufferSize()
*/
public int getBufferSize() throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#getLocale()
*/
public Locale getLocale() throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#isCommitted()
*/
public boolean isCommitted() throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#reset()
*/
public void reset() throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#resetBuffer()
*/
public void resetBuffer() throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#setBufferSize(int)
*/
public void setBufferSize(int size) throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/**
* @see javax.servlet.ServletResponse#setLocale(Locale)
*/
public void setLocale(Locale loc) throws UnsupportedOperationException {
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/* JavaServlet 2.4 API - For compilation only. Will not implement.
*
*/
public String getContentType() {
//return contentType;
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
/* For compilation only. Will not implement.
*
*/
public void setCharacterEncoding(String arg0) {
//this.contentType = contentType;
throw new UnsupportedOperationException(HttpMsg.HTTP_ONLY_SUPPORTS_2_1);
}
}