/*
* =============================================================================
*
* 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.io.Writer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import org.thymeleaf.exceptions.TemplateOutputException;
/**
*
* @author Daniel Fernández
*
* @since 3.0.0
*
*/
class ThrottledTemplateWriter extends Writer implements IThrottledTemplateWriterControl {
private final String templateName;
private final TemplateFlowController flowController;
private IThrottledTemplateWriterAdapter adapter;
private Writer writer;
private boolean flushable;
ThrottledTemplateWriter(final String templateName, final TemplateFlowController flowController) {
super();
this.templateName = templateName;
this.flowController = flowController;
this.adapter = null;
this.writer = null;
this.flushable = false;
}
void setOutput(final Writer writer) {
if (this.adapter != null && this.adapter instanceof ThrottledTemplateWriterOutputStreamAdapter) {
throw new TemplateOutputException(
"The throttled processor has already been initialized to use byte-based output (OutputStream), " +
"but a Writer has been specified.", this.templateName, -1, -1, null);
}
if (this.adapter == null) {
this.adapter = new ThrottledTemplateWriterWriterAdapter(this.templateName, this.flowController);
this.writer = ((ThrottledTemplateWriterWriterAdapter)this.adapter);
}
((ThrottledTemplateWriterWriterAdapter)this.adapter).setWriter(writer);
}
void setOutput(final OutputStream outputStream, final Charset charset, final int maxOutputInBytes) {
if (this.adapter != null && this.adapter instanceof ThrottledTemplateWriterWriterAdapter) {
throw new TemplateOutputException(
"The throttled processor has already been initialized to use char-based output (Writer), " +
"but an OutputStream has been specified.", this.templateName, -1, -1, null);
}
if (this.adapter == null) {
final int adapterOverflowBufferIncrementBytes =
(maxOutputInBytes == Integer.MAX_VALUE?
128 :
// output size could be too small, so we will set a minimum of 16b, and max of 128b
Math.min(128, Math.max(16, maxOutputInBytes / 8)));
this.adapter = new ThrottledTemplateWriterOutputStreamAdapter(this.templateName, this.flowController, adapterOverflowBufferIncrementBytes);
// We cannot directly use a java.io.OutputStreamWriter here because that class uses a CharsetEncoder
// underneath that always creates a 8192byte (8KB) buffer, and there is no way to configure that.
//
// The problem with such buffer is that we are counting the number of output bytes at the OutputStream
// wrapper (the adapter we just created), which is set as the output of the OutputStreamWriter, and which
// does not receive any bytes until the OutputStreamWriter flushes its 8KB buffer. But in a scenario in
// which, for instance, we only need 100 bytes to complete our output chunk, this would mean we would still
// have an overflow of more than 8,000 bytes. And that basically renders this whole template throttling
// mechanism useless.
//
// So we will use an alternative construct to OutputStreamWriter, based on a WritableByteChannel. This
// will basically work in the same way as an OutputStreamWriter, but by building it manually we will be
// able to specify the size of the buffer to be used.
//
// And we do not want the buffer at the Writer -> OutputStream converter to completely disappear, because
// it actually improves the performance of the converter. So we will use the maxOutputInBytes (the size
// of the output to be obtained from the throttled template the first time) as an approximate measure
// of what we will need in subsequent calls, and we will to try to adjust the size of the buffer so
// that we make the most use of it without needing to flush too often, nor 'losing' chars in the buffer.
//
// Last, note that in order to avoid this 'loss of chars' we will combine this with 'flush' calls at the
// 'isOverflown()' and 'isStopped()' calls.
final CharsetEncoder charsetEncoder = charset.newEncoder();
int channelBufferSize =
(maxOutputInBytes == Integer.MAX_VALUE?
1024 :
// Buffers of CharsetEncoders behave strangely (even hanging) when the buffers being
// set are too small to house the encoding of some elements (e.g. 1 or 2 bytes). So we
// will set a minimum of 64b and a max of 512b.
Math.min(512, Math.max(64, adapterOverflowBufferIncrementBytes * 2)));
final WritableByteChannel channel = Channels.newChannel((ThrottledTemplateWriterOutputStreamAdapter)this.adapter);
this.writer = Channels.newWriter(channel, charsetEncoder, channelBufferSize);
// Use of a wrapping BufferedWriter is recommended by OutputStreamWriter javadoc for improving efficiency,
// avoiding frequent converter invocations (note that the character converter also has its own buffer).
//this.writer = new BufferedWriter(new OutputStreamWriter((ThrottledTemplateWriterOutputStreamAdapter)this.adapter, charset));
}
((ThrottledTemplateWriterOutputStreamAdapter)this.adapter).setOutputStream(outputStream);
}
public boolean isOverflown() throws IOException {
if (this.flushable) {
// We need this flushing because OutputStreamWriter bufferizes, and given we might be taking account of
// the output bytes at an OutputStream implementation in a level below this OutputStreamWriter, we could
// have the wrong figures until we flush contents.
this.flush();
this.flushable = false;
}
return this.adapter.isOverflown();
}
public boolean isStopped() throws IOException {
if (this.flushable) {
// We need this flushing because OutputStreamWriter bufferizes, and given we might be taking account of
// the output bytes at an OutputStream implementation in a level below this OutputStreamWriter, we could
// have the wrong figures until we flush contents.
this.flush();
this.flushable = false;
}
return this.adapter.isStopped();
}
public int getWrittenCount() {
return this.adapter.getWrittenCount();
}
public int getMaxOverflowSize() {
return this.adapter.getMaxOverflowSize();
}
public int getOverflowGrowCount() {
return this.adapter.getOverflowGrowCount();
}
void allow(final int limit) {
this.adapter.allow(limit);
}
@Override
public void write(final int c) throws IOException {
this.flushable = true;
this.writer.write(c);
}
@Override
public void write(final String str) throws IOException {
this.flushable = true;
this.writer.write(str);
}
@Override
public void write(final String str, final int off, final int len) throws IOException {
this.flushable = true;
this.writer.write(str, off, len);
}
@Override
public void write(final char[] cbuf) throws IOException {
this.flushable = true;
this.writer.write(cbuf);
}
@Override
public void write(final char[] cbuf, final int off, final int len) throws IOException {
this.flushable = true;
this.writer.write(cbuf, off, len);
}
@Override
public void flush() throws IOException {
this.writer.flush();
}
@Override
public void close() throws IOException {
this.writer.close();
}
interface IThrottledTemplateWriterAdapter {
boolean isOverflown();
boolean isStopped();
int getWrittenCount();
int getMaxOverflowSize();
int getOverflowGrowCount();
void allow(final int limit);
}
}