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 }