View Javadoc
1   /* 
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   *
14   */
15  
16  package org.esigate.servlet.impl;
17  
18  import java.io.IOException;
19  import java.io.PrintWriter;
20  import java.io.Writer;
21  import java.util.Locale;
22  
23  import javax.servlet.ServletOutputStream;
24  import javax.servlet.http.Cookie;
25  import javax.servlet.http.HttpServletResponse;
26  import javax.servlet.http.HttpServletResponseWrapper;
27  
28  import org.apache.commons.io.output.ByteArrayOutputStream;
29  import org.apache.commons.io.output.StringBuilderWriter;
30  import org.apache.http.Header;
31  import org.apache.http.HttpHeaders;
32  import org.apache.http.HttpStatus;
33  import org.apache.http.HttpVersion;
34  import org.apache.http.client.methods.CloseableHttpResponse;
35  import org.apache.http.entity.ByteArrayEntity;
36  import org.apache.http.entity.ContentType;
37  import org.apache.http.entity.StringEntity;
38  import org.apache.http.message.BasicHttpResponse;
39  import org.apache.http.message.BasicStatusLine;
40  import org.esigate.Parameters;
41  import org.esigate.http.BasicCloseableHttpResponse;
42  import org.esigate.http.ContentTypeHelper;
43  import org.esigate.http.DateUtils;
44  import org.esigate.http.HttpResponseUtils;
45  import org.esigate.http.IncomingRequest;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * Wrapper to the HttpServletResponse that intercepts the content written in order to build an
51   * {@link org.apache.http.HttpResponse}.
52   * <ul>
53   * <li>If the content of the response is required for transformation (parseable content-type or proxy=false) or smaller
54   * than the buffer size the {@link org.apache.http.HttpResponse} will contain the entire response</li>
55   * <li>If the content of the response is not required for transformation, the {@link org.apache.http.HttpResponse} will
56   * contain only an abstract of the response truncated to the bufer size. The complete response will have already been
57   * written to the original {@link HttpServletResponse}</li>
58   * </ul>
59   * 
60   * @author Francois-Xavier Bonnet
61   * 
62   */
63  public class ResponseCapturingWrapper extends HttpServletResponseWrapper {
64      private static final Logger LOG = LoggerFactory.getLogger(ResponseCapturingWrapper.class);
65  
66      // OutputStream and Writer exposed
67      private ServletOutputStream outputStream;
68      private PrintWriter writer;
69  
70      // OutputStream and Writer of the wrapped HttpServletResponse
71      private ServletOutputStream responseOutputStream;
72      private PrintWriter responseWriter;
73  
74      // OutputStream and Writer buffers
75      private ByteArrayOutputStream internalOutputStream;
76      private StringBuilderWriter internalWriter;
77  
78      private HttpServletResponse response;
79  
80      private CloseableHttpResponse httpClientResponse = BasicCloseableHttpResponse.adapt(new BasicHttpResponse(
81              new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK")));
82  
83      private String contentType;
84      private String characterEncoding;
85      private int bufferSize;
86      private int bytesWritten = 0;
87      private boolean committed = false;
88      private ContentTypeHelper contentTypeHelper;
89      private final boolean proxy;
90      private boolean capture = true;
91      private final ResponseSender responseSender;
92      private final IncomingRequest incomingRequest;
93  
94      public ResponseCapturingWrapper(HttpServletResponse response, ContentTypeHelper contentTypeHelper, boolean proxy,
95              int bufferSize, ResponseSender responseSender, IncomingRequest incomingRequest) {
96          super(response);
97          this.response = response;
98          this.bufferSize = bufferSize;
99          this.contentTypeHelper = contentTypeHelper;
100         this.proxy = proxy;
101         this.responseSender = responseSender;
102         this.incomingRequest = incomingRequest;
103     }
104 
105     @Override
106     public void setStatus(int sc) {
107         httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, ""));
108     }
109 
110     @Override
111     public void setStatus(int sc, String sm) {
112         httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, sm));
113     }
114 
115     public int getStatus() {
116         return httpClientResponse.getStatusLine().getStatusCode();
117     }
118 
119     @Override
120     public void sendError(int sc, String msg) {
121         httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, msg));
122     }
123 
124     @Override
125     public void sendError(int sc) {
126         httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, sc, ""));
127     }
128 
129     @Override
130     public void sendRedirect(String location) {
131         httpClientResponse.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_MOVED_TEMPORARILY,
132                 "Temporary redirect"));
133         httpClientResponse.setHeader(HttpHeaders.LOCATION, location);
134     }
135 
136     @Override
137     public boolean containsHeader(String name) {
138         return httpClientResponse.containsHeader(name);
139     }
140 
141     @Override
142     public void setHeader(String name, String value) {
143         if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
144             setContentType(value);
145         } else {
146             httpClientResponse.setHeader(name, value);
147         }
148     }
149 
150     @Override
151     public void addHeader(String name, String value) {
152         if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
153             setContentType(value);
154         } else {
155             httpClientResponse.addHeader(name, value);
156         }
157     }
158 
159     @Override
160     public void setDateHeader(String name, long date) {
161         setHeader(name, DateUtils.formatDate(date));
162     }
163 
164     @Override
165     public void addDateHeader(String name, long date) {
166         addHeader(name, DateUtils.formatDate(date));
167     }
168 
169     @Override
170     public void setIntHeader(String name, int value) {
171         setHeader(name, Integer.toString(value));
172     }
173 
174     @Override
175     public void addIntHeader(String name, int value) {
176         addHeader(name, Integer.toString(value));
177     }
178 
179     @Override
180     public void setContentLength(int len) {
181         setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(len));
182     }
183 
184     @Override
185     public void setCharacterEncoding(String charset) {
186         this.characterEncoding = charset;
187         updateContentTypeHeader();
188     }
189 
190     @Override
191     public String getCharacterEncoding() {
192         return this.characterEncoding;
193     }
194 
195     @Override
196     public String getContentType() {
197         Header contentTypeHeader = httpClientResponse.getFirstHeader(HttpHeaders.CONTENT_TYPE);
198         if (contentTypeHeader != null) {
199             return contentTypeHeader.getValue();
200         } else {
201             return null;
202         }
203     }
204 
205     @Override
206     public void setContentType(String type) {
207         ContentType parsedContentType = ContentType.parse(type);
208         this.contentType = parsedContentType.getMimeType();
209         if (parsedContentType.getCharset() != null) {
210             this.characterEncoding = parsedContentType.getCharset().name();
211         }
212         updateContentTypeHeader();
213     }
214 
215     private void updateContentTypeHeader() {
216         if (contentType != null) {
217             if (characterEncoding == null) {
218                 httpClientResponse.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
219             } else {
220                 httpClientResponse.setHeader(HttpHeaders.CONTENT_TYPE, contentType + ";charset=" + characterEncoding);
221             }
222         }
223     }
224 
225     @Override
226     public void setLocale(Locale loc) {
227         response.setLocale(loc);
228         if (characterEncoding == null) {
229             characterEncoding = response.getCharacterEncoding();
230             updateContentTypeHeader();
231         }
232     }
233 
234     @Override
235     public Locale getLocale() {
236         return response.getLocale();
237     }
238 
239     @Override
240     public void setBufferSize(int size) {
241         this.bufferSize = size;
242     }
243 
244     @Override
245     public int getBufferSize() {
246         return bufferSize;
247     }
248 
249     @Override
250     public void addCookie(Cookie cookie) {
251         response.addCookie(cookie);
252     }
253 
254     @Override
255     public String encodeURL(String url) {
256         return response.encodeURL(url);
257     }
258 
259     @Override
260     public String encodeRedirectURL(String url) {
261         return response.encodeRedirectURL(url);
262     }
263 
264     @SuppressWarnings("deprecation")
265     @Override
266     public String encodeUrl(String url) {
267         return response.encodeUrl(url);
268     }
269 
270     @SuppressWarnings("deprecation")
271     @Override
272     public String encodeRedirectUrl(String url) {
273         return response.encodeRedirectUrl(url);
274     }
275 
276     @Override
277     public void reset() {
278         if (isCommitted()) {
279             throw new IllegalStateException("Response is already committed");
280         }
281         httpClientResponse =
282                 BasicCloseableHttpResponse.adapt(new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
283                         HttpStatus.SC_OK, "OK")));
284     }
285 
286     @Override
287     public ServletOutputStream getOutputStream() {
288         LOG.debug("getOutputStream");
289         if (writer != null) {
290             throw new IllegalStateException("Writer already obtained");
291         }
292         if (outputStream == null) {
293             internalOutputStream = new ByteArrayOutputStream(Parameters.DEFAULT_BUFFER_SIZE);
294             outputStream = new ServletOutputStream() {
295 
296                 @Override
297                 public void write(int b) throws IOException {
298                     if (capture || bytesWritten < bufferSize) {
299                         internalOutputStream.write(b);
300                     } else {
301                         responseOutputStream.write(b);
302                     }
303                     bytesWritten++;
304                     if (bytesWritten == bufferSize) {
305                         commit();
306                     }
307                 }
308 
309                 @Override
310                 public void flush() throws IOException {
311                     commit();
312                 }
313 
314                 @Override
315                 public void close() throws IOException {
316                     commit();
317                 }
318 
319                 private void commit() throws IOException {
320                     if (!committed) {
321                         capture = hasToCaptureOutput();
322                         if (!capture) {
323                             responseSender.sendHeaders(httpClientResponse, incomingRequest, response);
324                             responseOutputStream = response.getOutputStream();
325                             internalOutputStream.writeTo(responseOutputStream);
326                         }
327                         committed = true;
328                     }
329 
330                 }
331 
332             };
333         }
334         return outputStream;
335     }
336 
337     @Override
338     public PrintWriter getWriter() {
339         LOG.debug("getWriter");
340         if (outputStream != null) {
341             throw new IllegalStateException("OutputStream already obtained");
342         }
343         if (writer == null) {
344             internalWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
345             writer = new PrintWriter(new Writer() {
346 
347                 @Override
348                 public void write(char[] cbuf, int off, int len) throws IOException {
349                     if (capture || bytesWritten < bufferSize) {
350                         internalWriter.write(cbuf, off, len);
351                     } else {
352                         responseWriter.write(cbuf, off, len);
353                     }
354                     bytesWritten++;
355                     if (bytesWritten == bufferSize) {
356                         commit();
357                     }
358                 }
359 
360                 @Override
361                 public void flush() throws IOException {
362                     commit();
363                 }
364 
365                 @Override
366                 public void close() throws IOException {
367                     commit();
368                 }
369 
370                 private void commit() throws IOException {
371                     if (!committed) {
372                         capture = hasToCaptureOutput();
373                         if (!capture) {
374                             responseSender.sendHeaders(httpClientResponse, incomingRequest, response);
375                             responseWriter = response.getWriter();
376                             responseWriter.write(internalWriter.toString());
377                         }
378                         committed = true;
379                     }
380 
381                 }
382 
383             });
384         }
385         return writer;
386     }
387 
388     @Override
389     public void flushBuffer() throws IOException {
390         if (outputStream != null) {
391             outputStream.flush();
392         }
393         if (writer != null) {
394             writer.flush();
395         }
396     }
397 
398     @Override
399     public void resetBuffer() {
400         if (isCommitted()) {
401             throw new IllegalStateException("Response is already committed");
402         }
403         if (internalOutputStream != null) {
404             internalOutputStream.reset();
405         }
406         if (internalWriter != null) {
407             internalWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
408         }
409         bytesWritten = 0;
410     }
411 
412     @Override
413     public boolean isCommitted() {
414         return committed;
415     }
416 
417     /**
418      * We have to capture the output of the response in 2 cases :
419      * <ol>
420      * <li>the content type is text and may have to be transformed</li>
421      * <li>we are inside an include, in this case the content type must be considered as text</li>
422      * </ol>
423      * 
424      * @return true if we have to capture the output of the response
425      */
426     private boolean hasToCaptureOutput() {
427         return !proxy || contentTypeHelper.isTextContentType(httpClientResponse)
428                 || HttpResponseUtils.getFirstHeader(HttpHeaders.CONTENT_TYPE, httpClientResponse) == null;
429     }
430 
431     /**
432      * Returns the response. If the response has not been captured and has been written directly to the
433      * {@link HttpServletResponse}, calling this method closes the HttpServletResponse writer OutputStream
434      * 
435      * @return the response
436      */
437     public CloseableHttpResponse getCloseableHttpResponse() {
438         ContentType resultContentType = null;
439         if (this.contentType != null) {
440             resultContentType = ContentType.create(this.contentType, characterEncoding);
441         }
442         if (internalWriter != null) {
443             writer.flush();
444             httpClientResponse.setEntity(new StringEntity(internalWriter.toString(), resultContentType));
445         } else if (internalOutputStream != null) {
446             try {
447                 outputStream.flush();
448             } catch (IOException e) {
449                 // Nothing to do;
450             }
451             httpClientResponse.setEntity(new ByteArrayEntity(internalOutputStream.toByteArray(), resultContentType));
452         }
453         if (!capture) {
454             // The result has already been written to the response, let's close
455             // the response stream
456             if (responseWriter != null) {
457                 responseWriter.close();
458             }
459             if (responseOutputStream != null) {
460                 try {
461                     responseOutputStream.close();
462                 } catch (IOException e) {
463                     LOG.warn("Could not close servlet output stream: " + e.getMessage());
464                 }
465             }
466         }
467         return httpClientResponse;
468     }
469 
470 }