| /* |
| * 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; |
| } |
| } |