/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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 com.google.apphosting.vmruntime;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
/**
* An implementation of {@link ServletOutputStream} wrapping an OutputStream object. Writes are
* forwarded to the underlying object immediately. Calls that can trigger either a flush or a close
* are delayed until {@code CommitDelayingOutputStream#closeIfClosed} and
* {@code CommitDelayingOutputStream#flushIfFlushed} are called respectively.
*
* <p>This implementation is mimicking the behavior of the {@link ServletOutputStream} returned by
* the HTTP {@link org.eclipse.jetty.server.Response} implementation in Jetty9 with the important
* difference that no user visible methods are able to commit the response. A committed response
* means that the HTTP response headers have been sent to the user. For our purposes this means that
* any changes to HTTP headers after this point are ignored and that any API calls after that point
* might fail.
*
*/
class CommitDelayingOutputStream extends ServletOutputStream {
// The wrapped OutputStream is configured with an output buffer of 32 MB (see jetty9/jetty.xml).
// 32MB is also the maximum response size allowed by AppEngine. Setting the buffer size to the
// maximum response size ensures that no flush occurs due to full buffer.
static final int MAX_RESPONSE_SIZE_BYTES = 32 * 1024 * 1024;
private int bufferSize = MAX_RESPONSE_SIZE_BYTES;
// Make sure this matches responseHeaderSize value in jetty9/jetty.xml!
static final int MAX_RESPONSE_HEADERS_SIZE_BYTES = 8192;
// Previous two constants are package level so unit test has access.
// The number of bytes written to the OutputStream.
// Used to decide if we reached the buffer size.
private int bytesWritten = 0;
// True if the OutputStream was closed by the user. We do not forward the close call to
// the underlying OutputStream until closeIfClosed() is called.
private boolean closed = false;
// True if an operation has been performed that would have flushed the underlying OutputStream.
private boolean flushed = false;
// To emulate the behavior of the native Jetty9 Response OutputStream we need to know if the user
// set the content length header on the response. If that happens the response will be flushed as
// soon as "contentLength" bytes are written to the OutputStream.
private long contentLength = -1;
private boolean contentLengthSet = false;
// This is the underlying OutputStream where calls are forwarded. Writes are forwarded
// immediately. Calls that can trigger either a flush or a close are delayed until closeIfClosed()
// and flushIfFlushed() are called respectively.
private final OutputStream wrappedOutputStream;
/**
* Creates a new CommitDelayingOutputStream object.
*
* @param wrappedOutputStream The OutputStream to forward writes to.
*/
CommitDelayingOutputStream(OutputStream wrappedOutputStream) {
this.wrappedOutputStream = wrappedOutputStream;
}
/**
* Updates the number of bytes written to the stream. The stream is marked as flushed if the
* buffer size or content length has been reached.
*
* @param num The number of bytes written.
*/
private void bytesWritten(int num) {
bytesWritten += num;
if (flushed) {
return;
}
if (bytesWritten >= bufferSize) {
flushed = true;
return;
}
if (contentLengthSet && bytesWritten >= contentLength) {
flushed = true;
}
}
/**
* Verifies that this OutputStream is writable.
*
* @throws IOException If the OutputStream is closed.
*/
private void ensureWritable() throws IOException {
if (closed) {
throw new IOException("Closed");
}
}
/**
* Mark this stream as closed without forwarding the close call to the underlying stream. Writes
* to this object will still behave as if the stream is closed (i.e. throw an IOException) but the
* underlying stream is not closed until {@code CommitDelayingOutputStream#closeIfClosed()} is
* called.
*/
@Override
public void close() {
flushed = true;
closed = true;
}
/**
* Close the underlying stream if close() has been called on this object. This is a no-op if
* close() never was called on this object.
*
* @throws IOException If an IOException occurred when closing the underlying stream.
*/
void closeIfClosed() throws IOException {
if (closed) {
wrappedOutputStream.close();
}
}
/**
* Marks this stream as flushed. The flush is not forwarded to the underlying stream. Instead
* flush is called on that stream when {@code CommitDelayingOutputStream#flushIfFlushed()} is
* called.
*
* @throws IOException If the stream is closed.
*/
@Override
public void flush() throws IOException {
ensureWritable();
flushed = true;
}
/**
* Flush the underlying stream any action has been performed that would have resulted in a flush
* (through flush(), setBufferSize(), or setContentLength().
*
* @throws IOException If an IOException occurred when closing the underlying stream.
*/
void flushIfFlushed() throws IOException {
if (flushed) {
wrappedOutputStream.flush();
}
}
/**
* @return The buffer size of this stream.
*/
int getBufferSize() {
return bufferSize;
}
/**
* Sets the buffer size of this stream. If the number of bytes written is greater than or equal to
* the new buffer size the stream is marked as flushed.
*
* @param bufferSize The new buffer size.
*/
void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
if (bytesWritten >= bufferSize) {
flushed = true;
}
}
/**
* @return The number of bytes written to this stream.
*/
int getBytesWritten() {
return bytesWritten;
}
/**
* Clear any content length set on this stream.
*/
void clearContentLength() {
this.contentLength = -1;
this.contentLengthSet = false;
}
/**
* Returns the content length used by this steam. Note: this is content-length in the context of
* the HTTP header and is different from the number of bytes written to the stream so far.
*
* @return The content length used by this steam.
*/
long getContentLength() {
return contentLength;
}
/**
* @return true if content length has been set, false otherwise.
*/
boolean hasContentLength() {
return contentLengthSet;
}
/**
* Sets the content length to be used by this steam. If the number of bytes written is equal or
* greater than the new content length the stream is marked as flushed. Note: this is
* content-length in the context of the HTTP header and is different from the number of bytes
* written to the stream so far.
*
*/
void setContentLength(long contentLength) {
this.contentLengthSet = true;
this.contentLength = contentLength;
if (bytesWritten >= contentLength) {
flushed = true;
}
}
/**
* Returns true if the response is committed (i.e. flushed or closed). This is in an HTTP context
* where a committed response means that any HTTP headers have been sent to the user.
*
* @return True if the response is committed, false otherwise.
*/
boolean isCommitted() {
return closed || flushed;
}
/**
* Resets the stream by setting the number of bytes written to zero. Note: the underlying stream
* must be reset by calling reset() on the parent {@code HttpServletResponse}.
*/
void reset() {
bytesWritten = 0;
}
/**
* Make sure we don't go over the max buffer size, otherwise jetty's HttpOutput will
* automatically commit the stream, which defeats the purpose of this class.
*/
private void checkResponseSize(int bytesToWrite) throws IOException {
// We don't check against the current buffer size, but instead the max, because we intercept
// the setBufferSize call, meaning the underlying HttpOutput buffer size never changes from
// its initial value of the max. Also we subtract the max header size, since headers also
// count towards the total.
if (bytesWritten + bytesToWrite > MAX_RESPONSE_SIZE_BYTES - MAX_RESPONSE_HEADERS_SIZE_BYTES) {
throw new IOException("Max response size exceeded.");
}
}
/*
* @see java.io.OutputStream#write(byte[])
*/
@Override
public void write(byte[] b) throws IOException {
checkResponseSize(b.length);
ensureWritable();
wrappedOutputStream.write(b);
bytesWritten(b.length);
}
/*
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
checkResponseSize(len);
ensureWritable();
wrappedOutputStream.write(b, off, len);
bytesWritten(len);
}
/*
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(int b) throws IOException {
checkResponseSize(1);
ensureWritable();
wrappedOutputStream.write(b);
bytesWritten(1);
}
@Override
public void setWriteListener(WriteListener writeListener) {
// TODO(user): need to implement when really needed. (Servlet 3.1 specific).
}
@Override
public boolean isReady() {
return true;
}
}