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}