/*
* =============================================================================
*
* Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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.thymeleaf.engine;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import org.thymeleaf.exceptions.TemplateOutputException;
/**
*
* @author Daniel Fernández
*
* @since 3.0.0
*
*/
final class ThrottledTemplateWriterOutputStreamAdapter
extends OutputStream
implements ThrottledTemplateWriter.IThrottledTemplateWriterAdapter {
private final String templateName;
private final TemplateFlowController flowController;
// We will use a different increment depending on the size of the chunks asked by the throttled template
// client. This is a complex setup because this adapter will be fed by a byte array channel acting as a
// Writer -> OutputStream bridge that will in fact have its own buffer, and due to this we will need at least
// the same size as the buffer in this channel (in fact, a bit more) if we don't want to be continuously growing
// our overflow buffer. So if chunks are x in size, the channel's buffer will be x/4, and we will have an overflow
// of (x/8)*3, growing in chunks of x/8.
// See the implementation of this mechanism at ThrottledTemplateWriter for more info.
private final int overflowIncrementInBytes;
private OutputStream os;
private byte[] overflow;
private int overflowSize;
private int maxOverflowSize;
private int overflowGrowCount;
private boolean unlimited;
private int limit;
private int writtenCount;
ThrottledTemplateWriterOutputStreamAdapter(
final String templateName, final TemplateFlowController flowController, final int overflowIncrementInBytes) {
super();
this.templateName = templateName;
this.flowController = flowController;
this.overflowIncrementInBytes = overflowIncrementInBytes;
this.overflow = null;
this.overflowSize = 0;
this.maxOverflowSize = 0;
this.overflowGrowCount = 0;
this.unlimited = false;
this.limit = 0;
this.writtenCount = 0;
this.flowController.stopProcessing = true;
}
void setOutputStream(final OutputStream os) {
this.os = os;
this.writtenCount = 0;
}
public boolean isOverflown() {
return this.overflowSize > 0;
}
public boolean isStopped() {
return this.limit == 0;
}
public int getWrittenCount() {
return this.writtenCount;
}
public int getMaxOverflowSize() {
return this.maxOverflowSize;
}
public int getOverflowGrowCount() {
return this.overflowGrowCount;
}
public void allow(final int limit) {
if (limit == Integer.MAX_VALUE || limit < 0) {
this.unlimited = true;
this.limit = -1;
} else {
this.unlimited = false;
this.limit = limit;
}
this.flowController.stopProcessing = (this.limit == 0);
if (this.overflowSize == 0 || this.limit == 0) {
return;
}
try {
if (this.unlimited || this.limit > this.overflowSize) {
this.os.write(this.overflow, 0, this.overflowSize);
if (!this.unlimited) {
this.limit -= this.overflowSize;
}
this.writtenCount += this.overflowSize;
this.overflowSize = 0;
return;
}
this.os.write(this.overflow, 0, this.limit);
if (this.limit < this.overflowSize) {
System.arraycopy(this.overflow, this.limit, this.overflow, 0, this.overflowSize - this.limit);
}
this.overflowSize -= this.limit;
this.writtenCount += this.limit;
this.limit = 0;
this.flowController.stopProcessing = true;
} catch (final IOException e) {
throw new TemplateOutputException(
"Exception while trying to write overflowed buffer in throttled template", this.templateName, -1, -1, e);
}
}
@Override
public void write(final int b) throws IOException {
if (this.limit == 0) {
overflow(b);
return;
}
this.os.write(b);
if (!this.unlimited) {
this.limit--;
}
this.writtenCount++;
if (this.limit == 0) {
this.flowController.stopProcessing = true;
}
}
@Override
public void write(final byte[] bytes, final int off, final int len) throws IOException {
if (this.limit == 0) {
overflow(bytes, off, len);
return;
}
if (this.unlimited || this.limit > len) {
this.os.write(bytes, off, len);
if (!this.unlimited) {
this.limit -= len;
}
this.writtenCount += len;
return;
}
this.os.write(bytes, off, this.limit);
if (this.limit < len) {
overflow(bytes, off + this.limit, (len - this.limit));
}
this.writtenCount += this.limit;
this.limit = 0;
this.flowController.stopProcessing = true;
}
@Override
public void write(final byte[] bytes) throws IOException {
final int len = bytes.length;
if (this.limit == 0) {
overflow(bytes, 0, len);
return;
}
if (this.unlimited || this.limit > len) {
this.os.write(bytes, 0, len);
if (!this.unlimited) {
this.limit -= len;
}
this.writtenCount += len;
return;
}
this.os.write(bytes, 0, this.limit);
if (this.limit < len) {
overflow(bytes, this.limit, (len - this.limit));
}
this.writtenCount += this.limit;
this.limit = 0;
this.flowController.stopProcessing = true;
}
private void overflow(final int c) {
ensureOverflowCapacity(1);
this.overflow[this.overflowSize] = (byte)c;
this.overflowSize++;
if (this.overflowSize > this.maxOverflowSize) {
this.maxOverflowSize = this.overflowSize;
}
}
private void overflow(final byte[] bytes, final int off, final int len) {
ensureOverflowCapacity(len);
System.arraycopy(bytes, off, this.overflow, this.overflowSize, len);
this.overflowSize += len;
if (this.overflowSize > this.maxOverflowSize) {
this.maxOverflowSize = this.overflowSize;
}
}
private void ensureOverflowCapacity(final int len) {
if (this.overflow == null) {
int bufferInitialSize = this.overflowIncrementInBytes * 3;
while (bufferInitialSize < len) {
bufferInitialSize += this.overflowIncrementInBytes;
}
this.overflow = new byte[bufferInitialSize];
return;
}
final int targetLen = this.overflowSize + len;
if (this.overflow.length < targetLen) {
int newLen = this.overflow.length;
do {
newLen += this.overflowIncrementInBytes;
} while (newLen < targetLen);
this.overflow = Arrays.copyOf(this.overflow, newLen);
this.overflowGrowCount++;
}
}
@Override
public void flush() throws IOException {
// No need to control overflow here. The fact that this has overflow will be used as a flag to determine
// that further write operations are actually needed by means of the isOverflown() method.
this.os.flush();
}
@Override
public void close() throws IOException {
// This will normally be NEVER called, as Thymeleaf will not call close() on its Writers/OutputStreams
// (only flush() is guaranteed to be called at the end).
this.os.close();
}
}