javaxt.http.servlet.HttpServletResponse Maven / Gradle / Ivy
package javaxt.http.servlet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Calendar;
import java.util.Collection;
import java.util.TimeZone;
import java.util.zip.GZIPOutputStream;
import javaxt.http.Server.SocketConnection;
//******************************************************************************
//** HttpServletResponse Class
//******************************************************************************
/**
* Used to generate a response to an HTTP request. This class implements the
* javax.servlet.http.HttpServletResponse interface defined in Version 2.5 of
* the Java Servlet API.
*
******************************************************************************/
public class HttpServletResponse implements javax.servlet.http.HttpServletResponse {
private SocketConnection connection;
private HttpServletRequest request;
private Integer statusCode;
private String statusMessage;
private java.util.HashMap headers;
private boolean writeHeader = true;
private Boolean chunked = null;
private int bufferSize = 8096; // 8KB
private static final String z = "GMT";
private static final TimeZone tz = TimeZone.getTimeZone(z);
private static final ByteBuffer CRLF = getCRLF();
private String charSet = "UTF-8";
private java.util.Locale locale = java.util.Locale.getDefault();
private java.util.ArrayList cookies = new java.util.ArrayList();
private Long startRange, endRange;
private ServletOutputStream servletOutputStream;
// **************************************************************************
// ** Constructor
// **************************************************************************
/** Creates a new instance of this class. */
public HttpServletResponse(HttpServletRequest request, SocketConnection connection) {
this.connection = connection;
this.headers = new java.util.HashMap();
this.request = request;
// Set default response headers
setHeader("Accept-Ranges", "bytes");
setHeader("Connection", (request.isKeepAlive() ? "Keep-Alive" : "Close"));
setHeader("Server", request.getServletContext().getServerInfo());
setHeader("Date", getDate(Calendar.getInstance()));
setStatus(200, "OK");
// Parse range header (e.g. "Range: bytes=653316-676676")
String range = request.getHeader("Range");
if (range != null) {
for (String kvp : range.split(";")) { // can there be multiple keys?
kvp = kvp.trim();
int eq = kvp.indexOf("=");
if (eq > 0) {
String key = kvp.substring(0, eq).trim().toLowerCase();
String val = kvp.substring(eq + 1).trim();
if (key.equals("bytes")) {
String[] arr = val.split("-");
try {
startRange = Long.parseLong(arr[0].trim());
} catch (Exception e) {
}
try {
endRange = Long.parseLong(arr[1].trim());
} catch (Exception e) {
}
}
}
}
}
}
// **************************************************************************
// ** addCookie
// **************************************************************************
/**
* Adds the specified cookie to the response.
*/
public void addCookie(Cookie cookie) {
cookies.add(cookie);
}
// **************************************************************************
// ** setContentLength
// **************************************************************************
/**
* Sets the "Content-Length" in the response header. Note that the
* "Content-Length" header is set automatically by most of the write()
* methods. So unless you're writing directly to the ServletOutputStream,
* you do not need to set this header.
*/
@Override
public void setContentLength(int contentLength) {
setContentLength(Long.parseLong(contentLength + ""));
}
// **************************************************************************
// ** setContentLength
// **************************************************************************
/**
* Sets the "Content-Length" in the response header. Note that the
* "Content-Length" header is set automatically by most of the write()
* methods. So unless you're writing directly to the ServletOutputStream,
* you do not need to set this header.
*/
public void setContentLength(long contentLength) {
setHeader("Content-Length", contentLength + "");
}
// **************************************************************************
// ** getContentLength
// **************************************************************************
/**
* Returns the "Content-Length" defined in the response header. Returns null
* if the "Content-Length" is not defined or is less than zero.
*/
public Long getContentLength() {
try {
long l = Long.parseLong(getHeader("Content-Length"));
if (l < 0)
return null;
else
return l;
} catch (Exception e) {
return null;
}
}
// **************************************************************************
// ** setContentType
// **************************************************************************
/**
* Used to set/update the "Content-Type" response header (e.g.
* "text/html; charset=utf-8").
*/
@Override
public void setContentType(String contentType) {
setHeader("Content-Type", contentType);
}
// **************************************************************************
// ** getContentType
// **************************************************************************
/**
* Returns the "Content-Type" defined in the response header (e.g.
* "text/html; charset=utf-8").
*/
@Override
public String getContentType() {
return getHeader("Content-Type");
}
// **************************************************************************
// ** setCharacterEncoding
// **************************************************************************
/**
* Sets the name of the character encoding used in the response. Default is
* "UTF-8".
*
* @param charset
* String specifying the character set defined by
* IANA
* .
*/
@Override
public void setCharacterEncoding(String charset) {
if (charset != null && !charset.equalsIgnoreCase(charSet)) {
// Test the charset
try {
charset.getBytes(charset);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// If we're still here, update the class variable
charSet = charset;
}
}
// **************************************************************************
// ** getCharacterEncoding
// **************************************************************************
/**
* Returns the name of the character encoding used in the response.
*/
@Override
public String getCharacterEncoding() {
return charSet;
}
// **************************************************************************
// ** setLocale
// **************************************************************************
/**
* Sets the locale of the response. The locale is communicated via the
* "Content-Language" header and the character encoding in the
* "Content-Type" header.
*/
@Override
public void setLocale(java.util.Locale locale) {
this.locale = locale;
}
// **************************************************************************
// ** getLocale
// **************************************************************************
/**
* Returns the locale specified for this response using the setLocale()
* method.
*/
@Override
public java.util.Locale getLocale() {
return locale;
}
// **************************************************************************
// ** addHeader
// **************************************************************************
/**
* Adds a response header with the given name and value. According to spec,
* http response headers should be allowed to have multiple values. However,
* this implementation does not currently allow response headers to have
* multiple values.
*/
@Override
public void addHeader(String name, String value) {
setHeader(name, value);
}
// **************************************************************************
// ** setHeader
// **************************************************************************
/**
* Sets a response header with the given name and value.
*/
@Override
public void setHeader(String name, String value) {
if (!writeHeader)
return;
if (name == null)
return;
if (name.equalsIgnoreCase("Set-Cookie")) {
for (String cookie : value.split(";")) {
cookie = cookie.trim();
if (cookie.contains("=")) {
name = cookie.substring(0, cookie.indexOf("=")).trim();
value = cookie.substring(cookie.indexOf("=") + 1).trim();
cookies.add(new Cookie(name, value));
} else {
cookies.add(new Cookie(cookie));
}
}
} else {
// Don't set "Transfer-Encoding" to "chunked" if the client doesn't
// support it. Servers are explicitly forbidden from sending that
// particular encoding type to clients announcing themselves as
// HTTP/1.0 (e.g. Squid 2.5).
if (name.equalsIgnoreCase("Transfer-Encoding") && value != null) {
if (value.equalsIgnoreCase("chunked")) {
String httpClient = request.getHttpVersion();
if (httpClient == null)
return;
if (httpClient.equals("0.9") || httpClient.equals("1.0")) {
return;
}
}
}
headers.put(name, value);
}
}
// **************************************************************************
// ** getHeader
// **************************************************************************
@Override
public String getHeader(String name) {
java.util.Iterator it = headers.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (key.equalsIgnoreCase(name)) {
return headers.get(key);
}
}
return null;
}
// **************************************************************************
// ** containsHeader
// **************************************************************************
/**
* Returns a boolean indicating whether the named response header has
* already been set.
*/
@Override
public boolean containsHeader(String name) {
return (getHeader(name) != null);
}
// **************************************************************************
// ** setDateHeader
// **************************************************************************
/**
* Sets a response header with the given name and date-value.
*/
@Override
public void setDateHeader(String name, long date) {
setHeader(name, getDate(date));
}
// **************************************************************************
// ** addDateHeader
// **************************************************************************
/**
* Adds a response header with the given name and date-value. According to
* spec, http response headers should be allowed to have multiple values.
* However, this implementation does not currently allow response headers to
* have multiple values.
*/
@Override
public void addDateHeader(String name, long date) {
setDateHeader(name, date);
}
// **************************************************************************
// ** setIntHeader
// **************************************************************************
/**
* Sets a response header with the given name and integer value.
*/
@Override
public void setIntHeader(String name, int value) {
setHeader(name, value + "");
}
// **************************************************************************
// ** addIntHeader
// **************************************************************************
/**
* Adds a response header with the given name and integer value. According
* to spec, http response headers should be allowed to have multiple values.
* However, this implementation does not currently allow response headers to
* have multiple values.
*/
@Override
public void addIntHeader(String name, int value) {
setIntHeader(name, value);
}
// **************************************************************************
// ** setBufferSize
// **************************************************************************
/**
* Sets the preferred buffer size for the body of the response. A larger
* buffer allows more content to be sent to the client at a time. A smaller
* buffer decreases server memory load and allows the client to start
* receiving data more quickly.
*
*
* This method must be called before any response body content is written.
*/
@Override
public void setBufferSize(int size) {
if (size > 0)
bufferSize = size;
}
// **************************************************************************
// ** setBufferSize
// **************************************************************************
/**
* Returns the buffer size used for the response.
*/
@Override
public int getBufferSize() {
return bufferSize;
}
// **************************************************************************
// ** setStatus
// **************************************************************************
@Override
public void setStatus(int sc) {
this.setStatus(sc, getStatusMessage(sc));
}
@Override
public void setStatus(int statusCode, String statusMessage) {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
@Override
public int getStatus() {
if (statusCode == null)
return 200;
return statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
// **************************************************************************
// ** getStatusMessage
// **************************************************************************
/**
* Returns the status message for a given status code. Source:
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
*/
protected static String getStatusMessage(int statusCode) {
switch (statusCode) {
case 100:
return "Continue";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Partial Information"; // Non-Authoritative Information
case 204:
return "No Content";
case 206:
return "Partial Content";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 304:
return "Not Modified";
case 307:
return "Temporary Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 500:
return "Internal Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
default:
return null;
}
}
// **************************************************************************
// ** sendError
// **************************************************************************
/**
* Sends an error response to the client using the specified status. The
* server defaults to creating the response to look like an HTML-formatted
* server error page containing the specified message, setting the content
* type to "text/html", leaving cookies and other headers unmodified.
*
*
* If the response has already been committed, this method throws an
* IllegalStateException. After using this method, the response should be
* considered to be committed and should not be written to.
*
* @param sc
* the error status code
* @param msg
* the descriptive message
* @exception IOException
* If an input or output exception occurs
* @exception IllegalStateException
* If the response was committed
*/
@Override
public void sendError(int sc, String msg) throws IOException {
setStatus(sc, msg);
write("
" + "" + sc + " - " + msg + " " + "" + "" + "" + sc + "
" + msg
+ "");
}
// **************************************************************************
// ** sendError
// **************************************************************************
/**
* Sends an error response to the client using the specified status code and
* clearing the buffer.
*
* If the response has already been committed, this method throws an
* IllegalStateException. After using this method, the response should be
* considered to be committed and should not be written to.
*
* @param sc
* the error status code
* @exception IOException
* If an input or output exception occurs
* @exception IllegalStateException
* If the response was committed before this method call
*/
@Override
public void sendError(int sc) throws IOException {
sendError(sc, getStatusMessage(sc));
}
// **************************************************************************
// ** sendRedirect
// **************************************************************************
/**
* Sends a temporary redirect response to the client using the specified
* redirect location URL.
*/
@Override
public void sendRedirect(String location) throws IOException {
sendRedirect(location, false);
}
// **************************************************************************
// ** sendRedirect
// **************************************************************************
/**
* Sends a temporary or permanent redirect response to the client using the
* specified redirect location URL.
*/
public void sendRedirect(String location, boolean movedPermanently) throws IOException {
if (movedPermanently)
setStatus(301);
else
setStatus(307);
setHeader("Location", location);
write("
" + "Document Moved " + "" + "" + "Object Moved
"
+ "This document may be found here" + "");
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write a block of text in the response body. You should only call
* this method once.
*
* @param compressOutput
* Specify whether to gzip compress the text. Note that this
* option will be applied only if "Accept-Encoding" supports gzip
* compression.
*/
public void write(String text, boolean compressOutput) throws IOException {
try {
write(text.getBytes(charSet), compressOutput);
} catch (java.io.UnsupportedEncodingException e) {
// this error should have been thrown earlier (setCharacterEncoding)
}
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write a block of text in the response body. Will automatically
* try to gzip compress the text if "Accept-Encoding" supports gzip
* compression. You should only call this method once.
*/
public void write(String text) throws IOException {
this.write(text, true);
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write bytes to the response body. You should only call this
* method once.
*
* @param bytes
* Input byte array
* @param compressOutput
* Specify whether to gzip compress the byte array. Note that
* this option will be applied only if "Accept-Encoding" supports
* gzip compression. Do not use this option if your bytes are
* already gzip compressed.
*/
public void write(byte[] bytes, boolean compressOutput) throws IOException {
if (bytes == null)
return;
// Check whether we can/should compress the output
boolean gzip = false;
if (compressOutput && bytes.length > 50) {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding != null) {
if (acceptEncoding.toLowerCase().contains("gzip")) {
gzip = true;
}
}
}
if (gzip) {
// If the input byte array is smaller than the bufferSize we can
// compress the entire array in a single step. Otherwise, we will
// compress incrementally and chuck out the output.
if (bytes.length <= bufferSize) {
// Compress the byte array
ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
GZIPOutputStream out = new GZIPOutputStream(bos, bytes.length);
out.write(bytes);
out.finish();
out.close();
bytes = bos.toByteArray();
bos.reset();
bos = null;
out = null;
// Set content length. This is extremely important for
// persistant
// connections (i.e. "Connection: Keep-Alive").
setContentLength(bytes.length);
// Ensure that the Content Encoding is correct and write the
// header
setHeader("Content-Encoding", "gzip");
writeHeader();
// Write the body
ByteBuffer output = ByteBuffer.allocateDirect(bytes.length);
output.put(bytes);
output.flip();
write(output, output.capacity());
output.clear();
output = null;
} else { // Chunk the output
// Write header before sending the file contents. Ensure that
// the output is chunked and the content encoding is correct.
setHeader("Transfer-Encoding", "chunked");
setHeader("Content-Encoding", "gzip");
writeHeader();
// Incrementally compress the byte array and chunk the output
GZIPOutputStream out = new GZIPOutputStream(new ConnectionOutputStream(), bufferSize);
java.io.InputStream inputStream = new ByteArrayInputStream(bytes);
byte[] b = new byte[bufferSize];
int x = 0;
while ((x = inputStream.read(b)) != -1) {
out.write(b, 0, x);
}
inputStream.close();
out.finish();
out.close();
out = null;
b = null;
}
} else { // no compression
// Write header before sending the file contents.
setContentLength(bytes.length);
writeHeader();
// Send the contents of the byte array to the client
java.io.InputStream inputStream = new ByteArrayInputStream(bytes);
ConnectionOutputStream out = new ConnectionOutputStream();
byte[] b = new byte[bufferSize];
int x = 0;
while ((x = inputStream.read(b)) != -1) {
out.write(b, 0, x);
}
inputStream.close();
inputStream = null;
out.close();
out = null;
b = null;
}
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write bytes to the response body. Will automatically try to
* compress the byte array if "Accept-Encoding" supports gzip compression.
* You should only call this method once.
*/
public void write(byte[] bytes) throws IOException {
this.write(bytes, true);
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write contents of a file into the response body. Automatically
* compresses the file content if the client supports gzip compression. You
* should only call this method once.
*/
public void write(java.io.File file, String contentType, boolean useCache) throws IOException {
String fileName = null;
if (!contentType.startsWith("image") && !contentType.startsWith("text")) {
fileName = file.getName();
}
write(file, fileName, contentType, useCache);
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to write contents of a file into the response body. Automatically
* compresses the file content if the client supports gzip compression. You
* should only call this method once.
*
* @param fileName
* Optional file name used in the "Content-Disposition" header.
* If the fileName is null, the "Content-Disposition" header will
* not be set by this method.
*/
public void write(java.io.File file, String fileName, String contentType, boolean useCache) throws IOException {
if (!file.exists() || file.isDirectory()) {
this.setStatus(404);
return;
}
long fileSize = file.length();
long fileDate = file.lastModified();
// Process Cache Directives
if (useCache) {
String eTag = "W/\"" + fileSize + "-" + fileDate + "\"";
setHeader("ETag", eTag);
setHeader("Last-Modified", getDate(fileDate)); // Sat, 23 Oct 2010
// 13:04:28 GMT
// this.setHeader("Cache-Control", "max-age=315360000");
// this.setHeader("Expires", "Sun, 30 Sep 2018 16:23:15 GMT ");
// Return 304/Not Modified response if we can...
String matchTag = request.getHeader("if-none-match");
String cacheControl = request.getHeader("cache-control");
if (matchTag == null)
matchTag = "";
if (cacheControl == null)
cacheControl = "";
if (cacheControl.equalsIgnoreCase("no-cache") == false) {
if (eTag.equalsIgnoreCase(matchTag)) {
// System.out.println("Sending 304 Response!");
this.setStatus(304);
return;
} else {
// Internet Explorer 6 uses "if-modified-since" instead of
// "if-none-match"
matchTag = request.getHeader("if-modified-since");
if (matchTag != null) {
for (String tag : matchTag.split(";")) {
if (tag.trim().equalsIgnoreCase(getDate(fileDate))) {
// System.out.println("Sending 304 Response!");
this.setStatus(304);
return;
}
}
}
}
}
} else {
setHeader("Cache-Control", "no-cache");
}
// Set Content Type and Disposition
setContentType(contentType);
if (fileName != null) {
setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\"");
}
// If the file is small enough, convert it to a byte array and call the
// other write method.
if (file.length() <= bufferSize) {
byte[] b = new byte[(int) file.length()];
java.io.FileInputStream is = new java.io.FileInputStream(file);
java.nio.channels.FileChannel inputStream = is.getChannel();
ByteBuffer buf = ByteBuffer.allocateDirect(b.length);
buf.rewind();
inputStream.read(buf);
buf.rewind();
buf.get(b);
inputStream.close();
is.close();
write(b);
return;
}
// Check whether to compress the response
boolean gzip = false;
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding != null) {
if (acceptEncoding.toLowerCase().contains("gzip")) {
gzip = true;
}
}
// Dump file contents to servlet output stream
if (gzip) {
// Write header before sending the file contents. Ensure that the
// output is chunked and the content encoding is correct.
setHeader("Transfer-Encoding", "chunked");
setHeader("Content-Encoding", "gzip");
writeHeader();
GZIPOutputStream out = new GZIPOutputStream(new ConnectionOutputStream(), bufferSize);
java.io.InputStream inputStream = new java.io.FileInputStream(file);
byte[] b = new byte[bufferSize];
int x = 0;
while ((x = inputStream.read(b)) != -1) {
out.write(b, 0, x);
}
inputStream.close();
out.finish();
out.close();
out = null;
b = null;
} else {
// Write header before sending the file contents.
setContentLength(fileSize);
writeHeader();
// Send the contents of a file to the client
java.io.FileInputStream is = new java.io.FileInputStream(file);
java.nio.channels.FileChannel inputStream = is.getChannel();
ByteBuffer buf = ByteBuffer.allocateDirect(bufferSize);
while (inputStream.read(buf) > -1) {
writeChunk(buf);
buf.clear();
buf.rewind();
}
inputStream.close();
is.close();
inputStream = null;
is = null;
}
}
// **************************************************************************
// ** write
// **************************************************************************
/**
* Used to transfer bytes from an input stream to the response body.
*
* @param inputStream
* java.io.InputStream
* @param compressOutput
* Specify whether to gzip compress the byte array. Note that
* this option will be applied only if "Accept-Encoding" supports
* gzip compression. Do not use this option if your bytes are
* already gzip compressed.
*/
public void write(java.io.InputStream inputStream, boolean compressOutput) throws IOException {
if (inputStream == null)
return;
// Check whether we can/should compress the output
boolean gzip = false;
if (compressOutput) {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding != null) {
if (acceptEncoding.toLowerCase().contains("gzip")) {
gzip = true;
}
}
}
// Write header before sending the file contents.
setHeader("Transfer-Encoding", "chunked");
if (gzip)
setHeader("Content-Encoding", "gzip");
writeHeader();
// Write body. Compress as needed.
java.io.OutputStream out;
if (gzip)
out = new GZIPOutputStream(new ConnectionOutputStream(), bufferSize);
else
out = new ConnectionOutputStream();
byte[] b = new byte[bufferSize];
int x = 0;
while ((x = inputStream.read(b)) != -1) {
out.write(b, 0, x);
}
inputStream.close();
// out.finish();
out.close();
out = null;
b = null;
}
// **************************************************************************
// ** getOutputStream
// **************************************************************************
/**
* Returns an output stream for writing the body of an http response.
* Automatically encrypts the data if the connection is SSL/TLS encrypted.
* Note that by default, if the request header includes a keep-alive
* directive, the response header will include a keep-alive response. As
* such, you must explicitely set the content length, set the "Connection"
* header to "Close", or chunk the output using chunked encoding. Example:
*
*
* // IMPORTANT: Set response headers before getting the output stream!
* response.setContentLength(54674);
*
* // Get output stream
* java.io.OutputStream outputStream = response.getOutputStream();
*
* // Transfer bytes from an input stream to the output stream
* byte[] b = new byte[1024];
* int x = 0;
* while ((x = inputStream.read(b)) != -1) {
* outputStream.write(b, 0, x);
* }
*
* // Close the input and output streams
* outputStream.close();
* inputStream.close();
*
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
writeHeader();
if (servletOutputStream == null)
servletOutputStream = new ServletOutputStream(new ConnectionOutputStream());
return servletOutputStream;
}
// **************************************************************************
// ** getWriter
// **************************************************************************
/**
* Returns a PrintWriter object that can send character text to the client.
*/
@Override
public java.io.PrintWriter getWriter() throws IOException {
return new java.io.PrintWriter(this.getOutputStream());
}
// **************************************************************************
// ** ConnectionOutputStream
// **************************************************************************
/**
* Simple implementation of an OutputStream used to write bytes directly to
* a non-blocking SocketChannel. This class was originally written to
* support incremental GZIP compression but is used in other places as well.
*/
protected class ConnectionOutputStream extends java.io.OutputStream {
private ByteBuffer buf = ByteBuffer.allocateDirect(bufferSize);
public ConnectionOutputStream() {
}
@Override
public void write(int b) throws IOException {
buf.put((byte) b);
buf.position(buf.position());
if (buf.position() == buf.capacity()) {
writeChunk(buf);
buf.clear();
buf.rewind();
}
}
@Override
public void flush() throws IOException {
close();
}
@Override
public void close() throws IOException {
if (buf.position() > 0) {
writeChunk(buf);
buf.clear();
buf.rewind();
}
}
}
// **************************************************************************
// ** writeChunk
// **************************************************************************
/**
* Writes a "chunk" of data to the client. Transparently wraps the bytes
* into "chunked" transfer format as needed.
*/
private void writeChunk(ByteBuffer buf) throws IOException {
// Late check to see if we should chunk the output
if (chunked == null) {
String TransferEncoding = getHeader("Transfer-Encoding");
chunked = (TransferEncoding != null ? TransferEncoding.equalsIgnoreCase("chunked") : false);
}
if (buf.position() == buf.capacity()) { // Send all the bytes to the
// client
if (chunked) {
// Create new byte buffer and insert chunk header
ByteBuffer sz = getBuffer(buf.position());
int len = sz.capacity() + 2 + buf.capacity() + 2;
ByteBuffer b = ByteBuffer.allocateDirect(len);
b.put(sz);
b.put(CRLF);
CRLF.rewind();
// Insert bytes
buf.rewind();
b.put(buf);
// Insert chunk delimitor
b.put(CRLF);
CRLF.rewind();
// Send buffer to client
b.rewind();
write(b, b.capacity());
b.clear();
} else {
buf.rewind();
write(buf, buf.capacity());
}
} else { // Buffer is only partially filled. This is our queue that
// we're
// done sending bytes to the client.
// Trim the byte buffer
byte[] arr = new byte[buf.position()];
buf.rewind();
buf.get(arr);
buf = ByteBuffer.allocateDirect(arr.length);
buf.put(arr);
arr = null;
if (chunked) {
// Create new byte buffer and insert chunk header
ByteBuffer sz = getBuffer(buf.position());
ByteBuffer z = getBuffer(0);
int len = sz.capacity() + 2 + buf.capacity() + 2 + z.capacity() + 4;
ByteBuffer b = ByteBuffer.allocateDirect(len);
b.put(sz);
sz.clear();
b.put(CRLF);
CRLF.rewind();
// Insert bytes
buf.rewind();
b.put(buf);
// Insert chunk delimitor and a zero-length last chunk: "0\r\n"
// and the final "\r\n".
b.put(CRLF);
CRLF.rewind();
b.put(z);
z.clear();
b.put(CRLF);
CRLF.rewind();
b.put(CRLF);
CRLF.rewind();
// Send buffer to client
b.rewind();
write(b, b.capacity());
b.clear();
} else {
buf.rewind();
write(buf, buf.capacity());
}
chunked = null;
}
}
/** Total bytes processed before writing the response body. */
private long ttl = 0;
// **************************************************************************
// ** write
// **************************************************************************
/**
* Of all the write methods, this is the only on that actually writes
* something to the SocketChannel.
*/
private void write(ByteBuffer buf, int length) throws IOException {
// Check whether we have satisfied the range request. Update the byte
// buffer as needed.
if (startRange != null && !writeHeader) {
if (getStatus() == 416)
return;
if ((ttl + length) < startRange) {
// Haven't gotten to the start range yet and the current buffer
// doesn't get us there either.
ttl += length;
return;
} else if (ttl < startRange) {
// Haven't gotten to the start range but the current buffer does
// contain bytes we need to satisfy the request. Resize the
// buffer
// accordingly.
int pos = (int) (startRange - ttl);
length = length - pos;
ttl += pos;
buf.position(pos);
byte[] arr = new byte[length];
buf.get(arr);
buf = ByteBuffer.allocateDirect(arr.length);
buf.put(arr);
buf.flip();
}
if (endRange != null) {
if (ttl > endRange) {
return; // we're past end range
} else {
long remainingBytes = (endRange + 1) - (ttl);
if (remainingBytes < length) {
length = (int) remainingBytes;
byte[] arr = new byte[length];
buf.rewind();
buf.get(arr);
buf = ByteBuffer.allocateDirect(length);
buf.put(arr);
buf.flip();
} else {
// System.err.println("Send bytes? Range (" + startRange
// + "-" + endRange + "). "
// + "Written " + (ttl) + "bytes. Need to send " +
// remainingBytes + " more");
}
}
} else { // no end range specified and content length is unknown
// System.err.println("No end range specified and content length
// is unknown? "
// + "Range (" + startRange + "-" + endRange + "). Written " +
// (ttl) + " bytes. Buffer is " + length);
}
}
if (request.isEncrypted()) {
buf = request.wrap(buf);
length = buf.capacity();
}
connection.write(buf, length);
}
// **************************************************************************
// ** getBuffer
// **************************************************************************
/**
* Converts an integer into a ByteBuffer.
*/
private ByteBuffer getBuffer(int i) throws IOException {
char[] chars = Integer.toHexString(i).toCharArray();
ByteBuffer buf = ByteBuffer.allocateDirect(chars.length);
for (char c : chars) {
buf.put((byte) c);
buf.position(buf.position());
}
buf.rewind();
chars = null;
return buf;
}
// **************************************************************************
// ** getCRLF
// **************************************************************************
/**
* Used to create and fill a ByteBuffer with a Carriage Return (CR) and Line
* Feed (LF).
*/
private static ByteBuffer getCRLF() {
ByteBuffer buf = ByteBuffer.allocateDirect(2);
buf.put("\r\n".getBytes());
buf.rewind();
return buf;
}
// **************************************************************************
// ** getChannel
// **************************************************************************
/**
* Returns the raw NIO SocketChannel associated with this response. Use with
* care.
*/
public java.nio.channels.SocketChannel getChannel() {
return connection.getChannel();
}
// **************************************************************************
// ** writeHeader
// **************************************************************************
/**
* Used to write the http header to the response.
*/
private void writeHeader() throws IOException {
if (writeHeader == false)
return;
else {
byte[] header = getHeader().getBytes(charSet);
ByteBuffer output = ByteBuffer.allocateDirect(header.length);
output.put(header);
output.flip();
write(output, output.capacity());
output.clear();
output = null;
writeHeader = false;
}
}
// **************************************************************************
// ** getHeader
// **************************************************************************
/**
* Returns the raw HTTP response header.
*/
public String getHeader() {
// Update the status code and message as needed
if (statusCode == null)
statusCode = 200;
if (statusMessage == null)
statusMessage = getStatusMessage(statusCode);
String TransferEncoding = getHeader("Transfer-Encoding");
boolean chunked = (TransferEncoding != null ? TransferEncoding.equalsIgnoreCase("chunked") : false);
// Update header for range requests
if (statusCode < 300 && startRange != null) {
setStatus(206);
if (chunked)
setHeader("Transfer-Encoding", null);
String contentRange = "bytes " + startRange + "-";
Long contentLength = getContentLength();
if (contentLength != null) {
if (endRange == null) {
endRange = contentLength - 1;
contentRange += endRange + "/" + contentLength;
setContentLength((endRange + 1) - startRange);
} else {
if (endRange >= contentLength) {
setStatus(416);
contentRange = "bytes */" + contentLength;
setContentLength(0);
} else {
contentRange += endRange + "/" + contentLength;
setContentLength((endRange + 1) - startRange);
}
}
} else {// Content Length is Unknown
if (endRange == null) {
contentRange += "/*";
} else {
contentRange += endRange + "/*";
setContentLength((endRange + 1) - startRange);
}
}
setHeader("Content-Range", contentRange);
} else {
startRange = endRange = null;
}
// The http response headers must include a value for "Content-Length"
// if using a persistant connection (i.e. "Connection: Keep-Alive") and
// chunkedTransfer is turned off.
String connType = getHeader("Connection");
boolean isKeepAlive = (connType != null ? connType.equalsIgnoreCase("Keep-Alive") : false);
if (isKeepAlive && getContentLength() == null && chunked == false) {
setHeader("Connection", "Close");
}
// If a new session has been created, add the session id to the response
HttpSession session = request.getSession(false);
if (session != null) {
if (session.isNew())
addCookie(new Cookie("JSESSIONID", session.getID()));
}
// Add status line
StringBuffer header = new StringBuffer();
header.append("HTTP/1.1 " + statusCode + (statusMessage == null ? "" : " " + statusMessage) + "\r\n");
// Add headers
java.util.Iterator it = headers.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
String val = headers.get(key);
if (val != null)
header.append(key + ": " + val + "\r\n");
}
// Add cookies
if (!cookies.isEmpty()) {
header.append("Set-Cookie: ");
java.util.Iterator cookie = cookies.iterator();
while (cookie.hasNext()) {
header.append(cookie.next().toString());
if (cookie.hasNext())
header.append(" ");
}
header.append("\r\n");
}
header.append("\r\n");
return header.toString();
}
// **************************************************************************
// ** toString
// **************************************************************************
/**
* Returns the raw HTTP response header.
*/
@Override
public String toString() {
return getHeader();
}
// **************************************************************************
// ** getDate
// **************************************************************************
/**
* Used to convert a date to a string (e.g. "Mon, 20 Feb 2012 07:22:20 EST"
* ). This method does not rely on the java.text.SimpleDateFormat for
* performance reasons.
*/
private static String getDate(Calendar cal) {
if (!cal.getTimeZone().equals(tz)) {
cal = (java.util.Calendar) cal.clone();
cal.setTimeZone(tz);
}
StringBuffer str = new StringBuffer(29);
switch (cal.get(Calendar.DAY_OF_WEEK)) {
case Calendar.MONDAY:
str.append("Mon, ");
break;
case Calendar.TUESDAY:
str.append("Tue, ");
break;
case Calendar.WEDNESDAY:
str.append("Wed, ");
break;
case Calendar.THURSDAY:
str.append("Thu, ");
break;
case Calendar.FRIDAY:
str.append("Fri, ");
break;
case Calendar.SATURDAY:
str.append("Sat, ");
break;
case Calendar.SUNDAY:
str.append("Sun, ");
break;
}
int i = cal.get(Calendar.DAY_OF_MONTH);
str.append(i < 10 ? "0" + i : i);
switch (cal.get(Calendar.MONTH)) {
case Calendar.JANUARY:
str.append(" Jan ");
break;
case Calendar.FEBRUARY:
str.append(" Feb ");
break;
case Calendar.MARCH:
str.append(" Mar ");
break;
case Calendar.APRIL:
str.append(" Apr ");
break;
case Calendar.MAY:
str.append(" May ");
break;
case Calendar.JUNE:
str.append(" Jun ");
break;
case Calendar.JULY:
str.append(" Jul ");
break;
case Calendar.AUGUST:
str.append(" Aug ");
break;
case Calendar.SEPTEMBER:
str.append(" Sep ");
break;
case Calendar.OCTOBER:
str.append(" Oct ");
break;
case Calendar.NOVEMBER:
str.append(" Nov ");
break;
case Calendar.DECEMBER:
str.append(" Dec ");
break;
}
str.append(cal.get(Calendar.YEAR));
str.append(" ");
i = cal.get(Calendar.HOUR_OF_DAY);
str.append(i < 10 ? "0" + i + ":" : i + ":");
i = cal.get(Calendar.MINUTE);
str.append(i < 10 ? "0" + i + ":" : i + ":");
i = cal.get(Calendar.SECOND);
str.append(i < 10 ? "0" + i + " " : i + " ");
str.append(z);
return str.toString();
// new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
// return f.format(date);
}
private String getDate(long milliseconds) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTimeInMillis(milliseconds);
return getDate(cal);
}
// **************************************************************************
// ** reset
// **************************************************************************
/**
* Clears the status code and headers. This method is called automatically
* after each new http request to free up server resources. You do not need
* to call this method explicitly from your application.
*/
@Override
public void reset() {
if (headers != null) {
headers.clear();
headers = null;
}
if (request != null) {
request.clear();
request = null;
}
}
// **************************************************************************
// ** flushBuffer
// **************************************************************************
/**
* Forces any content in the buffer to be written to the client. A call to
* this method automatically commits the response, meaning the status code
* and headers will be written. This method is called automatically after
* each http request to free up server resources. You do not need to call
* this method explicitly from your application.
*/
@Override
public void flushBuffer() {
if (writeHeader && headers != null) {
try {
setContentLength(0);
writeHeader();
} catch (IOException e) {
}
}
}
public void closeBuffer() {
if (servletOutputStream != null)
try {
servletOutputStream.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
servletOutputStream = null;
}
// **************************************************************************
// ** resetBuffer
// **************************************************************************
/**
* According to spec, this method is supposed to clear the content buffer.
* However, this implementation doesn't use a content buffer. All content is
* written immediately.
*/
@Override
public void resetBuffer() {
if (isCommitted())
throw new IllegalStateException();
}
// **************************************************************************
// ** isCommitted
// **************************************************************************
/**
* Returns a boolean indicating if the response has been committed. A
* committed response has already had its status code and headers written.
*/
@Override
public boolean isCommitted() {
return !writeHeader;
}
// **************************************************************************
// ** encodeURL
// **************************************************************************
/**
* According to spec, this method is supposed to encode the specified URL by
* including the session ID in it, or, if encoding is not needed, returns
* the URL unchanged. This is important for browsers that don't support
* cookies. In our case, sessions are maintained using cookies so we return
* the URL unchanged.
*/
@Override
public String encodeURL(String url) {
return url;
}
// **************************************************************************
// ** encodeRedirectURL
// **************************************************************************
/**
* According to spec, this method is supposed to encode the specified URL
* for use in the sendRedirect method or, if encoding is not needed, returns
* the URL unchanged. The implementation of this method includes logic to
* determine whether the session ID needs to be encoded in the URL. This is
* important for browsers that don't support cookies. In our case, sessions
* are maintained using cookies so we return the URL unchanged.
*/
@Override
public String encodeRedirectURL(String url) {
return url;
}
// **************************************************************************
// ** encodeUrl
// **************************************************************************
/**
* @deprecated Use encodeURL(String url) instead
*/
@Deprecated
@Override
public String encodeUrl(String url) {
return encodeURL(url);
}
// **************************************************************************
// ** encodeRedirectUrl
// **************************************************************************
/**
* @deprecated Use encodeRedirectURL(String url) instead
*/
@Deprecated
@Override
public String encodeRedirectUrl(String url) {
return url;
}
// **************************************************************************
// ** Server status codes; see RFC 2068.
// **************************************************************************
/** Status code (100) indicating the client can continue. */
public static final int SC_CONTINUE = 100;
/**
* Status code (101) indicating the server is switching protocols according
* to Upgrade header.
*/
public static final int SC_SWITCHING_PROTOCOLS = 101;
/** Status code (200) indicating the request succeeded normally. */
public static final int SC_OK = 200;
/**
* Status code (201) indicating the request succeeded and created a new
* resource on the server.
*/
public static final int SC_CREATED = 201;
/**
* Status code (202) indicating that a request was accepted for processing,
* but was not completed.
*/
public static final int SC_ACCEPTED = 202;
/**
* Status code (203) indicating that the meta information presented by the
* client did not originate from the server.
*/
public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
/**
* Status code (204) indicating that the request succeeded but that there
* was no new information to return.
*/
public static final int SC_NO_CONTENT = 204;
/**
* Status code (205) indicating that the agent SHOULD reset the
* document view which caused the request to be sent.
*/
public static final int SC_RESET_CONTENT = 205;
/**
* Status code (206) indicating that the server has fulfilled the partial
* GET request for the resource.
*/
public static final int SC_PARTIAL_CONTENT = 206;
/**
* Status code (300) indicating that the requested resource corresponds to
* any one of a set of representations, each with its own specific location.
*/
public static final int SC_MULTIPLE_CHOICES = 300;
/**
* Status code (301) indicating that the resource has permanently moved to a
* new location, and that future references should use a new URI with their
* requests.
*/
public static final int SC_MOVED_PERMANENTLY = 301;
/**
* Status code (302) indicating that the resource has temporarily moved to
* another location, but that future references should still use the
* original URI to access the resource. This definition is being retained
* for backwards compatibility. SC_FOUND is now the preferred definition.
*/
public static final int SC_MOVED_TEMPORARILY = 302;
/**
* Status code (302) indicating that the resource reside temporarily under a
* different URI. Since the redirection might be altered on occasion, the
* client should continue to use the Request-URI for future requests.
* (HTTP/1.1) To represent the status code (302), it is recommended to use
* this variable.
*/
public static final int SC_FOUND = 302;
/**
* Status code (303) indicating that the response to the request can be
* found under a different URI.
*/
public static final int SC_SEE_OTHER = 303;
/**
* Status code (304) indicating that a conditional GET operation found that
* the resource was available and not modified.
*/
public static final int SC_NOT_MODIFIED = 304;
/**
* Status code (305) indicating that the requested resource MUST be
* accessed through the proxy given by the Location field.
*/
public static final int SC_USE_PROXY = 305;
/**
* Status code (307) indicating that the requested resource resides
* temporarily under a different URI. The temporary URI SHOULD be
* given by the Location
field in the response.
*/
public static final int SC_TEMPORARY_REDIRECT = 307;
/**
* Status code (400) indicating the request sent by the client was
* syntactically incorrect.
*/
public static final int SC_BAD_REQUEST = 400;
/**
* Status code (401) indicating that the request requires HTTP
* authentication.
*/
public static final int SC_UNAUTHORIZED = 401;
/** Status code (402) reserved for future use. */
public static final int SC_PAYMENT_REQUIRED = 402;
/**
* Status code (403) indicating the server understood the request but
* refused to fulfill it.
*/
public static final int SC_FORBIDDEN = 403;
/**
* Status code (404) indicating that the requested resource is not
* available.
*/
public static final int SC_NOT_FOUND = 404;
/**
* Status code (405) indicating that the method specified in the
* Request-Line
is not allowed for the resource
* identified by the Request-URI
.
*/
public static final int SC_METHOD_NOT_ALLOWED = 405;
/**
* Status code (406) indicating that the resource identified by the request
* is only capable of generating response entities which have content
* characteristics not acceptable according to the accept headers sent in
* the request.
*/
public static final int SC_NOT_ACCEPTABLE = 406;
/**
* Status code (407) indicating that the client MUST first
* authenticate itself with the proxy.
*/
public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
/**
* Status code (408) indicating that the client did not produce a request
* within the time that the server was prepared to wait.
*/
public static final int SC_REQUEST_TIMEOUT = 408;
/**
* Status code (409) indicating that the request could not be completed due
* to a conflict with the current state of the resource.
*/
public static final int SC_CONFLICT = 409;
/**
* Status code (410) indicating that the resource is no longer available at
* the server and no forwarding address is known. This condition
* SHOULD be considered permanent.
*/
public static final int SC_GONE = 410;
/**
* Status code (411) indicating that the request cannot be handled without a
* defined Content-Length
.
*/
public static final int SC_LENGTH_REQUIRED = 411;
/**
* Status code (412) indicating that the precondition given in one or more
* of the request-header fields evaluated to false when it was tested on the
* server.
*/
public static final int SC_PRECONDITION_FAILED = 412;
/**
* Status code (413) indicating that the server is refusing to process the
* request because the request entity is larger than the server is willing
* or able to process.
*/
public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
/**
* Status code (414) indicating that the server is refusing to service the
* request because the Request-URI
is longer than the
* server is willing to interpret.
*/
public static final int SC_REQUEST_URI_TOO_LONG = 414;
/**
* Status code (415) indicating that the server is refusing to service the
* request because the entity of the request is in a format not supported by
* the requested resource for the requested method.
*/
public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
/**
* Status code (416) indicating that the server cannot serve the requested
* byte range.
*/
public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
/**
* Status code (417) indicating that the server could not meet the
* expectation given in the Expect request header.
*/
public static final int SC_EXPECTATION_FAILED = 417;
/**
* Status code (500) indicating an error inside the HTTP server which
* prevented it from fulfilling the request.
*/
public static final int SC_INTERNAL_SERVER_ERROR = 500;
/**
* Status code (501) indicating the HTTP server does not support the
* functionality needed to fulfill the request.
*/
public static final int SC_NOT_IMPLEMENTED = 501;
/**
* Status code (502) indicating that the HTTP server received an invalid
* response from a server it consulted when acting as a proxy or gateway.
*/
public static final int SC_BAD_GATEWAY = 502;
/**
* Status code (503) indicating that the HTTP server is temporarily
* overloaded, and unable to handle the request.
*/
public static final int SC_SERVICE_UNAVAILABLE = 503;
/**
* Status code (504) indicating that the server did not receive a timely
* response from the upstream server while acting as a gateway or proxy.
*/
public static final int SC_GATEWAY_TIMEOUT = 504;
/**
* Status code (505) indicating that the server does not support or refuses
* to support the HTTP protocol version that was used in the request
* message.
*/
public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
@Override
public void setContentLengthLong(long len) {
// TODO Auto-generated method stub
}
@Override
public void addCookie(javax.servlet.http.Cookie cookie) {
// TODO Auto-generated method stub
}
@Override
public Collection getHeaders(String name) {
// TODO Auto-generated method stub
return null;
}
@Override
public Collection getHeaderNames() {
// TODO Auto-generated method stub
return null;
}
}