blob: 6c593fd2f154be8831069e99a7e0add175242cdc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.struts2.util;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.jsp.JspWriter;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.LinkedList;
/**
* A speedy implementation of ByteArrayOutputStream. It's not synchronized, and it
* does not copy buffers when it's expanded. There's also no copying of the internal buffer
* if it's contents is extracted with the writeTo(stream) method.
*
*/
public class FastByteArrayOutputStream extends OutputStream {
private static final Logger LOG = LogManager.getLogger(FastByteArrayOutputStream.class);
private static final int DEFAULT_BLOCK_SIZE = 8192;
private LinkedList<byte[]> buffers;
private byte buffer[];
private int index;
private int size;
private int blockSize;
private boolean closed;
public FastByteArrayOutputStream() {
this(DEFAULT_BLOCK_SIZE);
}
public FastByteArrayOutputStream(int blockSize) {
buffer = new byte[this.blockSize = blockSize];
}
public void writeTo(OutputStream out) throws IOException {
if (buffers != null) {
for (byte[] bytes : buffers) {
out.write(bytes, 0, blockSize);
}
}
out.write(buffer, 0, index);
}
public void writeTo(RandomAccessFile out) throws IOException {
if (buffers != null) {
for (byte[] bytes : buffers) {
out.write(bytes, 0, blockSize);
}
}
out.write(buffer, 0, index);
}
/**
* This is a patched method (added for common Writer, needed for tests)
* @param out Writer
* @param encoding Encoding
* @throws IOException If some output failed
*/
public void writeTo(Writer out, String encoding) throws IOException {
if (encoding != null) {
CharsetDecoder decoder = getDecoder(encoding);
// Create buffer for characters decoding
CharBuffer charBuffer = CharBuffer.allocate(buffer.length);
// Create buffer for bytes
float bytesPerChar = decoder.charset().newEncoder().maxBytesPerChar();
ByteBuffer byteBuffer = ByteBuffer.allocate((int) (buffer.length + bytesPerChar));
if (buffers != null) {
for (byte[] bytes : buffers) {
decodeAndWriteOut(out, bytes, bytes.length, byteBuffer, charBuffer, decoder, false);
}
}
decodeAndWriteOut(out, buffer, index, byteBuffer, charBuffer, decoder, true);
} else {
if (buffers != null) {
for (byte[] bytes : buffers) {
writeOut(out, bytes, bytes.length);
}
}
writeOut(out, buffer, index);
}
}
private CharsetDecoder getDecoder(String encoding) {
Charset charset = Charset.forName(encoding);
return charset.newDecoder().
onMalformedInput(CodingErrorAction.REPORT).
onUnmappableCharacter(CodingErrorAction.REPLACE);
}
/**
* This is a patched method (standard)
* @param out Writer
* @param encoding Encoding
* @throws IOException If some output failed
*/
public void writeTo(JspWriter out, String encoding) throws IOException {
try {
writeTo((Writer) out, encoding);
} catch (IOException e) {
writeToFile();
throw e;
} catch (Throwable e) {
writeToFile();
throw new RuntimeException(e);
}
}
/**
* This method is need only for debug. And needed for tests generated files.
*/
private void writeToFile() {
try (FileOutputStream fileOutputStream = new FileOutputStream(File.createTempFile(getClass().getName() + System.currentTimeMillis(), ".log"))){
writeTo(fileOutputStream);
} catch (IOException e) {
// Ignore
}
}
private void writeOut(Writer out, byte[] bytes, int length) throws IOException {
out.write(new String(bytes, 0, length));
}
private static void decodeAndWriteOut(Writer writer, byte[] bytes, int length, ByteBuffer in, CharBuffer out, CharsetDecoder decoder, boolean endOfInput) throws IOException {
// Append bytes to current buffer
// Previous data maybe partially decoded, this part will appended to previous
in.put(bytes, 0, length);
// To begin processing of data
in.flip();
decodeAndWriteBuffered(writer, in, out, decoder, endOfInput);
}
private static void decodeAndWriteBuffered(Writer writer, ByteBuffer in, CharBuffer out, CharsetDecoder decoder, boolean endOfInput) throws IOException {
// Decode
CoderResult result;
do {
result = decodeAndWrite(writer, in, out, decoder, endOfInput);
// Check that all data are decoded
if (in.hasRemaining()) {
// Move remaining to top of buffer
in.compact();
if (result.isOverflow() && !result.isError()) { // isError covers isMalformed and isUnmappable
// Not all buffer chars decoded, spin it again
// Set to begin
in.flip();
}
} else {
// Clean up buffer
in.clear();
}
} while (in.hasRemaining() && result.isOverflow() && !result.isError()); // isError covers isMalformed and isUnmappable
if (result.isError()) {
if (LOG.isWarnEnabled()) {
// Provide a log warning when the decoding fails (prior to 2.5.19 it failed silently).
// Note: Set FastByteArrayOutputStream's Logger level to error or higher to suppress this log warning.
LOG.warn("Buffer decoding-in-to-out [{}] failed, coderResult [{}]", decoder.charset().name(), result.toString());
}
}
}
private static CoderResult decodeAndWrite(Writer writer, ByteBuffer in, CharBuffer out, CharsetDecoder decoder, boolean endOfInput) throws IOException {
CoderResult result = decoder.decode(in, out, endOfInput);
// To begin processing of decoded data
out.flip();
// Output
writer.write(out.toString());
// Clear output to avoid infinite loops, see WW-4383
out.clear();
return result;
}
public int getSize() {
return size + index;
}
public byte[] toByteArray() {
byte data[] = new byte[getSize()];
int position = 0;
if (buffers != null) {
for (byte[] bytes : buffers) {
System.arraycopy(bytes, 0, data, position, blockSize);
position += blockSize;
}
}
System.arraycopy(buffer, 0, data, position, index);
return data;
}
public String toString() {
return new String(toByteArray());
}
protected void addBuffer() {
if (buffers == null) {
buffers = new LinkedList<>();
}
buffers.addLast(buffer);
buffer = new byte[blockSize];
size += index;
index = 0;
}
public void write(int datum) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
if (index == blockSize) {
addBuffer();
}
buffer[index++] = (byte) datum;
}
public void write(byte data[], int offset, int length) throws IOException {
if (data == null) {
throw new NullPointerException();
}
if (offset < 0 || offset + length > data.length || length < 0) {
throw new IndexOutOfBoundsException();
}
if (closed) {
throw new IOException("Stream closed");
}
if (index + length > blockSize) {
do {
if (index == blockSize) {
addBuffer();
}
int copyLength = blockSize - index;
if (length < copyLength) {
copyLength = length;
}
System.arraycopy(data, offset, buffer, index, copyLength);
offset += copyLength;
index += copyLength;
length -= copyLength;
} while (length > 0);
} else {
System.arraycopy(data, offset, buffer, index, length);
index += length;
}
}
public void close() {
closed = true;
}
}