/**
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.openejb.server.httpd;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.server.httpd.session.SessionManager;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.OpenEjbVersion;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import static java.util.Collections.singletonList;
/**
* This class takes care of HTTP Responses. It sends data back to the browser.
*/
public class HttpResponseImpl implements HttpResponse {
private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB_SERVER, HttpResponseImpl.class.getName());
private static final String DEFAULT_CONTENT_TYPE = SystemInstance.get().getProperty("openejb.http.default-content-type", "text/html");
/**
* Response string
*/
private String responseString = "OK";
/**
* Code
*/
private int code = HttpServletResponse.SC_OK;
/**
* Response headers
*/
private final Map<String, List<String>> headers = new HashMap<>();
/**
* the writer for the response
*/
private transient PrintWriter writer;
/**
* the raw body
*/
private transient ServletByteArrayOutputStream sosi;
/**
* the HTTP version
*/
public static final String HTTP_VERSION = "HTTP/1.1";
/**
* a line feed character
*/
public static final String CRLF = "\r\n";
/**
* a space character
*/
public static final String SP = " ";
/**
* a colon and space
*/
public static final String CSP = ": ";
/**
* the server to send data from
*/
public static String server;
private HttpRequestImpl request;
private URLConnection content;
private boolean commited = false;
private String encoding = "UTF-8";
private Locale locale = Locale.getDefault();
protected void setRequest(final HttpRequestImpl request) {
this.request = request;
}
/**
* sets a header to be sent back to the browser
*
* @param name the name of the header
* @param value the value of the header
*/
public void setHeader(final String name, final String value) {
headers.put(name, new ArrayList<>(singletonList(value)));
}
@Override
public void setIntHeader(final String s, final int i) {
setHeader(s, Integer.toString(i));
}
@Override
public void setStatus(final int i) {
setCode(i);
}
@Override
public void setStatus(final int i, final String s) {
setCode(i);
setStatusMessage(s);
}
@Override
public void addCookie(final Cookie cookie) {
setHeader(cookie.getName(), cookie.getValue());
}
@Override
public void addDateHeader(final String s, final long l) {
setHeader(s, Long.toString(l));
}
@Override
public void addHeader(final String s, final String s1) {
Collection<String> list = headers.get(s);
if (list == null) {
setHeader(s, s1);
} else {
list.add(s1);
}
}
@Override
public void addIntHeader(final String s, final int i) {
setIntHeader(s, i);
}
@Override
public boolean containsHeader(final String s) {
return headers.containsKey(s);
}
@Override
public String encodeURL(final String s) {
return toEncoded(s);
}
@Override
public String encodeRedirectURL(final String s) {
return toEncoded(s);
}
@Override
public String encodeUrl(final String s) {
return toEncoded(s);
}
@Override
public String encodeRedirectUrl(final String s) {
return encodeRedirectURL(s);
}
/**
* Gets a header based on the name passed in
*
* @param name The name of the header
* @return the value of the header
*/
public String getHeader(final String name) {
final Collection<String> strings = headers.get(name);
return strings == null ? null : strings.iterator().next();
}
@Override
public Collection<String> getHeaderNames() {
return headers.keySet();
}
@Override
public Collection<String> getHeaders(final String s) {
return headers.get(s);
}
@Override
public int getStatus() {
return getCode();
}
@Override
public void sendError(final int i) throws IOException {
setCode(i);
}
@Override
public void sendError(final int i, final String s) throws IOException {
setCode(i);
setStatusMessage(s);
}
@Override
public void sendRedirect(final String path) throws IOException {
if (commited) {
throw new IllegalStateException("response already committed");
}
resetBuffer();
try {
setStatus(SC_FOUND);
setHeader("Location", base() + toEncoded(path));
} catch (final IllegalArgumentException e) {
setStatus(SC_NOT_FOUND);
}
}
@Override
public void setDateHeader(final String s, final long l) {
addDateHeader(s, l);
}
/**
* gets the OutputStream to send data to the browser
*
* @return the OutputStream to send data to the browser
*/
public ServletOutputStream getOutputStream() {
return sosi;
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
@Override
public boolean isCommitted() {
return commited;
}
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
}
}
@Override
public int getBufferSize() {
return sosi.getOutputStream().size();
}
@Override
public String getCharacterEncoding() {
return encoding;
}
/**
* sets the HTTP response code to be sent to the browser. These codes are:
* <p/>
* OPTIONS = 0
* GET = 1
* HEAD = 2
* POST = 3
* PUT = 4
* DELETE = 5
* TRACE = 6
* CONNECT = 7
* UNSUPPORTED = 8
*
* @param code the code to be sent to the browser
*/
public void setCode(final int code) {
this.code = code;
commited = true;
}
/**
* gets the HTTP response code
*
* @return the HTTP response code
*/
public int getCode() {
return code;
}
/**
* sets the content type to be sent back to the browser
*
* @param type the type to be sent to the browser (i.e. "text/html")
*/
public void setContentType(final String type) {
setHeader("Content-Type", type);
}
@Override
public void setLocale(final Locale loc) {
locale = loc;
}
/**
* gets the content type that will be sent to the browser
*
* @return the content type (i.e. "text/html")
*/
public String getContentType() {
return getHeader("Content-Type");
}
@Override
public Locale getLocale() {
return locale;
}
/**
* Sets the response string to be sent to the browser
*
* @param responseString the response string
*/
public void setResponseString(final String responseString) {
this.responseString = responseString;
}
/**
* resets the data to be sent to the browser
*/
public void reset() {
initBody();
}
@Override
public void resetBuffer() {
sosi.getOutputStream().reset();
}
@Override
public void setBufferSize(final int i) {
// no-op
}
@Override
public void setCharacterEncoding(final String s) {
encoding = s;
}
@Override
public void setContentLength(final int i) {
// no-op
}
@Override
public void setContentLengthLong(final long length) {
// no-op
}
/**
* resets the data to be sent to the browser with the response code and response
* string
*
* @param code the code to be sent to the browser
* @param responseString the response string to be sent to the browser
*/
public void reset(final int code, final String responseString) {
setCode(code);
setResponseString(responseString);
initBody();
}
/*------------------------------------------------------------*/
/* Methods for writing out a response */
/*------------------------------------------------------------*/
/**
* creates a new instance of HttpResponseImpl with default values
*/
protected HttpResponseImpl() {
this(200, "OK", DEFAULT_CONTENT_TYPE);
}
/**
* Creates a new HttpResponseImpl with user provided parameters
*
* @param code the HTTP Response code, see <a href="http://www.ietf.org/rfc/rfc2616.txt">http://www.ietf.org/rfc/rfc2616.txt</a>
* for these codes
* @param responseString the response string to be sent back
* @param contentType the content type to be sent back
*/
protected HttpResponseImpl(final int code, final String responseString, final String contentType) {
this.responseString = responseString;
this.code = code;
// Default headers
setHeader("Server", getServerName());
setHeader("Connection", "close");
setHeader("Content-Type", contentType);
// create the body.
initBody();
}
/**
* Takes care of sending the response line, headers and body
* <p/>
* HTTP/1.1 200 OK
* Server: Netscape-Enterprise/3.6 SP3
* Date: Thu, 07 Jun 2001 17:30:42 GMT
* Content-Type: text/html
* Connection: close
*
* @param output the output to send the response to
* @throws java.io.IOException if an exception is thrown
*/
protected void writeMessage(final OutputStream output, final boolean indent) throws IOException {
flushBuffer();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
closeMessage();
writeResponseLine(out);
writeHeaders(out);
writeBody(out, indent);
out.flush();
output.write(baos.toByteArray());
output.flush();
}
/**
* initalizes the body
*/
private void initBody() {
sosi = new ServletByteArrayOutputStream();
writer = new PrintWriter(sosi);
}
/**
* Creates a string version of the response similar to:
* <p/>
* HTTP/1.1 200 OK
*
* @return the string value of this HttpResponseImpl
*/
public String toString() {
return HTTP_VERSION + SP + code + SP + responseString;
}
/**
* closes the message sent to the browser
*/
private void closeMessage() {
setContentLengthHeader();
setCookieHeader();
}
private void setContentLengthHeader() {
if (content == null) {
writer.flush();
writer.close();
final int length = sosi.getOutputStream().size();
setHeader("Content-Length", length + "");
} else {
setHeader("Content-Length", content.getContentLength() + "");
}
}
private void setCookieHeader() {
if (request == null) {
return;
}
final HttpSession session = request.getSession(false);
if (session == null) {
return;
}
setHeader(HttpRequest.HEADER_SET_COOKIE, SessionManager.EJBSESSIONID + '=' + session.getId() + "; Path=/");
}
/**
* Writes a response line similar to this:
* <p/>
* HTTP/1.1 200 OK
* <p/>
* to the browser
*
* @param out the output stream to write the response line to
* @throws java.io.IOException if an exception is thrown
*/
private void writeResponseLine(final DataOutput out) throws IOException {
out.writeBytes(HTTP_VERSION);
out.writeBytes(SP);
out.writeBytes(code + "");
out.writeBytes(SP);
if (responseString != null) {
out.writeBytes(responseString);
}
out.writeBytes(CRLF);
}
/**
* writes the headers out to the browser
*
* @param out the output stream to be sent to the browser
* @throws java.io.IOException if an exception is thrown
*/
private void writeHeaders(final DataOutput out) throws IOException {
for (final Map.Entry<String, List<String>> entry : headers.entrySet()) {
if (entry.getValue().size() == 1) {
writeHeader(out, entry.getKey(), entry.getValue().get(0));
} else if (entry.getValue().size() > 1) {
for (final String val : entry.getValue()) {
writeHeader(out, entry.getKey(), val);
}
}
}
}
private void writeHeader(final DataOutput out, final String name, final String value) throws IOException {
out.writeBytes(name);
out.writeBytes(CSP);
out.writeBytes(value);
out.writeBytes(CRLF);
}
/**
* writes the body out to the browser
*
* @param out the output stream that writes to the browser
* @param indent format xml
* @throws java.io.IOException if an exception is thrown
*/
private void writeBody(final DataOutput out, final boolean indent) throws IOException {
out.writeBytes(CRLF);
if (content == null) {
if (indent && OpenEJBHttpServer.isTextXml(headers)) {
final String xml = new String(sosi.getOutputStream().toByteArray());
out.write(OpenEJBHttpServer.reformat(xml).getBytes());
} else {
out.write(sosi.getOutputStream().toByteArray());
}
} else {
final InputStream in = content.getInputStream();
final byte[] buf = new byte[1024];
int i;
while ((i = in.read(buf)) != -1) {
out.write(buf, 0, i);
}
}
}
/**
* gets the name of the server being used
*
* @return the name of the server
*/
public String getServerName() {
if (server == null) {
final String version = OpenEjbVersion.get().getVersion();
final String os = System.getProperty("os.name") + "/" + System.getProperty("os.version") + " (" + System.getProperty("os.arch") + ")";
server = "OpenEJB/" + version + " " + os;
}
return server;
}
/**
* This could be improved at some day in the future
* to also include a stack trace of the exceptions
*
* @param message the error message to be sent
* @return the HttpResponseImpl that this error belongs to
*/
@SuppressWarnings("unused")
protected static HttpResponseImpl createError(final String message) {
return createError(message, null);
}
/**
* creates an error with user defined variables
*
* @param message the message of the error
* @param t a Throwable to print a stack trace to
* @return the HttpResponseImpl that this error belongs to
*/
protected static HttpResponseImpl createError(String message, final Throwable t) {
final HttpResponseImpl res = new HttpResponseImpl(500, "Internal Server Error", "text/html");
final PrintWriter body;
try {
body = res.getWriter();
} catch (final IOException e) { // impossible normally
return res;
}
body.println("<html>");
body.println("<body>");
body.println("<h3>Internal Server Error</h3>");
body.println("<br><br>");
if (LOGGER.isDebugEnabled()) { // this is not an error, don't log it by default
LOGGER.error(String.valueOf(t), t);
}
if (message != null) {
final StringTokenizer msg = new StringTokenizer(message, "\n\r");
while (msg.hasMoreTokens()) {
body.print(msg.nextToken());
body.println("<br>");
}
}
if (t != null) {
PrintWriter writer = null;
try {
body.println("<br><br>");
body.println("Stack Trace:<br>");
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
writer = new PrintWriter(baos);
t.printStackTrace(writer);
writer.flush();
message = new String(baos.toByteArray());
final StringTokenizer msg = new StringTokenizer(message, "\n\r");
while (msg.hasMoreTokens()) {
body.print(msg.nextToken());
body.println("<br>");
}
} catch (final Exception e) {
//no-op
} finally {
if (writer != null) {
writer.close();
}
}
}
body.println("</body>");
body.println("</html>");
return res;
}
/**
* Creates a forbidden response to be sent to the browser using IP authentication
*
* @param ip the ip that is forbidden
* @return the HttpResponseImpl that this error belongs to
*/
@SuppressWarnings("unused")
protected static HttpResponseImpl createForbidden(final String ip) {
final HttpResponseImpl res = new HttpResponseImpl(403, "Forbidden", "text/html");
final PrintWriter body;
try {
body = res.getWriter();
} catch (final IOException e) { // normally impossible
return res;
}
body.println("<html>");
body.println("<body>");
body.println("<h3>Forbidden</h3>");
body.println("<br><br>");
// Add more text here
// IP not allowed, etc.
body.println("IP address: " + ip + " is not registered on this server, please contact your system administrator.");
body.println("</body>");
body.println("</html>");
return res;
}
/**
* writes this object out to a file
*
* @param out the ObjectOutputStream to write to
* @throws java.io.IOException if an exception is thrown
*/
private void writeObject(final java.io.ObjectOutputStream out) throws IOException {
/** Response string */
out.writeObject(responseString);
/** Code */
out.writeInt(code);
/** Response headers */
out.writeObject(headers);
/** Response body */
writer.flush();
final byte[] body = sosi.getOutputStream().toByteArray();
//System.out.println("[] body "+body.length );
out.writeObject(body);
}
/**
* Reads in a serilized HttpResponseImpl object from a file
*
* @param in the input to read the object from
* @throws java.io.IOException if an exception is thrown
* @throws ClassNotFoundException if an exception is thrown
*/
@SuppressWarnings({"unchecked"})
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
/** Response string */
this.responseString = (String) in.readObject();
/** Code */
this.code = in.readInt();
/** Response headers */
final Map headers = (Map) in.readObject();
this.headers.clear();
this.headers.putAll(headers);
/** Response body */
final byte[] body = (byte[]) in.readObject();
//System.out.println("[] body "+body.length );
sosi = new ServletByteArrayOutputStream();
sosi.write(body);
writer = new PrintWriter(sosi);
}
/**
* @param content The content to set.
*/
public void setContent(final URLConnection content) {
this.content = content;
}
public void setStatusMessage(final String responseString) {
this.setResponseString(responseString);
}
private String base() {
return request == null ? "" : request.getURI().getScheme() + "://" + request.getURI().getAuthority();
}
private String toEncoded(final String url) {
return url; // should add ;JSESSIONID=xxx but breaks other things and here we don't need it that much
}
}