ResponseCapturingWrapper.java
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.esigate.servlet.impl;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Locale;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.esigate.Parameters;
import org.esigate.http.BasicCloseableHttpResponse;
import org.esigate.http.ContentTypeHelper;
import org.esigate.http.DateUtils;
import org.esigate.http.HttpResponseUtils;
import org.esigate.http.IncomingRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wrapper to the HttpServletResponse that intercepts the content written in order to build an
* {@link org.apache.http.HttpResponse}.
* <ul>
* <li>If the content of the response is required for transformation (parseable content-type or proxy=false) or smaller
* than the buffer size the {@link org.apache.http.HttpResponse} will contain the entire response</li>
* <li>If the content of the response is not required for transformation, the {@link org.apache.http.HttpResponse} will
* contain only an abstract of the response truncated to the bufer size. The complete response will have already been
* written to the original {@link HttpServletResponse}</li>
* </ul>
*
* @author Francois-Xavier Bonnet
*
*/
public class ResponseCapturingWrapper extends HttpServletResponseWrapper {
private static final Logger LOG = LoggerFactory.getLogger(ResponseCapturingWrapper.class);
// OutputStream and Writer exposed
private ServletOutputStream outputStream;
private PrintWriter writer;
// OutputStream and Writer of the wrapped HttpServletResponse
private ServletOutputStream responseOutputStream;
private PrintWriter responseWriter;
// OutputStream and Writer buffers
private ByteArrayOutputStream internalOutputStream;
private StringBuilderWriter internalWriter;
private HttpServletResponse response;
private CloseableHttpResponse httpClientResponse = BasicCloseableHttpResponse.adapt(new BasicHttpResponse(
new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK")));
private String contentType;
private String characterEncoding;
private int bufferSize;
private int bytesWritten = 0;
private boolean committed = false;
private ContentTypeHelper contentTypeHelper;
private final boolean proxy;
private boolean capture = true;
private final ResponseSender responseSender;
private final IncomingRequest incomingRequest;
public ResponseCapturingWrapper(HttpServletResponse response, ContentTypeHelper contentTypeHelper, boolean proxy,
int bufferSize, ResponseSender responseSender, IncomingRequest incomingRequest) {
super(response);
this.response = response;
this.bufferSize = bufferSize;
this.contentTypeHelper = contentTypeHelper;
this.proxy = proxy;
this.responseSender = responseSender;
this.incomingRequest = incomingRequest;
}
@Override
public void setStatus(int sc) {
httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, ""));
}
@Override
public void setStatus(int sc, String sm) {
httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, sm));
}
public int getStatus() {
return httpClientResponse.getStatusLine().getStatusCode();
}
@Override
public void sendError(int sc, String msg) {
httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, msg));
}
@Override
public void sendError(int sc) {
httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, ""));
}
@Override
public void sendRedirect(String location) {
httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_MOVED_TEMPORARILY,
"Temporary redirect"));
httpClientResponse.setHeader(HttpHeaders.LOCATION, location);
}
@Override
public boolean containsHeader(String name) {
return httpClientResponse.containsHeader(name);
}
@Override
public void setHeader(String name, String value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value);
} else {
httpClientResponse.setHeader(name, value);
}
}
@Override
public void addHeader(String name, String value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value);
} else {
httpClientResponse.addHeader(name, value);
}
}
@Override
public void setDateHeader(String name, long date) {
setHeader(name, DateUtils.formatDate(date));
}
@Override
public void addDateHeader(String name, long date) {
addHeader(name, DateUtils.formatDate(date));
}
@Override
public void setIntHeader(String name, int value) {
setHeader(name, Integer.toString(value));
}
@Override
public void addIntHeader(String name, int value) {
addHeader(name, Integer.toString(value));
}
@Override
public void setContentLength(int len) {
setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(len));
}
@Override
public void setCharacterEncoding(String charset) {
this.characterEncoding = charset;
updateContentTypeHeader();
}
@Override
public String getCharacterEncoding() {
return this.characterEncoding;
}
@Override
public String getContentType() {
Header contentTypeHeader = httpClientResponse.getFirstHeader(HttpHeaders.CONTENT_TYPE);
if (contentTypeHeader != null) {
return contentTypeHeader.getValue();
} else {
return null;
}
}
@Override
public void setContentType(String type) {
ContentType parsedContentType = ContentType.parse(type);
this.contentType = parsedContentType.getMimeType();
if (parsedContentType.getCharset() != null) {
this.characterEncoding = parsedContentType.getCharset().name();
}
updateContentTypeHeader();
}
private void updateContentTypeHeader() {
if (contentType != null) {
if (characterEncoding == null) {
httpClientResponse.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
} else {
httpClientResponse.setHeader(HttpHeaders.CONTENT_TYPE, contentType + ";charset=" + characterEncoding);
}
}
}
@Override
public void setLocale(Locale loc) {
response.setLocale(loc);
if (characterEncoding == null) {
characterEncoding = response.getCharacterEncoding();
updateContentTypeHeader();
}
}
@Override
public Locale getLocale() {
return response.getLocale();
}
@Override
public void setBufferSize(int size) {
this.bufferSize = size;
}
@Override
public int getBufferSize() {
return bufferSize;
}
@Override
public void addCookie(Cookie cookie) {
response.addCookie(cookie);
}
@Override
public String encodeURL(String url) {
return response.encodeURL(url);
}
@Override
public String encodeRedirectURL(String url) {
return response.encodeRedirectURL(url);
}
@SuppressWarnings("deprecation")
@Override
public String encodeUrl(String url) {
return response.encodeUrl(url);
}
@SuppressWarnings("deprecation")
@Override
public String encodeRedirectUrl(String url) {
return response.encodeRedirectUrl(url);
}
@Override
public void reset() {
if (isCommitted()) {
throw new IllegalStateException("Response is already committed");
}
httpClientResponse =
BasicCloseableHttpResponse.adapt(new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK")));
}
@Override
public ServletOutputStream getOutputStream() {
LOG.debug("getOutputStream");
if (writer != null) {
throw new IllegalStateException("Writer already obtained");
}
if (outputStream == null) {
internalOutputStream = new ByteArrayOutputStream(Parameters.DEFAULT_BUFFER_SIZE);
outputStream = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
if (capture || bytesWritten < bufferSize) {
internalOutputStream.write(b);
} else {
responseOutputStream.write(b);
}
bytesWritten++;
if (bytesWritten == bufferSize) {
commit();
}
}
@Override
public void flush() throws IOException {
commit();
}
@Override
public void close() throws IOException {
commit();
}
private void commit() throws IOException {
if (!committed) {
capture = hasToCaptureOutput();
if (!capture) {
responseSender.sendHeaders(httpClientResponse, incomingRequest, response);
responseOutputStream = response.getOutputStream();
internalOutputStream.writeTo(responseOutputStream);
}
committed = true;
}
}
};
}
return outputStream;
}
@Override
public PrintWriter getWriter() {
LOG.debug("getWriter");
if (outputStream != null) {
throw new IllegalStateException("OutputStream already obtained");
}
if (writer == null) {
internalWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
writer = new PrintWriter(new Writer() {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
if (capture || bytesWritten < bufferSize) {
internalWriter.write(cbuf, off, len);
} else {
responseWriter.write(cbuf, off, len);
}
bytesWritten++;
if (bytesWritten == bufferSize) {
commit();
}
}
@Override
public void flush() throws IOException {
commit();
}
@Override
public void close() throws IOException {
commit();
}
private void commit() throws IOException {
if (!committed) {
capture = hasToCaptureOutput();
if (!capture) {
responseSender.sendHeaders(httpClientResponse, incomingRequest, response);
responseWriter = response.getWriter();
responseWriter.write(internalWriter.toString());
}
committed = true;
}
}
});
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (outputStream != null) {
outputStream.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void resetBuffer() {
if (isCommitted()) {
throw new IllegalStateException("Response is already committed");
}
if (internalOutputStream != null) {
internalOutputStream.reset();
}
if (internalWriter != null) {
internalWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
}
bytesWritten = 0;
}
@Override
public boolean isCommitted() {
return committed;
}
/**
* We have to capture the output of the response in 2 cases :
* <ol>
* <li>the content type is text and may have to be transformed</li>
* <li>we are inside an include, in this case the content type must be considered as text</li>
* </ol>
*
* @return true if we have to capture the output of the response
*/
private boolean hasToCaptureOutput() {
return !proxy || contentTypeHelper.isTextContentType(httpClientResponse)
|| HttpResponseUtils.getFirstHeader(HttpHeaders.CONTENT_TYPE, httpClientResponse) == null;
}
/**
* Returns the response. If the response has not been captured and has been written directly to the
* {@link HttpServletResponse}, calling this method closes the HttpServletResponse writer OutputStream
*
* @return the response
*/
public CloseableHttpResponse getCloseableHttpResponse() {
ContentType resultContentType = null;
if (this.contentType != null) {
resultContentType = ContentType.create(this.contentType, characterEncoding);
}
if (internalWriter != null) {
writer.flush();
httpClientResponse.setEntity(new StringEntity(internalWriter.toString(), resultContentType));
} else if (internalOutputStream != null) {
try {
outputStream.flush();
} catch (IOException e) {
// Nothing to do;
}
httpClientResponse.setEntity(new ByteArrayEntity(internalOutputStream.toByteArray(), resultContentType));
}
if (!capture) {
// The result has already been written to the response, let's close
// the response stream
if (responseWriter != null) {
responseWriter.close();
}
if (responseOutputStream != null) {
try {
responseOutputStream.close();
} catch (IOException e) {
LOG.warn("Could not close servlet output stream: " + e.getMessage());
}
}
}
return httpClientResponse;
}
}