package org.pereni.dina.http;
import org.pereni.dina.server.ServerRuntimeException;
import org.pereni.dina.util.MultiMap;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Provides the default implementation of a HttpResponse.
*
* @author Remus Pereni <remus@pereni.org>
*/
public class HttpResponseImpl implements HttpResponse {
/**
* Server string
*/
public static final String SERVER_INFO = "Dina/0.1";
/**
* Class logger.
*/
private static final Logger LOG = Logger.getLogger( HttpResponseImpl.class.getName());
/**
* Default response version
*/
private static final String RESPONSE_VERSION = "HTTP/1.1";
/**
* Response content type
*/
private String contentType = "text/html";
/**
* Response status code
*/
private int statusCode = 200;
/**
* Response status message
*/
private String statusMessage = "OK";
/**
* Response content length
*/
private long contentLength = -1;
/**
* Flag monitoring if the response is committed
*/
private boolean isCommitted = false;
/**
* Response headers
*/
private MultiMap headers = new MultiMap();
/**
* Response character encoding
*/
private String characterEncoding = "UTF-8";
/**
* Response output channel
*/
private WritableByteChannel outputByteChannel;
/**
* Temp output for calculating content length.
*/
private ByteArrayOutputStream tempOutput;
/**
* Date format used for sending date headers.
*/
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
/**
* Print writer exposed by the interface
*/
private PrintWriter outputWriter;
/**
* The response servlet context
*/
private ServletContext servletContext;
/**
* Flag indicating if there is a body in the response or the response is header only.
*/
private boolean isHeaderOnlyResponse = false;
/**
* Default constructor
*/
public HttpResponseImpl() {
addHeader( Http.Headers.Server, SERVER_INFO);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
}
/**
* Constructor with the output channel already set.
* @param writableByteChannel
*/
public HttpResponseImpl(WritableByteChannel writableByteChannel) {
this();
this.outputByteChannel = writableByteChannel;
}
/**
*
* @param outputByteChannel
*/
public void setOutputChannel(WritableByteChannel outputByteChannel) {
this.outputByteChannel = outputByteChannel;
}
/**
*
* @param statusCode
* @param statusMessage
* @throws IOException
*/
@Override
public void sendError(int statusCode, String statusMessage) throws IOException {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
this.contentType = null;
this.statusCode = statusCode;
this.statusMessage = statusMessage;
flushBuffer();
}
/**
*
* @param statusCode
* @throws IOException
*/
@Override
public void sendError(int statusCode) throws IOException {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
this.contentType = null;
this.statusCode = statusCode;
this.statusMessage = null;
flushBuffer();
}
/**
*
* @param name the name of the header to set
* @param date the assigned date value
*/
@Override
public void setDateHeader(String name, long date) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.set(name, dateFormat.format( date ));
}
/**
*
* @param name the name of the header to set
* @param date the assigned date value
*/
@Override
public void addDateHeader(String name, long date) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.put(name, dateFormat.format( date ));
}
/**
*
* @param name the name of the header to set
* @param value the assigned header value
*/
@Override
public void setHeader(String name, String value) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.set(name, value);
}
/**
*
* @param name the name of the header to set
* @param value the assigned header value
*/
@Override
public void addHeader(String name, String value) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.put(name, value);
}
/**
*
* @param name the name of the header
* @param value the assigned integer value
*/
@Override
public void setIntHeader(String name, int value) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.set(name, String.valueOf(value));
}
/**
*
* @param name the name of the header
* @param value the assigned integer value
*/
@Override
public void addIntHeader(String name, int value) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
headers.put(name, String.valueOf(value));
}
/**
*
* @param statusCode
*/
@Override
public void setStatus(int statusCode) {
if( isCommitted() ) throw new IllegalStateException("The response was already committed");
this.statusCode = statusCode;
// NO Content
if( statusCode == 204 ) {
setHeaderOnlyResponse(true);
}
}
/**
*
* @return
*/
@Override
public int getStatus() {
return statusCode;
}
/**
*
* @return
*/
@Override
public String getCharacterEncoding() {
return this.characterEncoding;
}
/**
*
* @return
*/
@Override
public String getContentType() {
return this.contentType;
}
/**
*
* @return
* @throws IOException
*/
@Override
public PrintWriter getWriter() throws IOException {
if( outputWriter == null ) {
// Check to see if we already have the output byte channel
// if not then we'll use a temp buffer to write what we need into
// it and when the actual output will be available make sure we
// copy the content.
//
// The reason we need a temp buffer is to capture error codes and
// messages processed during request processing
if( getOutputChannel() != null && contentLength != -1 ) {
isCommitted = true;
outputWriter = new PrintWriter( Channels.newWriter( outputByteChannel, getCharacterEncoding()));
sendStatusLine(outputWriter );
sendHeaders(outputWriter);
outputWriter.flush();
} else {
LOG.log(Level.FINER, "No output byte channel yet or contentLength so working with temp output");
tempOutput = new ByteArrayOutputStream();
outputWriter = new PrintWriter( tempOutput );
}
}
return outputWriter;
}
/**
*
* @param charset charset - a String specifying only the character set defined by IANA
*/
@Override
public void setCharacterEncoding(String charset) {
this.characterEncoding = charset;
}
/**
*
* @param contentLength
*/
@Override
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
/**
*
* @param contentType
*/
@Override
public void setContentType(String contentType ) {
this.contentType = contentType;
}
/**
*
* @return
*/
@Override
public boolean isCommitted() {
return this.isCommitted;
}
/**
*
*/
@Override
public void flushBuffer() {
LOG.finer("Flush buffer requested, response is committed " + isCommitted());
if( outputWriter == null ) {
try {
getWriter();
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to create writer", e);
}
}
outputWriter.flush();
}
/**
*
* @return
*/
@Override
public ServletContext getServletContext() {
return servletContext;
}
/**
*
* @param servletContext
*/
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
*
* @param out
*/
private void sendStatusLine(PrintWriter out) {
out.print(RESPONSE_VERSION);
out.print( Http.WS );
out.print( statusCode );
if( statusMessage != null ) {
out.print( Http.WS );
out.print( statusMessage );
}
out.print( Http.EOL );
LOG.finer("Response sent status line " + RESPONSE_VERSION + " " + statusCode );
}
/**
*
* @param out
*/
private void sendHeaders(PrintWriter out){
if( getContentType() != null ) {
sendHeader(out, Http.Headers.Content_Type, getContentType());
}
Iterator<String> headerKeyIterator = headers.getKeys().iterator();
String keyName;
Object keyValue;
while( headerKeyIterator.hasNext() ){
keyName = headerKeyIterator.next();
keyValue = headers.getActualValue( keyName );
if( keyValue instanceof List) {
Iterator<String> valueListIterator = ( (List) keyValue).iterator();
while( valueListIterator.hasNext() ){
sendHeader(out, keyName, String.valueOf(valueListIterator.next()));
}
} else {
sendHeader(out, keyName, String.valueOf(keyValue));
}
}
if( contentLength >= 0 ) {
sendHeader(out, Http.Headers.Content_Length, String.valueOf(contentLength));
}
out.print( Http.EOH );
}
/**
*
* @return
*/
@Override
public WritableByteChannel getOutputChannel() {
return outputByteChannel;
}
/**
*
* @return
*/
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append( getStatus() ).append(' ');
res.append(getContentType()).append(' ');
res.append(contentLength);
return res.toString();
}
/**
*
*/
public void dump() {
LOG.finer("Response dump " + toString());
}
/**
*
* @return
*/
public boolean isHeaderOnlyResponse() {
return isHeaderOnlyResponse;
}
/**
*
* @param headerOnlyResponse
*/
public void setHeaderOnlyResponse(boolean headerOnlyResponse) {
isHeaderOnlyResponse = headerOnlyResponse;
}
/**
*
* @param out
* @param headerName
* @param headerValue
*/
private void sendHeader(PrintWriter out, String headerName, String headerValue) {
out.print( headerName);
out.print( Http.HEADER_SEPARATOR );
out.print( headerValue );
out.print( Http.EOL );
}
/**
*
*/
private void copyTempOutputToOutputChannel() {
if( outputByteChannel == null ) {
LOG.warning("Unable to copy temp content to the output, output byte channel is null, ignoring output");
return;
}
if( outputWriter != null ) outputWriter.flush();
if( !isCommitted() ) {
isCommitted = true;
outputWriter = new PrintWriter( Channels.newWriter( outputByteChannel, getCharacterEncoding()));
sendStatusLine(outputWriter );
sendHeaders(outputWriter);
outputWriter.flush();
}
if( !isHeaderOnlyResponse() && tempOutput != null && tempOutput.size() > 0 ) {
ByteBuffer tempBuffer = ByteBuffer.wrap( tempOutput.toByteArray());
try {
this.outputByteChannel.write(tempBuffer);
LOG.finer("Wrote " + tempOutput.size() + " bytes to output channel");
} catch (IOException e) {
throw new ServerRuntimeException("Unable to empty temporary output buffer");
}
}
}
/**
*
*/
public void finalizeResponse() {
if( isCommitted() ) {
outputWriter.flush();
return;
}
if( tempOutput != null && contentLength == -1 ) {
setContentLength(tempOutput.size());
}
copyTempOutputToOutputChannel();
}
}