blob: 327386ff690a2ab82d6173e683cf3069bace8ec8 [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.logging.log4j.core.layout;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
/**
* Helper class to encode text to binary data without allocating temporary objects.
*
* @since 2.6
*/
public class TextEncoderHelper {
private TextEncoderHelper() {
}
static void encodeTextFallBack(final Charset charset, final StringBuilder text,
final ByteBufferDestination destination) {
final byte[] bytes = text.toString().getBytes(charset);
destination.writeBytes(bytes, 0, bytes.length);
}
/**
* Converts the specified text to bytes and writes the resulting bytes to the specified destination.
* Attempts to postpone synchronizing on the destination as long as possible to minimize lock contention.
*
* @param charsetEncoder thread-local encoder instance for converting chars to bytes
* @param charBuf thread-local text buffer for converting text to bytes
* @param byteBuf thread-local buffer to temporarily hold converted bytes before copying them to the destination
* @param text the text to convert and write to the destination
* @param destination the destination to write the bytes to
* @throws CharacterCodingException if conversion failed
*/
static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf, final ByteBuffer byteBuf,
final StringBuilder text, final ByteBufferDestination destination)
throws CharacterCodingException {
charsetEncoder.reset();
if (text.length() > charBuf.capacity()) {
encodeChunkedText(charsetEncoder, charBuf, byteBuf, text, destination);
return;
}
charBuf.clear();
text.getChars(0, text.length(), charBuf.array(), charBuf.arrayOffset());
charBuf.limit(text.length());
final CoderResult result = charsetEncoder.encode(charBuf, byteBuf, true);
writeEncodedText(charsetEncoder, charBuf, byteBuf, destination, result);
}
/**
* This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
* into the ByteBuffer. A CoderResult of UNDERFLOW means that the contents fit into the ByteBuffer and we can move
* on to the next step, flushing. Otherwise, we need to synchronize on the destination, copy the ByteBuffer to the
* destination and encode the remainder of the CharBuffer while holding the lock on the destination.
*
* @since 2.9
*/
private static void writeEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
final ByteBuffer byteBuf, final ByteBufferDestination destination, CoderResult result) {
if (!result.isUnderflow()) {
writeChunkedEncodedText(charsetEncoder, charBuf, destination, byteBuf, result);
return;
}
result = charsetEncoder.flush(byteBuf);
if (!result.isUnderflow()) {
synchronized (destination) {
flushRemainingBytes(charsetEncoder, destination, byteBuf);
}
return;
}
// Thread-safety note: no explicit synchronization on ByteBufferDestination below. This is safe, because
// if the byteBuf is actually the destination's buffer, this method call should be protected with
// synchronization on the destination object at some level, so the call to destination.getByteBuffer() should
// be safe. If the byteBuf is an unrelated buffer, the comparison between the buffers should fail despite
// destination.getByteBuffer() is not protected with the synchronization on the destination object.
if (byteBuf != destination.getByteBuffer()) {
byteBuf.flip();
destination.writeBytes(byteBuf);
byteBuf.clear();
}
}
/**
* This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
* into the ByteBuffer and we found that the ByteBuffer is too small to hold all the content.
* Therefore, we need to synchronize on the destination, copy the ByteBuffer to the
* destination and encode the remainder of the CharBuffer while holding the lock on the destination.
*
* @since 2.9
*/
private static void writeChunkedEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
final ByteBufferDestination destination, ByteBuffer byteBuf, final CoderResult result) {
synchronized (destination) {
byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, true, destination, byteBuf,
result);
flushRemainingBytes(charsetEncoder, destination, byteBuf);
}
}
/**
* This method is called <em>before</em> the CharEncoder has encoded any content from the CharBuffer
* into the ByteBuffer, but we have already detected that the CharBuffer contents is too large to fit into the
* ByteBuffer. Therefore, at some point we need to synchronize on the destination, copy the ByteBuffer to the
* destination and encode the remainder of the CharBuffer while holding the lock on the destination.
*
* @since 2.9
*/
private static void encodeChunkedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
ByteBuffer byteBuf, final StringBuilder text, final ByteBufferDestination destination) {
// LOG4J2-1874 ByteBuffer, CharBuffer and CharsetEncoder are thread-local, so no need to synchronize while
// modifying these objects. Postpone synchronization until accessing the ByteBufferDestination.
int start = 0;
CoderResult result = CoderResult.UNDERFLOW;
boolean endOfInput = false;
while (!endOfInput && result.isUnderflow()) {
charBuf.clear();
final int copied = copy(text, start, charBuf);
start += copied;
endOfInput = start >= text.length();
charBuf.flip();
result = charsetEncoder.encode(charBuf, byteBuf, endOfInput);
}
if (endOfInput) {
writeEncodedText(charsetEncoder, charBuf, byteBuf, destination, result);
return;
}
synchronized (destination) {
byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, endOfInput, destination, byteBuf,
result);
while (!endOfInput) {
result = CoderResult.UNDERFLOW;
while (!endOfInput && result.isUnderflow()) {
charBuf.clear();
final int copied = copy(text, start, charBuf);
start += copied;
endOfInput = start >= text.length();
charBuf.flip();
result = charsetEncoder.encode(charBuf, byteBuf, endOfInput);
}
byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, endOfInput, destination, byteBuf,
result);
}
flushRemainingBytes(charsetEncoder, destination, byteBuf);
}
}
/**
* For testing purposes only.
*/
@Deprecated
public static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
final ByteBufferDestination destination) {
charsetEncoder.reset();
synchronized (destination) {
ByteBuffer byteBuf = destination.getByteBuffer();
byteBuf = encodeAsMuchAsPossible(charsetEncoder, charBuf, true, destination, byteBuf);
flushRemainingBytes(charsetEncoder, destination, byteBuf);
}
}
/**
* Continues to write the contents of the ByteBuffer to the destination and encode more of the CharBuffer text
* into the ByteBuffer until the remaining encoded text fit into the ByteBuffer, at which point the ByteBuffer
* is returned (without flushing the CharEncoder).
* <p>
* This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
* into the ByteBuffer and we found that the ByteBuffer is too small to hold all the content.
* </p><p>
* Thread-safety note: This method should be called while synchronizing on the ByteBufferDestination.
* </p>
* @return the ByteBuffer resulting from draining the temporary ByteBuffer to the destination. In the case
* of a MemoryMappedFile, a remap() may have taken place and the returned ByteBuffer is now the
* MappedBuffer of the newly mapped region of the memory mapped file.
* @since 2.9
*/
private static ByteBuffer writeAndEncodeAsMuchAsPossible(final CharsetEncoder charsetEncoder,
final CharBuffer charBuf, final boolean endOfInput, final ByteBufferDestination destination,
ByteBuffer temp, CoderResult result) {
while (true) {
temp = drainIfByteBufferFull(destination, temp, result);
if (!result.isOverflow()) {
break;
}
result = charsetEncoder.encode(charBuf, temp, endOfInput);
}
if (!result.isUnderflow()) { // we should have fully read the char buffer contents
throwException(result);
}
return temp;
}
// @since 2.9
private static void throwException(final CoderResult result) {
try {
result.throwException();
} catch (final CharacterCodingException e) {
throw new IllegalStateException(e);
}
}
private static ByteBuffer encodeAsMuchAsPossible(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
final boolean endOfInput, final ByteBufferDestination destination, ByteBuffer temp) {
CoderResult result;
do {
result = charsetEncoder.encode(charBuf, temp, endOfInput);
temp = drainIfByteBufferFull(destination, temp, result);
} while (result.isOverflow()); // byte buffer has been drained: retry
if (!result.isUnderflow()) { // we should have fully read the char buffer contents
throwException(result);
}
return temp;
}
/**
* If the CoderResult indicates the ByteBuffer is full, synchronize on the destination and write the content
* of the ByteBuffer to the destination. If the specified ByteBuffer is owned by the destination, we have
* reached the end of a MappedBuffer and we call drain() on the destination to remap().
* <p>
* If the CoderResult indicates more can be encoded, this method does nothing and returns the temp ByteBuffer.
* </p>
*
* @param destination the destination to write bytes to
* @param temp the ByteBuffer containing the encoded bytes. May be a temporary buffer or may be the ByteBuffer of
* the ByteBufferDestination
* @param result the CoderResult from the CharsetEncoder
* @return the ByteBuffer to encode into for the remainder of the text
*/
private static ByteBuffer drainIfByteBufferFull(final ByteBufferDestination destination, final ByteBuffer temp,
final CoderResult result) {
if (result.isOverflow()) { // byte buffer full
// all callers already synchronize on destination but for safety ensure we are synchronized because
// below calls to drain() may cause destination to swap in a new ByteBuffer object
synchronized (destination) {
final ByteBuffer destinationBuffer = destination.getByteBuffer();
if (destinationBuffer != temp) {
temp.flip();
ByteBufferDestinationHelper.writeToUnsynchronized(temp, destination);
temp.clear();
return destination.getByteBuffer();
} else {
return destination.drain(destinationBuffer);
}
}
} else {
return temp;
}
}
private static void flushRemainingBytes(final CharsetEncoder charsetEncoder,
final ByteBufferDestination destination, ByteBuffer temp) {
CoderResult result;
do {
// write any final bytes to the output buffer once the overall input sequence has been read
result = charsetEncoder.flush(temp);
temp = drainIfByteBufferFull(destination, temp, result);
} while (result.isOverflow()); // byte buffer has been drained: retry
if (!result.isUnderflow()) { // we should have fully flushed the remaining bytes
throwException(result);
}
if (temp.remaining() > 0 && temp != destination.getByteBuffer()) {
temp.flip();
ByteBufferDestinationHelper.writeToUnsynchronized(temp, destination);
temp.clear();
}
}
/**
* Copies characters from the StringBuilder into the CharBuffer,
* starting at the specified offset and ending when either all
* characters have been copied or when the CharBuffer is full.
*
* @return the number of characters that were copied
*/
static int copy(final StringBuilder source, final int offset, final CharBuffer destination) {
final int length = Math.min(source.length() - offset, destination.remaining());
final char[] array = destination.array();
final int start = destination.position();
source.getChars(offset, offset + length, array, destination.arrayOffset() + start);
destination.position(start + length);
return length;
}
}