| /* |
| * 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); |
| } |
| } |