001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.layout;
018
019import java.nio.ByteBuffer;
020import java.nio.CharBuffer;
021import java.nio.charset.CharacterCodingException;
022import java.nio.charset.Charset;
023import java.nio.charset.CharsetEncoder;
024import java.nio.charset.CoderResult;
025
026/**
027 * Helper class to encode text to binary data without allocating temporary objects.
028 *
029 * @since 2.6
030 */
031public class TextEncoderHelper {
032
033    private TextEncoderHelper() {
034    }
035
036    static void encodeTextFallBack(final Charset charset, final StringBuilder text,
037            final ByteBufferDestination destination) {
038        final byte[] bytes = text.toString().getBytes(charset);
039        destination.writeBytes(bytes, 0, bytes.length);
040    }
041
042    /**
043     * Converts the specified text to bytes and writes the resulting bytes to the specified destination.
044     * Attempts to postpone synchronizing on the destination as long as possible to minimize lock contention.
045     *
046     * @param charsetEncoder thread-local encoder instance for converting chars to bytes
047     * @param charBuf thread-local text buffer for converting text to bytes
048     * @param byteBuf thread-local buffer to temporarily hold converted bytes before copying them to the destination
049     * @param text the text to convert and write to the destination
050     * @param destination the destination to write the bytes to
051     * @throws CharacterCodingException if conversion failed
052     */
053    static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf, final ByteBuffer byteBuf,
054            final StringBuilder text, final ByteBufferDestination destination)
055            throws CharacterCodingException {
056        charsetEncoder.reset();
057        if (text.length() > charBuf.capacity()) {
058            encodeChunkedText(charsetEncoder, charBuf, byteBuf, text, destination);
059            return;
060        }
061        charBuf.clear();
062        text.getChars(0, text.length(), charBuf.array(), charBuf.arrayOffset());
063        charBuf.limit(text.length());
064        final CoderResult result = charsetEncoder.encode(charBuf, byteBuf, true);
065        writeEncodedText(charsetEncoder, charBuf, byteBuf, destination, result);
066    }
067
068    /**
069     * This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
070     * into the ByteBuffer. A CoderResult of UNDERFLOW means that the contents fit into the ByteBuffer and we can move
071     * on to the next step, flushing. Otherwise, we need to synchronize on the destination, copy the ByteBuffer to the
072     * destination and encode the remainder of the CharBuffer while holding the lock on the destination.
073     *
074     * @since 2.9
075     */
076    private static void writeEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
077            final ByteBuffer byteBuf, final ByteBufferDestination destination, CoderResult result) {
078        if (!result.isUnderflow()) {
079            writeChunkedEncodedText(charsetEncoder, charBuf, destination, byteBuf, result);
080            return;
081        }
082        result = charsetEncoder.flush(byteBuf);
083        if (!result.isUnderflow()) {
084            synchronized (destination) {
085                flushRemainingBytes(charsetEncoder, destination, byteBuf);
086            }
087            return;
088        }
089        // Thread-safety note: no explicit synchronization on ByteBufferDestination below. This is safe, because
090        // if the byteBuf is actually the destination's buffer, this method call should be protected with
091        // synchronization on the destination object at some level, so the call to destination.getByteBuffer() should
092        // be safe. If the byteBuf is an unrelated buffer, the comparison between the buffers should fail despite
093        // destination.getByteBuffer() is not protected with the synchronization on the destination object.
094        if (byteBuf != destination.getByteBuffer()) {
095            byteBuf.flip();
096            destination.writeBytes(byteBuf);
097            byteBuf.clear();
098        }
099    }
100
101    /**
102     * This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
103     * into the ByteBuffer and we found that the ByteBuffer is too small to hold all the content.
104     * Therefore, we need to synchronize on the destination, copy the ByteBuffer to the
105     * destination and encode the remainder of the CharBuffer while holding the lock on the destination.
106     *
107     * @since 2.9
108     */
109    private static void writeChunkedEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
110            final ByteBufferDestination destination, ByteBuffer byteBuf, final CoderResult result) {
111        synchronized (destination) {
112            byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, true, destination, byteBuf,
113                    result);
114            flushRemainingBytes(charsetEncoder, destination, byteBuf);
115        }
116    }
117
118    /**
119     * This method is called <em>before</em> the CharEncoder has encoded any content from the CharBuffer
120     * into the ByteBuffer, but we have already detected that the CharBuffer contents is too large to fit into the
121     * ByteBuffer. Therefore, at some point we need to synchronize on the destination, copy the ByteBuffer to the
122     * destination and encode the remainder of the CharBuffer while holding the lock on the destination.
123     *
124     * @since 2.9
125     */
126    private static void encodeChunkedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
127            ByteBuffer byteBuf, final StringBuilder text, final ByteBufferDestination destination) {
128
129        // LOG4J2-1874 ByteBuffer, CharBuffer and CharsetEncoder are thread-local, so no need to synchronize while
130        // modifying these objects. Postpone synchronization until accessing the ByteBufferDestination.
131        int start = 0;
132        CoderResult result = CoderResult.UNDERFLOW;
133        boolean endOfInput = false;
134        while (!endOfInput && result.isUnderflow()) {
135            charBuf.clear();
136            final int copied = copy(text, start, charBuf);
137            start += copied;
138            endOfInput = start >= text.length();
139            charBuf.flip();
140            result = charsetEncoder.encode(charBuf, byteBuf, endOfInput);
141        }
142        if (endOfInput) {
143            writeEncodedText(charsetEncoder, charBuf, byteBuf, destination, result);
144            return;
145        }
146        synchronized (destination) {
147            byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, endOfInput, destination, byteBuf,
148                    result);
149            while (!endOfInput) {
150                result = CoderResult.UNDERFLOW;
151                while (!endOfInput && result.isUnderflow()) {
152                    charBuf.clear();
153                    final int copied = copy(text, start, charBuf);
154                    start += copied;
155                    endOfInput = start >= text.length();
156                    charBuf.flip();
157                    result = charsetEncoder.encode(charBuf, byteBuf, endOfInput);
158                }
159                byteBuf = writeAndEncodeAsMuchAsPossible(charsetEncoder, charBuf, endOfInput, destination, byteBuf,
160                        result);
161            }
162            flushRemainingBytes(charsetEncoder, destination, byteBuf);
163        }
164    }
165
166    /**
167     * For testing purposes only.
168     */
169    @Deprecated
170    public static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
171            final ByteBufferDestination destination) {
172        charsetEncoder.reset();
173        synchronized (destination) {
174            ByteBuffer byteBuf = destination.getByteBuffer();
175            byteBuf = encodeAsMuchAsPossible(charsetEncoder, charBuf, true, destination, byteBuf);
176            flushRemainingBytes(charsetEncoder, destination, byteBuf);
177        }
178    }
179
180    /**
181     * Continues to write the contents of the ByteBuffer to the destination and encode more of the CharBuffer text
182     * into the ByteBuffer until the remaining encoded text fit into the ByteBuffer, at which point the ByteBuffer
183     * is returned (without flushing the CharEncoder).
184     * <p>
185     * This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
186     * into the ByteBuffer and we found that the ByteBuffer is too small to hold all the content.
187     * </p><p>
188     * Thread-safety note: This method should be called while synchronizing on the ByteBufferDestination.
189     * </p>
190     * @return the ByteBuffer resulting from draining the temporary ByteBuffer to the destination. In the case
191     *          of a MemoryMappedFile, a remap() may have taken place and the returned ByteBuffer is now the
192     *          MappedBuffer of the newly mapped region of the memory mapped file.
193     * @since 2.9
194     */
195    private static ByteBuffer writeAndEncodeAsMuchAsPossible(final CharsetEncoder charsetEncoder,
196            final CharBuffer charBuf, final boolean endOfInput, final ByteBufferDestination destination,
197            ByteBuffer temp, CoderResult result) {
198        while (true) {
199            temp = drainIfByteBufferFull(destination, temp, result);
200            if (!result.isOverflow()) {
201                break;
202            }
203            result = charsetEncoder.encode(charBuf, temp, endOfInput);
204        }
205        if (!result.isUnderflow()) { // we should have fully read the char buffer contents
206            throwException(result);
207        }
208        return temp;
209    }
210
211    // @since 2.9
212    private static void throwException(final CoderResult result) {
213        try {
214            result.throwException();
215        } catch (final CharacterCodingException e) {
216            throw new IllegalStateException(e);
217        }
218    }
219
220    private static ByteBuffer encodeAsMuchAsPossible(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
221            final boolean endOfInput, final ByteBufferDestination destination, ByteBuffer temp) {
222        CoderResult result;
223        do {
224            result = charsetEncoder.encode(charBuf, temp, endOfInput);
225            temp = drainIfByteBufferFull(destination, temp, result);
226        } while (result.isOverflow()); // byte buffer has been drained: retry
227        if (!result.isUnderflow()) { // we should have fully read the char buffer contents
228            throwException(result);
229        }
230        return temp;
231    }
232
233    /**
234     * If the CoderResult indicates the ByteBuffer is full, synchronize on the destination and write the content
235     * of the ByteBuffer to the destination. If the specified ByteBuffer is owned by the destination, we have
236     * reached the end of a MappedBuffer and we call drain() on the destination to remap().
237     * <p>
238     * If the CoderResult indicates more can be encoded, this method does nothing and returns the temp ByteBuffer.
239     * </p>
240     *
241     * @param destination the destination to write bytes to
242     * @param temp the ByteBuffer containing the encoded bytes. May be a temporary buffer or may be the ByteBuffer of
243     *              the ByteBufferDestination
244     * @param result the CoderResult from the CharsetEncoder
245     * @return the ByteBuffer to encode into for the remainder of the text
246     */
247    private static ByteBuffer drainIfByteBufferFull(final ByteBufferDestination destination, final ByteBuffer temp,
248            final CoderResult result) {
249        if (result.isOverflow()) { // byte buffer full
250            // all callers already synchronize on destination but for safety ensure we are synchronized because
251            // below calls to drain() may cause destination to swap in a new ByteBuffer object
252            synchronized (destination) {
253                final ByteBuffer destinationBuffer = destination.getByteBuffer();
254                if (destinationBuffer != temp) {
255                    temp.flip();
256                    ByteBufferDestinationHelper.writeToUnsynchronized(temp, destination);
257                    temp.clear();
258                    return destination.getByteBuffer();
259                } else {
260                    return destination.drain(destinationBuffer);
261                }
262            }
263        } else {
264            return temp;
265        }
266    }
267
268    private static void flushRemainingBytes(final CharsetEncoder charsetEncoder,
269            final ByteBufferDestination destination, ByteBuffer temp) {
270        CoderResult result;
271        do {
272            // write any final bytes to the output buffer once the overall input sequence has been read
273            result = charsetEncoder.flush(temp);
274            temp = drainIfByteBufferFull(destination, temp, result);
275        } while (result.isOverflow()); // byte buffer has been drained: retry
276        if (!result.isUnderflow()) { // we should have fully flushed the remaining bytes
277            throwException(result);
278        }
279        if (temp.remaining() > 0 && temp != destination.getByteBuffer()) {
280            temp.flip();
281            ByteBufferDestinationHelper.writeToUnsynchronized(temp, destination);
282            temp.clear();
283        }
284    }
285
286    /**
287     * Copies characters from the StringBuilder into the CharBuffer,
288     * starting at the specified offset and ending when either all
289     * characters have been copied or when the CharBuffer is full.
290     *
291     * @return the number of characters that were copied
292     */
293    static int copy(final StringBuilder source, final int offset, final CharBuffer destination) {
294        final int length = Math.min(source.length() - offset, destination.remaining());
295        final char[] array = destination.array();
296        final int start = destination.position();
297        source.getChars(offset, offset + length, array, destination.arrayOffset() + start);
298        destination.position(start + length);
299        return length;
300    }
301}