In this post
we will draw the main lines of creating a buffered HTTP Servlet response. Is
far for our aim to present the "bowels" of Servlets API, but we can
sketch a simple flow of an HTTP response and we will see how to obtain a
buffered version of it.
Basically, a
Servlet receive a request from the client and provide an answer, well-known as response. So, we can draw the base line
in the javax.servlet.ServletResponse
interface. The Servlet container creates this object especially to assist us
during sending a response to the client. The response can be plain text (character
text) or binary. The response-text is managed through a java.io.PrintWriter
object, which extends the functionality of a java.io.Writer for printing
formatted representations of objects to a text-output stream (obtained via ServletResponse#getWriter()
method) and the response-binary data is managed through a javax.servlet.ServletOutputStream
object, which extends the functionality of java.io.OutputStream to provide
an output stream for sending binary data to the client (obtained via ServletResponse#getOutputStream()
method).
The ServletResponse
interface is extended with HTTP-specific functionality in sending a response by
the javax.servlet.http.HttpServletResponse
interface (notice the presence of 'http' word in each class name that is
related to HTTP specific functionalities). Most probably, you are familiar with
this interface from the doGet() and doPost() methods of a
Servlet - this is the second argument in these methods, next to the HttpServletRequest
object. We know that the Servlet container will provide them out of the box,
and we just call the getWriter() or getOutputStream() methods to obtain an
object capable to write a response to the client. Most probably, you saw many
times the below snippet:
...
@Override
protected
void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
try (PrintWriter out = response.getWriter()) {
//...
out.println(some_character_text);
}
try (ServletOutputStream out =
response.getOutputStream()) {
//...
out.write(some_bytes);
}
}
...
Based on
this snippet, we know that some_character_text
and some_bytes will be sent to the
client.
A convenient
implementation of the HttpServletResponse interface is named, javax.servlet.http.HttpServletResponseWrapper.
This class is mainly important for developers who want to provide a custom
implementation (behavior) to the response from a Servlet. When this class is
extended it provides at least two major advantages: first, you can focus on
overriding only the necessary methods, and second, via HttpServletResponseWrapper
constructor, we can obtain a response adaptor wrapping the given response:
public
HttpServletResponseWrapper(HttpServletResponse response)
As you will
see later, OmniFaces extends this class in order to provide a buffered
response. But, before that, OmniFaces focused on java.io.Writer and java.io.OutputStream.
First, it extends the java.io.Writer to obtain "a resettable buffered writer capable to buffer everything until the
given buffer size, regardless of flush calls. Only when the buffer size is
exceeded, or when close is called, then the buffer will be actually flushed"
- source: OmniFaces documentation. You can see the implementation below and
focus on the highlighted part (originating here):
public class
ResettableBufferedWriter extends Writer implements ResettableBuffer {
private Writer writer;
private Charset charset;
private CharArrayWriter buffer;
private int bufferSize;
private int writtenBytes;
public ResettableBufferedWriter(Writer writer,
int bufferSize, String characterEncoding) {
this.writer
= writer;
this.bufferSize
= bufferSize;
this.charset
= Charset.forName(characterEncoding);
this.buffer
= new CharArrayWriter(bufferSize);
}
@Override
public void write(char[] chars, int offset,
int length) throws IOException {
if (buffer != null) {
if ((writtenBytes +=
charset.encode(CharBuffer.wrap(chars, offset, length)).limit()) > bufferSize)
{
writer.write(buffer.toCharArray());
writer.write(chars, offset, length);
buffer = null;
}
else {
buffer.write(chars, offset, length);
}
} else {
writer.write(chars, offset, length);
}
}
@Override
public void reset() {
buffer = new CharArrayWriter(bufferSize);
writtenBytes = 0;
}
@Override
public void flush() throws IOException {
if (buffer == null) {
writer.flush();
}
}
@Override
public void close() throws IOException {
if (buffer != null) {
writer.write(buffer.toCharArray());
buffer = null;
}
writer.close();
}
@Override
public boolean isResettable() {
return buffer != null;
}
}
In order to
cover the binary part, OmniFaces follows the same principle as above and extends
the java.io.OutputStream
and creates " a resettable buffered
output stream will buffer everything until the given buffer size, regardless of
flush calls. Only when the buffer size is exceeded, or when close is called,
then the buffer will be actually flushed." - source: OmniFaces
documentation. You can see the implementation below and focus on the highlighted
part (originating here):
public class
ResettableBufferedOutputStream extends OutputStream implements ResettableBuffer
{
private OutputStream output;
private ByteArrayOutputStream buffer;
private int bufferSize;
private int writtenBytes;
public
ResettableBufferedOutputStream(OutputStream output, int bufferSize) {
this.output = output;
this.bufferSize = bufferSize;
this.buffer = new
ByteArrayOutputStream(bufferSize);
}
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte) b }, 0, 1);
}
@Override
public void write(byte[] bytes) throws
IOException {
write(bytes, 0, bytes.length);
}
@Override
public void write(byte[] bytes, int offset,
int length) throws IOException {
if (buffer != null) {
if ((writtenBytes += (length - offset))
> bufferSize) {
output.write(buffer.toByteArray());
output.write(bytes, offset, length);
buffer
= null;
} else {
buffer.write(bytes, offset, length);
}
} else {
output.write(bytes, offset, length);
}
}
@Override
public void reset() {
buffer = new ByteArrayOutputStream(bufferSize);
writtenBytes = 0;
}
@Override
public void flush() throws IOException {
if (buffer == null) {
output.flush();
}
}
@Override
public void close() throws IOException {
if (buffer != null) {
output.write(buffer.toByteArray());
buffer = null;
}
output.close();
}
@Override
public boolean isResettable() {
return buffer != null;
}
}
The ResettableBuffer
interface is the base interface for a resettable buffer (nothing fancy here):
public
interface ResettableBuffer {
void reset();
boolean isResettable();
}
So, as a
quick resume, we have a resettable-text-buffer
and a resettable-binary-buffer ready
for use. Further, OmniFaces uses the ResettableBufferedWriter and ResettableBufferedOutputStream in the
extension of the HttpServletResponseWrapper, named HttpServletResponseOutputWrapper
(abstract implementation). Is a good moment to point the fact that OmniFaces
allows us to choose between the default PrintWriter/ServletOutputStream
and the custom ones (new PrintWriter/ServletOutputStream for using
the above buffers) via a simple flag named, passThrough (if passThrough
is true
(default false), then use defaults). You can see the implementation below and
focus on the highlighted part (originating here):
public
abstract class HttpServletResponseOutputWrapper extends
HttpServletResponseWrapper {
private static final String
ERROR_GETOUTPUT_ALREADY_CALLED =
"getOutputStream() has already been
called on this response.";
private static final String
ERROR_GETWRITER_ALREADY_CALLED =
"getWriter() has already been called
on this response.";
private ServletOutputStream output;
private PrintWriter writer;
private ResettableBuffer buffer;
private boolean passThrough;
public
HttpServletResponseOutputWrapper(HttpServletResponse wrappedResponse) {
super(wrappedResponse);
}
protected abstract OutputStream
createOutputStream();
@Override
public ServletOutputStream getOutputStream()
throws IOException {
if (passThrough) {
return super.getOutputStream();
}
if (writer != null) {
throw new
IllegalStateException(ERROR_GETWRITER_ALREADY_CALLED);
}
if (output == null) {
buffer = new
ResettableBufferedOutputStream(createOutputStream(), getBufferSize());
output = new
ServletOutputStream() {
@Override
public void write(int b) throws IOException {
(OutputStream) buffer).write(b);
}
@Override
public void write(byte[] bytes) throws IOException {
((OutputStream) buffer).write(bytes);
}
@Override
public void write(byte[] bytes, int offset, int length) throws IOException {
((OutputStream) buffer).write(bytes, offset, length);
}
@Override
public void flush() throws IOException {
((OutputStream) buffer).flush();
}
@Override
public void close() throws IOException {
((OutputStream) buffer).close();
}
};
public void write(int b) throws IOException {
(OutputStream) buffer).write(b);
}
@Override
public void write(byte[] bytes) throws IOException {
((OutputStream) buffer).write(bytes);
}
@Override
public void write(byte[] bytes, int offset, int length) throws IOException {
((OutputStream) buffer).write(bytes, offset, length);
}
@Override
public void flush() throws IOException {
((OutputStream) buffer).flush();
}
@Override
public void close() throws IOException {
((OutputStream) buffer).close();
}
};
}
return output;
}
@Override
public PrintWriter getWriter() throws
IOException {
if (passThrough) {
return super.getWriter();
}
if (output != null) {
throw new
IllegalStateException(ERROR_GETOUTPUT_ALREADY_CALLED);
}
if (writer == null) {
buffer = new ResettableBufferedWriter(new
OutputStreamWriter(createOutputStream(),getCharacterEncoding()),
getBufferSize(), getCharacterEncoding());
writer = new PrintWriter((Writer) buffer);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
super.flushBuffer();
if (passThrough) {
return;
}
if (writer != null) {
writer.flush();
} else if (output != null) {
output.flush();
}
}
public void close() throws IOException {
if
(writer != null) {
writer.close();
} else
if (output != null) {
output.close();
}
}
@Override
public void reset() {
super.reset();
if (buffer != null) {
buffer.reset();
}
}
@Override
public boolean isCommitted() {
return super.isCommitted() || (buffer != null
&& !buffer.isResettable());
}
public boolean isPassThrough() {
return passThrough;
}
public void setPassThrough(boolean
passThrough) {
this.passThrough = passThrough;
}
}
One more
thing is relevant here, the createOutputStream() abstract method. When
a developer need to extend the HttpServletResponseOutputWrapper, it can simply
override the createOutputStream()
method. The indicated output stream will be used in both, getOutputStream()
and getWriter().
Now,
OmniFaces provides an out of the box implementation of an HTTP servlet response
that buffers the entire response body. The buffered response body is available
as a byte array via a method named, getBuffer() or as a string via a method
named, getBufferAsString().You can see the implementation below and focus on the highlighted part (originating here):
public class
BufferedHttpServletResponse extends HttpServletResponseOutputWrapper {
private final ByteArrayOutputStream buffer;
public
BufferedHttpServletResponse(HttpServletResponse response) {
super(response);
buffer = new
ByteArrayOutputStream(response.getBufferSize());
}
@Override
protected OutputStream createOutputStream() {
return buffer;
}
public byte[] getBuffer() throws IOException {
close();
return buffer.toByteArray();
}
public String getBufferAsString() throws
IOException {
return new String(getBuffer(), getCharacterEncoding());
}
}
Finally, we have
reached the top of the iceberg. Now, you can try your own implementation by
extending the HttpServletResponseOutputWrapper,
or use this implementation. OmniFaces use this implementation in several
artifacts, per example, you may want to check the source code for the ResourceInclude
component. Here it is a small fragment of how it is used:
...
FacesContext
context;
ExternalContext
externalContext = context.getExternalContext();
HttpServletRequest
request = (HttpServletRequest) externalContext.getRequest();
HttpServletResponse
response = (HttpServletResponse) externalContext.getResponse();
BufferedHttpServletResponse
bufferedResponse = new BufferedHttpServletResponse(response);
request.getRequestDispatcher((String)
getAttributes().get("path")).include(request, bufferedResponse);
...
Of course,
you can used it for many other scenarios. Another example can be seen in "Use OmniFaces to Buffer FacesServlet Output".
Niciun comentariu :
Trimiteți un comentariu