/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.server.http; import java.io.IOException; import java.io.OutputStream; import java.io.Writer; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletResponse; import com.caucho.server.cache.AbstractCacheEntry; import com.caucho.server.cache.AbstractCacheFilterChain; import com.caucho.server.webapp.WebApp; import com.caucho.util.L10N; import com.caucho.vfs.ClientDisconnectException; abstract public class ResponseStream extends ToByteResponseStream { private static final Logger log = Logger.getLogger(ResponseStream.class.getName()); private static final L10N L = new L10N(ResponseStream.class); private final byte []_singleByteBuffer = new byte[1]; private AbstractHttpResponse _response; private CauchoResponse _proxyCacheResponse; private AbstractCacheFilterChain _cacheInvocation; private OutputStream _cacheStream; private long _cacheMaxLength; private boolean _isDisableAutoFlush; // bytes actually written private int _contentLength; private boolean _isAllowFlush = true; private boolean _isComplete; public ResponseStream() { } protected ResponseStream(AbstractHttpResponse response) { setResponse(response); } public void setResponse(AbstractHttpResponse response) { _response = response; } protected AbstractHttpResponse getResponse() { return _response; } public void setProxyCacheResponse(CauchoResponse response) { _proxyCacheResponse = response; } public CauchoResponse getCauchoResponse() { return _proxyCacheResponse; } /** * initializes the Response stream at the beginning of a request. */ @Override public void start() { super.start(); _contentLength = 0; _isAllowFlush = true; _isDisableAutoFlush = false; _cacheStream = null; _proxyCacheResponse = null; _isComplete = false; } /** * Returns true for a Caucho response stream. */ @Override public boolean isCauchoResponseStream() { return true; } /** * Sets the underlying cache stream for a cached request. * * @param cacheStream the cache stream. */ @Override public void setByteCacheStream(OutputStream cacheStream) { _cacheStream = cacheStream; if (cacheStream == null) return; AbstractHttpRequest req = _response.getRequest(); WebApp webApp = req.getWebApp(); _cacheMaxLength = webApp.getCacheMaxLength(); } @Override protected OutputStream getByteCacheStream() { return _cacheStream; } /** * Response stream is a writable stream. */ public boolean canWrite() { return true; } @Override protected boolean setFlush(boolean flush) { boolean isFlush = _isAllowFlush; _isAllowFlush = flush; return isFlush; } @Override public void setAutoFlush(boolean isAutoFlush) { setDisableAutoFlush(! isAutoFlush); } void setDisableAutoFlush(boolean disable) { _isDisableAutoFlush = disable; } @Override protected boolean isDisableAutoFlush() { return _isDisableAutoFlush; } @Override public final int getContentLength() { // server/05e8 try { flushCharBuffer(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } if (isCommitted()) return _contentLength; else return super.getContentLength(); } @Override public void setBufferSize(int size) { if (isCommitted()) throw new IllegalStateException(L.l("Buffer size cannot be set after commit")); super.setBufferSize(size); } public boolean hasData() { return isCommitted() || _contentLength > 0; } @Override public boolean isCommitted() { if (super.isCommitted()) return true; // when the data hits the content-length, the request // is committed even if the data isn't actually flushed. try { if (_contentLength > 0) { flushCharBuffer(); int bufferOffset = getByteBufferOffset(); // server/05e8 if (_contentLength <= bufferOffset) { setCommitted(); return true; } } } catch (Exception e) { log.log(Level.FINER, e.toString(), e); } return false; } @Override public void clear() throws IOException { clearBuffer(); if (isCommitted()) throw new IOException(L.l("can't clear response after writing headers")); } @Override public void clearBuffer() { super.clearBuffer(); if (! isCommitted()) { // jsp/15la _response.setHeaderWritten(false); } clearNext(); } public boolean isCloseComplete() { return super.isCloseComplete() || _isComplete; } /** * Clear the closed state, because of the NOT_MODIFIED */ public void clearClosed() { // _isClosed = false; } @Override protected void writeHeaders(int length) throws IOException { if (isCommitted()) return; // server/05ef if (! isCloseComplete() || isCharFlushing()) length = -1; CauchoResponse proxyCacheResponse = _proxyCacheResponse; _proxyCacheResponse = null; if (proxyCacheResponse != null) { proxyCacheResponse.writeHeaders(length); } startCaching(true); _response.writeHeaders(length); // server/2hf3 setCommitted(); } @Override public final void write(int ch) throws IOException { _singleByteBuffer[0] = (byte) ch; write(_singleByteBuffer, 0, 1); } /** * Returns the byte buffer. */ @Override public final byte []getBuffer() throws IOException { if (isCommitted()) { flushBuffer(); return getNextBuffer(); } else return super.getBuffer(); } /** * Returns the byte offset. */ @Override public final int getBufferOffset() throws IOException { if (! isCommitted()) return super.getBufferOffset(); flushBuffer(); return getNextBufferOffset(); } /** * Sets the byte offset. */ @Override public final void setBufferOffset(int offset) throws IOException { if (isClosed()) { return; } if (! isCommitted()) { super.setBufferOffset(offset); return; } flushBuffer(); int startOffset = getNextStartOffset(); if (offset == startOffset) return; int oldOffset = getNextBufferOffset(); int sublen = (offset - oldOffset); long lengthHeader = _response.getContentLengthHeader(); if (lengthHeader > 0 && lengthHeader < _contentLength + sublen) { byte []nextBuffer = getNextBuffer(); lengthException(nextBuffer, oldOffset, sublen, lengthHeader); sublen = (int) (lengthHeader - _contentLength); offset = oldOffset + sublen; } _contentLength += sublen; if (_cacheStream != null) { byte []nextBuffer = getNextBuffer(); writeCache(nextBuffer, oldOffset, sublen); } if (! isHead()) { // server/051e setNextBufferOffset(offset); } } /** * Sets the next buffer */ @Override public final byte []nextBuffer(int offset) throws IOException { if (! isCommitted()) { // server/055b return super.nextBuffer(offset); } if (isClosed()) { return getNextBuffer(); } flushBuffer(); byte []nextBuffer = getNextBuffer(); int startOffset = getNextStartOffset(); int oldOffset = getNextBufferOffset(); int sublen = offset - oldOffset; long lengthHeader = _response.getContentLengthHeader(); if (lengthHeader > 0 && lengthHeader < _contentLength + sublen) { lengthException(nextBuffer, startOffset, sublen, lengthHeader); sublen = (int) (lengthHeader - _contentLength); } _contentLength += sublen; // server/1213 offset = oldOffset + sublen; if (isHead()) { return nextBuffer; } if (_cacheStream != null) writeCache(nextBuffer, oldOffset, sublen); return writeNextBuffer(offset); } /** * Writes the next chunk of data to the response stream. * * @param buf the buffer containing the data * @param offset start offset into the buffer * @param length length of the data in the buffer */ @Override protected final void writeNext(byte []buf, int offset, int length, boolean isFinished) throws IOException { if (isClosed()) { return; } if (_isDisableAutoFlush && ! isFinished) throw new IOException(L.l("auto-flushing has been disabled")); int bufferOffset = getNextBufferOffset(); if (length == 0 && bufferOffset == 0) { return; } int bufferStart = getNextStartOffset(); // server/05e2 if (length == 0 && bufferStart == bufferOffset) { // server/26a5 // writeNextBuffer(bufferOffset); return; } long contentLengthHeader = _response.getContentLengthHeader(); // Can't write beyond the content length if (0 < contentLengthHeader && contentLengthHeader < length + _contentLength) { if (lengthException(buf, offset, length, contentLengthHeader)) return; length = (int) (contentLengthHeader - _contentLength); } if (isHead()) { return; } if (_cacheStream != null) writeCache(buf, offset, length); byte []buffer = getNextBuffer(); int writeLength = length; while (writeLength > 0) { int sublen = buffer.length - bufferOffset; if (writeLength < sublen) sublen = writeLength; System.arraycopy(buf, offset, buffer, bufferOffset, sublen); writeLength -= sublen; offset += sublen; bufferOffset += sublen; _contentLength += sublen; if (writeLength > 0) { buffer = writeNextBuffer(bufferOffset); bufferStart = getNextStartOffset(); bufferOffset = bufferStart; } } // server/051c if (bufferOffset < buffer.length) setNextBufferOffset(bufferOffset); else { writeNextBuffer(bufferOffset); } } private boolean lengthException(byte []buf, int offset, int length, long contentLengthHeader) { if (_response.isConnectionClosed() || isHead() || isClosed()) { } else if (contentLengthHeader < _contentLength) { AbstractHttpRequest request = _response.getRequest(); String msg = L.l("{0}: Can't write {1} extra bytes beyond the content-length header {2}. Check that the Content-Length header correctly matches the expected bytes, and ensure that any filter which modifies the content also suppresses the content-length (to use chunked encoding).", request.getRequestURL(), "" + (length + _contentLength), "" + contentLengthHeader); log.fine(msg); return false; } for (int i = (int) (offset + contentLengthHeader - _contentLength); i < offset + length; i++) { int ch = buf[i]; if (ch != '\r' && ch != '\n' && ch != ' ' && ch != '\t') { AbstractHttpRequest request = _response.getRequest(); String graph = ""; if (Character.isLetterOrDigit((char) ch)) graph = "'" + (char) ch + "', "; String msg = L.l("{0}: tried to write {1} bytes with content-length {2} (At {3}char={4}). Check that the Content-Length header correctly matches the expected bytes, and ensure that any filter which modifies the content also suppresses the content-length (to use chunked encoding).", request.getRequestURL(), "" + (length + _contentLength), "" + contentLengthHeader, graph, "" + ch); log.fine(msg); break; } } length = (int) (contentLengthHeader - _contentLength); return (length <= 0); } /** * Flushes the buffered response to the output stream. */ @Override public final void flush() throws IOException { _isDisableAutoFlush = false; if (_isAllowFlush && ! isClosed()) { flushBuffer(); int bufferOffset = getNextBufferOffset(); if (bufferOffset > 0) { int bufferStart = getNextStartOffset(); if (bufferStart != bufferOffset) { // server/10c9 // _contentLength += (bufferOffset - bufferStart); writeNextBuffer(bufferOffset); } } flushNext(); } } /** * Flushes the buffered response to the output stream. */ @Override public void flushByte() throws IOException { flush(); } /** * Flushes the buffered response to the writer. */ @Override public void flushChar() throws IOException { flush(); } /** * Complete the request. */ @Override protected void closeImpl() throws IOException { try { closeBuffer(); writeTail(true); closeCache(); closeNext(); } catch (ClientDisconnectException e) { _response.clientDisconnect(); if (! _response.isIgnoreClientDisconnect()) { throw e; } } } private void closeBuffer() throws IOException { _isDisableAutoFlush = false; flushCharBuffer(); _isAllowFlush = true; flushBuffer(); // flushBuffer can force 304 and then a cache write which would // complete the finish. /* if (isClosed()) { return; } */ // XXX: this needs to be cleaned up with the above // use of writeHeaders if (! _response.isHeaderWritten()) { writeHeaders(-1); } } // // proxy caching // /** * Called to start caching. */ protected void startCaching(boolean isByte) { // server/1373 for getBufferSize() HttpServletResponseImpl res = _response.getRequest().getResponseFacade(); if (res == null || res.getStatus() != HttpServletResponse.SC_OK || res.isDisableCache()) { return; } // server/13de if (_cacheInvocation != null) return; AbstractCacheFilterChain cacheInvocation = res.getCacheInvocation(); if (cacheInvocation == null) return; _cacheInvocation = cacheInvocation; HttpServletRequestImpl req = _response.getRequest().getRequestFacade(); ArrayList<String> keys = res.getHeaderKeys(); ArrayList<String> values = res.getHeaderValues(); String contentType = res.getContentTypeImpl(); String charEncoding = res.getCharacterEncodingImpl(); int contentLength = -1; AbstractCacheEntry newCacheEntry = cacheInvocation.startCaching(req, res, keys, values, contentType, charEncoding, contentLength); if (newCacheEntry == null) { } else if (isByte) { setByteCacheStream(newCacheEntry.openOutputStream()); } else { setCharCacheStream(newCacheEntry.openWriter()); } } private void writeCache(byte []buf, int offset, int length) throws IOException { if (length == 0) return; if (_cacheMaxLength < _contentLength) { _cacheStream = null; // XXX: _response.killCache(); } else { _cacheStream.write(buf, offset, length); } } @Override public void killCaching() { AbstractCacheFilterChain cacheInvocation = _cacheInvocation; if (cacheInvocation != null) { HttpServletResponseImpl res = _response.getRequest().getResponseFacade(); cacheInvocation.killCaching(res); setByteCacheStream(null); setCharCacheStream(null); } } @Override public void completeCache() { HttpServletResponseImpl res = _response.getRequest().getResponseFacade(); HttpServletRequestImpl req = _response.getRequest().getRequestFacade(); if (req == null) return; // server/1la7 if (req.isAsyncStarted()) return; try { _isComplete = true; closeBuffer(); if (! isNextValid()) { killCaching(); } OutputStream cacheStream = getByteCacheStream(); setByteCacheStream(null); Writer cacheWriter = getCharCacheStream(); setCharCacheStream(null); if (cacheStream != null) cacheStream.close(); if (cacheWriter != null) cacheWriter.close(); AbstractCacheFilterChain cache = _cacheInvocation; if (cache != null && res != null) { _cacheInvocation = null; WebApp webApp = res.getRequest().getWebApp(); if (webApp != null && webApp.isActive()) { cache.finishCaching(res); } } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } finally { AbstractCacheFilterChain cache = _cacheInvocation; _cacheInvocation = null; if (cache != null) cache.killCaching(res); } } private void closeCache() { AbstractCacheFilterChain cache = _cacheInvocation; _cacheInvocation = null; HttpServletResponseImpl res = _response.getRequest().getResponseFacade(); try { OutputStream cacheStream = getByteCacheStream(); setByteCacheStream(null); Writer cacheWriter = getCharCacheStream(); setCharCacheStream(null); if (cacheStream != null) cacheStream.close(); if (cacheWriter != null) cacheWriter.close(); } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } finally { if (cache != null) cache.killCaching(res); } } // // implementations // protected final boolean isNextValid() { return ! _response.isConnectionClosed(); } protected void clearNext() { } abstract protected byte []getNextBuffer(); protected int getNextStartOffset() { return 0; } abstract protected int getNextBufferOffset() throws IOException; protected final void setNextBufferOffset(int offset) throws IOException { boolean isValid = false; try { setNextBufferOffsetImpl(offset); isValid = true; } catch (ClientDisconnectException e) { if (! _response.isIgnoreClientDisconnect()) throw e; } finally { if (! isValid) _response.clientDisconnect(); } } abstract protected void setNextBufferOffsetImpl(int offset) throws IOException; protected final byte []writeNextBuffer(int offset) throws IOException { boolean isValid = false; try { byte []buffer = writeNextBufferImpl(offset); isValid = true; return buffer; } catch (ClientDisconnectException e) { if (! _response.isIgnoreClientDisconnect()) throw e; else { log.log(Level.FINER, e.toString(), e); } return getNextBuffer(); } finally { if (! isValid) _response.clientDisconnect(); } } abstract protected byte []writeNextBufferImpl(int offset) throws IOException; @Override public final void flushNext() throws IOException { boolean isValid = false; try { flushNextImpl(); isValid = true; } catch (ClientDisconnectException e) { if (! _response.isIgnoreClientDisconnect()) throw e; } finally { if (! isValid) _response.clientDisconnect(); } } protected abstract void flushNextImpl() throws IOException; protected final void closeNext() throws IOException { boolean isValid = false; try { closeNextImpl(); isValid = true; } finally { if (! isValid) { _response.clientDisconnect(); } } } abstract protected void closeNextImpl() throws IOException; protected final void writeTail(boolean isClose) throws IOException { boolean isValid = false; try { writeTailImpl(isClose); isValid = true; } finally { if (! isValid) _response.clientDisconnect(); } } protected void writeTailImpl(boolean isClosed) throws IOException { } protected String dbgId() { Object request = _response.getRequest(); if (request instanceof AbstractHttpRequest) { AbstractHttpRequest req = (AbstractHttpRequest) request; return req.dbgId(); } else return "inc "; } @Override public String toString() { return getClass().getSimpleName() + "[" + _response + "]"; } }