View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import java.nio.ByteBuffer;
20  import java.nio.CharBuffer;
21  import java.nio.charset.CharacterCodingException;
22  import java.nio.charset.Charset;
23  import java.nio.charset.CharsetEncoder;
24  import java.nio.charset.CoderResult;
25  
26  /**
27   * Helper class to encode text to binary data without allocating temporary objects.
28   *
29   * @since 2.6
30   */
31  public class TextEncoderHelper {
32  
33      private TextEncoderHelper() {
34      }
35  
36      static void encodeTextFallBack(final Charset charset, final StringBuilder text,
37              final ByteBufferDestination destination) {
38          final byte[] bytes = text.toString().getBytes(charset);
39          destination.writeBytes(bytes, 0, bytes.length);
40      }
41  
42      /**
43       * Converts the specified text to bytes and writes the resulting bytes to the specified destination.
44       * Attempts to postpone synchronizing on the destination as long as possible to minimize lock contention.
45       *
46       * @param charsetEncoder thread-local encoder instance for converting chars to bytes
47       * @param charBuf thread-local text buffer for converting text to bytes
48       * @param byteBuf thread-local buffer to temporarily hold converted bytes before copying them to the destination
49       * @param text the text to convert and write to the destination
50       * @param destination the destination to write the bytes to
51       * @throws CharacterCodingException if conversion failed
52       */
53      static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf, final ByteBuffer byteBuf,
54              final StringBuilder text, final ByteBufferDestination destination)
55              throws CharacterCodingException {
56          charsetEncoder.reset();
57          if (text.length() > charBuf.capacity()) {
58              encodeChunkedText(charsetEncoder, charBuf, byteBuf, text, destination);
59              return;
60          }
61          charBuf.clear();
62          text.getChars(0, text.length(), charBuf.array(), charBuf.arrayOffset());
63          charBuf.limit(text.length());
64          final CoderResult result = charsetEncoder.encode(charBuf, byteBuf, true);
65          writeEncodedText(charsetEncoder, charBuf, byteBuf, destination, result);
66      }
67  
68      /**
69       * This method is called when the CharEncoder has encoded (but not yet flushed) content from the CharBuffer
70       * into the ByteBuffer. A CoderResult of UNDERFLOW means that the contents fit into the ByteBuffer and we can move
71       * on to the next step, flushing. Otherwise, we need to synchronize on the destination, copy the ByteBuffer to the
72       * destination and encode the remainder of the CharBuffer while holding the lock on the destination.
73       *
74       * @since 2.9
75       */
76      private static void writeEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
77              final ByteBuffer byteBuf, final ByteBufferDestination destination, CoderResult result) {
78          if (!result.isUnderflow()) {
79              writeChunkedEncodedText(charsetEncoder, charBuf, destination, byteBuf, result);
80              return;
81          }
82          result = charsetEncoder.flush(byteBuf);
83          if (!result.isUnderflow()) {
84              synchronized (destination) {
85                  flushRemainingBytes(charsetEncoder, destination, byteBuf);
86              }
87              return;
88          }
89          // Thread-safety note: no explicit synchronization on ByteBufferDestination below. This is safe, because
90          // if the byteBuf is actually the destination's buffer, this method call should be protected with
91          // synchronization on the destination object at some level, so the call to destination.getByteBuffer() should
92          // be safe. If the byteBuf is an unrelated buffer, the comparison between the buffers should fail despite
93          // destination.getByteBuffer() is not protected with the synchronization on the destination object.
94          if (byteBuf != destination.getByteBuffer()) {
95              byteBuf.flip();
96              destination.writeBytes(byteBuf);
97              byteBuf.clear();
98          }
99      }
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 }