/* * 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.felix.httplite.servlet; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** * This class represents an HTTP response and handles sending properly * formatted responses to HTTP requests. **/ public class HttpServletResponseImpl implements HttpServletResponse { private static final int COPY_BUFFER_SIZE = 1024 * 4; private final SimpleDateFormat m_dateFormat; private final OutputStream m_out; private int m_bufferSize = COPY_BUFFER_SIZE; private ByteArrayOutputStream m_buffer; private final Map m_headers = new HashMap(); private String m_characterEncoding = "UTF-8"; //TODO: Make locale static and perhaps global to the service. private Locale m_locale = new Locale(System.getProperty("user.language"), System.getProperty( "user.country" )); private boolean m_getOutputStreamCalled = false; private boolean m_getWriterCalled = false; private ServletOutputStreamImpl m_servletOutputStream; private PrintWriter m_printWriter; private List m_cookies = null; private int m_statusCode = HttpURLConnection.HTTP_OK; private String m_customStatusMessage = null; private boolean m_headersWritten = false; /** * Constructs an HTTP response for the specified server and request. * @param outputStream The output stream for the client. **/ public HttpServletResponseImpl(OutputStream outputStream) { m_out = outputStream; m_dateFormat = new SimpleDateFormat(HttpConstants.HTTP_DATE_FORMAT); m_dateFormat.setTimeZone(TimeZone.getTimeZone(HttpConstants.HTTP_TIMEZONE)); } /** * Write HTTP headers to output stream. * * @param close if true, should save state, only allow headers to be written once. Does not close the stream. * @throws IOException on I/O error */ void writeHeaders(boolean close) throws IOException { if (m_headersWritten) { throw new IllegalStateException("Headers have already been written."); } if (!m_headers.containsKey(HttpConstants.HEADER_CONTENT_LENGTH) && m_buffer != null) { setContentLength(m_buffer.size()); } m_out.write(buildResponse(m_statusCode, m_headers, m_customStatusMessage, null)); if (m_cookies != null) { m_out.write( "Set-Cookie: ".getBytes() ); for (Iterator i = m_cookies.iterator(); i.hasNext();) { Cookie cookie = (Cookie) i.next(); m_out.write( cookieToHeader(cookie) ); if (i.hasNext()) { m_out.write( ';' ); } } } m_out.write(HttpConstants.HEADER_DELEMITER.getBytes()); m_out.flush(); if (close) { m_headersWritten = true; } else { m_headers.clear(); } } /** * Copy the contents of the input to the output stream, then close the input stream. * @param inputStream input stream * @param close if connection should be closed * @throws IOException on I/O error */ public void writeToOutputStream(final InputStream inputStream, final boolean close) throws IOException { InputStream bufferedInput = new BufferedInputStream(inputStream); if (!m_headers.containsKey(HttpConstants.HEADER_CONTENT_LENGTH)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); copy(bufferedInput, baos); byte[] outputBuffer = baos.toByteArray(); setContentLength(outputBuffer.length); bufferedInput = new ByteArrayInputStream(outputBuffer); } if (!m_headersWritten) { writeHeaders(close); } try { copy(bufferedInput, m_out); m_out.flush(); } finally { if (bufferedInput != null) { bufferedInput.close(); } } } /** * Copy an input stream to an output stream. * * @param input InputStream * @param output OutputStream * @throws IOException on I/O error. */ public static void copy(final InputStream input, final OutputStream output) throws IOException { byte[] buf = new byte[COPY_BUFFER_SIZE]; for (int len = input.read(buf); len >= 0; len = input.read(buf)) { output.write(buf, 0, len); } } /** * Static utility method to send a continue response. * @throws java.io.IOException If any I/O error occurs. **/ public void sendContinueResponse() throws IOException { m_out.write(buildResponse(HttpConstants.HTTP_RESPONSE_CONTINUE)); m_out.flush(); } /** * Static utility method to send a missing host response. * @throws java.io.IOException If any I/O error occurs. **/ public void sendMissingHostResponse() throws IOException { m_out.write(buildResponse(HttpURLConnection.HTTP_BAD_REQUEST)); m_out.flush(); } /** * Static utility method to send a not implemented response. * @throws java.io.IOException If any I/O error occurs. **/ public void sendNotImplementedResponse() throws IOException { m_out.write(buildResponse(HttpURLConnection.HTTP_NOT_IMPLEMENTED)); m_out.flush(); } /** * Static utility method to send a moved permanently response. * @param hostname The hostname of the new location. * @param port The port of the new location. * @param newURI The path of the new location. * @throws java.io.IOException If any I/O error occurs. **/ public void sendMovedPermanently(final String hostname, final int port, final String newURI) throws IOException { StringBuffer sb = new StringBuffer(); sb.append(HttpConstants.HEADER_LOCATION); sb.append(HttpConstants.HEADER_VALUE_DELIMITER); sb.append(HttpConstants.HTTP_SCHEME); sb.append("://"); sb.append(hostname); if (port != 80) { sb.append(':'); sb.append(Integer.toString(port)); } sb.append(newURI); sb.append(HttpConstants.HEADER_DELEMITER); m_out.write(buildResponse(301, null, sb.toString(), null)); m_out.flush(); } /** * Static utility method to send a Not Found (404) response. * @throws java.io.IOException If any I/O error occurs. **/ public void sendNotFoundResponse() throws IOException { m_out.write(buildResponse(HttpURLConnection.HTTP_NOT_FOUND)); m_out.flush(); } /* (non-Javadoc) * @see javax.servlet.ServletResponse#flushBuffer() */ synchronized public void flushBuffer() throws IOException { if (m_getOutputStreamCalled) { m_servletOutputStream.flush(); } else if (m_getWriterCalled) { m_printWriter.flush(); } if (!m_headersWritten) { writeHeaders(true); } if (m_buffer != null) { byte[] content = m_buffer.toByteArray(); copy(new ByteArrayInputStream(content), m_out); m_out.flush(); } } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getBufferSize() */ public int getBufferSize() { if (m_buffer != null) { return m_buffer.size(); } return m_bufferSize; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getCharacterEncoding() */ public String getCharacterEncoding() { return m_characterEncoding; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getContentType() */ public String getContentType() { Object contentType = m_headers.get(HttpConstants.HEADER_CONTENT_TYPE); if (contentType != null) { return contentType.toString(); } return null; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getLocale() */ public Locale getLocale() { return m_locale; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getOutputStream() */ public ServletOutputStream getOutputStream() throws IOException { m_getOutputStreamCalled = true; if (m_getWriterCalled) throw new IllegalStateException( "getWriter method has already been called for this response object."); if (m_servletOutputStream == null) { m_buffer = new ByteArrayOutputStream(m_bufferSize); m_servletOutputStream = new ServletOutputStreamImpl(m_buffer); } return m_servletOutputStream; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#getWriter() */ public PrintWriter getWriter() throws IOException { m_getWriterCalled = true; if (m_getOutputStreamCalled) throw new IllegalStateException( "getOutputStream method has already been called for this response object."); if (m_printWriter == null) { m_buffer = new ByteArrayOutputStream(m_bufferSize); m_printWriter = new PrintWriter(m_buffer); } return m_printWriter; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#isCommitted() */ public boolean isCommitted() { return m_headersWritten; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#reset() */ public void reset() { if (isCommitted()) { throw new IllegalStateException("Response has already been committed."); } m_buffer.reset(); m_printWriter = null; m_servletOutputStream = null; m_getOutputStreamCalled = false; m_getWriterCalled = false; m_headers.clear(); m_statusCode = HttpURLConnection.HTTP_OK; m_customStatusMessage = null; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#resetBuffer() */ public void resetBuffer() { if (isCommitted()) { throw new IllegalStateException("Response has already been committed."); } m_buffer.reset(); m_printWriter = null; m_servletOutputStream = null; m_getOutputStreamCalled = false; m_getWriterCalled = false; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#setBufferSize(int) */ public void setBufferSize(final int arg0) { if (isCommitted()) { throw new IllegalStateException("Response has already been committed."); } m_bufferSize = arg0; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#setCharacterEncoding(java.lang.String) */ public void setCharacterEncoding(final String arg0) { m_characterEncoding = arg0; } /* (non-Javadoc) * @see javax.servlet.ServletResponse#setContentLength(int) */ public void setContentLength(final int arg0) { m_headers.put(HttpConstants.HEADER_CONTENT_LENGTH, Integer.toString(arg0)); } /** * Can be 'close' or 'Keep-Alive'. * @param type the connection type */ public void setConnectionType(final String type) { setHeader(HttpConstants.HEADER_CONNECTION, type); } /* (non-Javadoc) * @see javax.servlet.ServletResponse#setContentType(java.lang.String) */ public void setContentType(final String arg0) { setHeader(HttpConstants.HEADER_CONTENT_TYPE, arg0); } /* (non-Javadoc) * @see javax.servlet.ServletResponse#setLocale(java.util.Locale) */ public void setLocale(final Locale arg0) { m_locale = arg0; } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#addCookie(javax.servlet.http.Cookie) */ public void addCookie(final Cookie cookie) { if (m_cookies == null) { m_cookies = new ArrayList(); } if (!m_cookies.contains( cookie )) { m_cookies.add( cookie ); } } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#containsHeader(java.lang.String) */ public boolean containsHeader(final String name) { return m_headers.get(name) != null; } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String) */ public String encodeURL(final String url) { // TODO Re-examing if/when sessions are supported. return url; } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String) */ public String encodeRedirectURL(final String url) { return encodeUrl(url); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#encodeUrl(java.lang.String) */ public String encodeUrl(final String url) { //Deprecated method used for Java 1.3 compatibility. return URLEncoder.encode(url); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#encodeRedirectUrl(java.lang.String) */ public String encodeRedirectUrl(String url) { return encodeURL(url); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#sendError(int, java.lang.String) */ public void sendError(final int sc, final String msg) throws IOException { if (m_headersWritten) throw new IllegalStateException( "Response has already been committed, unable to send error."); m_out.write(buildResponse(sc, msg)); m_out.flush(); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#sendError(int) */ public void sendError(final int sc) throws IOException { sendError(sc, null); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#sendRedirect(java.lang.String) */ public void sendRedirect(final String location) throws IOException { if (m_headersWritten) { throw new IllegalStateException("Response has already been committed."); } Map map = new HashMap(); map.put("Location", location); m_out.write(buildResponse(307, map, null, null)); m_out.flush(); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long) */ public void setDateHeader(final String name, final long date) { setHeader(name, m_dateFormat.format(new Date(date))); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long) */ public void addDateHeader(final String name, final long date) { addHeader(name, m_dateFormat.format(new Date(date))); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String) */ public void setHeader(final String name, final String value) { if (value != null) { m_headers.put(name, value); } } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String) */ public void addHeader(final String name, final String value) { if (value != null && m_headers.containsKey(name)) { Object pvalue = m_headers.get(name); if (pvalue instanceof List) { ((List) pvalue).add(value); } else { List vlist = new ArrayList(); vlist.add(pvalue); vlist.add(value); } } else { setHeader(name, value); } } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int) */ public void setIntHeader(final String name, final int value) { setHeader(name, Integer.toString(value)); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int) */ public void addIntHeader(final String name, final int value) { addHeader(name, Integer.toString(value)); } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#setStatus(int) */ public void setStatus(final int sc) { m_statusCode = sc; } /* (non-Javadoc) * @see javax.servlet.http.HttpServletResponse#setStatus(int, java.lang.String) */ public void setStatus(int sc, String sm) { m_statusCode = sc; m_customStatusMessage = sm; } /** * @param code HTTP code * @return byte array of response */ public static byte[] buildResponse(int code) { return buildResponse(code, null, null, HttpConstants.DEFAULT_HTML_HEADER); } /** * @param code HTTP code * @param userMessage user message * @return byte array of response */ public static byte[] buildResponse(int code, String userMessage) { return buildResponse(code, null, userMessage, HttpConstants.DEFAULT_HTML_HEADER); } /** * Build a response given input parameters. * * @param code HTTP code * @param headers Map of HTTP headers * @param userMessage user message * @param htmlStartTag custom HTML document start * @return byte array of response. */ public static byte[] buildResponse(int code, Map headers, String userMessage, String htmlStartTag) { StringBuffer buffer = new StringBuffer(); buffer.append(HttpConstants.HTTP11_VERSION); buffer.append(' '); buffer.append(code); buffer.append(' '); if (code > 399) { buffer.append("HTTP Error "); buffer.append(code); } buffer.append(HttpConstants.HEADER_DELEMITER); if (code == 100) { buffer.append(HttpConstants.HEADER_DELEMITER); } else if (headers != null) { if (headers.containsKey(HttpConstants.HEADER_CONTENT_TYPE)) { appendHeader(buffer, HttpConstants.HEADER_CONTENT_TYPE, headers.get(HttpConstants.HEADER_CONTENT_TYPE).toString()); } for (Iterator i = headers.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry) i.next(); if (entry.getValue() == null) { throw new IllegalStateException( "Header map contains value with null value: " + entry.getKey()); } appendHeader(buffer, entry.getKey().toString(), entry.getValue().toString()); } } //Only append error HTML messages if the return code is in the error range. if (code > 399) { //TODO: Consider disabling the HTML generation, optionally, so clients have full control of the response content. if (htmlStartTag == null) { htmlStartTag = HttpConstants.DEFAULT_HTML_HEADER; } buffer.append(htmlStartTag); buffer.append("<h1>"); buffer.append(code); buffer.append(' '); buffer.append("HTTP Error "); buffer.append(code); if (userMessage != null) { buffer.append("</h1><p>"); buffer.append(userMessage); buffer.append("</p>"); } else { buffer.append("</h1>"); } buffer.append("<h3>" + HttpConstants.SERVER_INFO + "</h3></html>"); } return buffer.toString().getBytes(); } /** * Convert a cookie into the HTTP header in response. * * @param cookie Cookie * @return String as byte array of cookie as header */ private byte[] cookieToHeader( Cookie cookie ) { if (cookie == null || cookie.getName() == null || cookie.getValue() == null) { throw new IllegalArgumentException( "Invalid cookie" ); } StringBuffer sb = new StringBuffer(); sb.append( cookie.getName() ); sb.append( '=' ); sb.append( cookie.getValue() ); //TODO: Implement all Cookie fields return sb.toString().getBytes(); } /** * Append name and value as an HTTP header to a StringBuffer * * @param sb StringBuffer * @param name Name * @param value Value */ private static void appendHeader(StringBuffer sb, String name, String value) { sb.append(name); sb.append(HttpConstants.HEADER_VALUE_DELIMITER); sb.append(value); sb.append(HttpConstants.HEADER_DELEMITER); } }