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 org.apache.logging.log4j.core.util.Constants;
020import org.apache.logging.log4j.status.StatusLogger;
021
022import java.nio.ByteBuffer;
023import java.nio.CharBuffer;
024import java.nio.charset.Charset;
025import java.nio.charset.CharsetEncoder;
026import java.nio.charset.CodingErrorAction;
027import java.util.Objects;
028
029/**
030 * Encoder for StringBuilders that uses ThreadLocals to avoid locking as much as possible.
031 */
032public class StringBuilderEncoder implements Encoder<StringBuilder> {
033
034    private static final int DEFAULT_BYTE_BUFFER_SIZE = 8 * 1024;
035    private final ThreadLocal<CharBuffer> charBufferThreadLocal = new ThreadLocal<>();
036    private final ThreadLocal<ByteBuffer> byteBufferThreadLocal = new ThreadLocal<>();
037    private final ThreadLocal<CharsetEncoder> charsetEncoderThreadLocal = new ThreadLocal<>();
038    private final Charset charset;
039    private final int charBufferSize;
040    private final int byteBufferSize;
041
042    public StringBuilderEncoder(final Charset charset) {
043        this(charset, Constants.ENCODER_CHAR_BUFFER_SIZE, DEFAULT_BYTE_BUFFER_SIZE);
044    }
045
046    public StringBuilderEncoder(final Charset charset, final int charBufferSize, final int byteBufferSize) {
047        this.charBufferSize = charBufferSize;
048        this.byteBufferSize = byteBufferSize;
049        this.charset = Objects.requireNonNull(charset, "charset");
050    }
051
052    @Override
053    public void encode(final StringBuilder source, final ByteBufferDestination destination) {
054        final ByteBuffer temp = getByteBuffer();
055        temp.clear();
056        temp.limit(Math.min(temp.capacity(), destination.getByteBuffer().capacity()));
057        final CharsetEncoder charsetEncoder = getCharsetEncoder();
058
059        final int estimatedBytes = estimateBytes(source.length(), charsetEncoder.maxBytesPerChar());
060        if (temp.remaining() < estimatedBytes) {
061            encodeSynchronized(getCharsetEncoder(), getCharBuffer(), source, destination);
062        } else {
063            encodeWithThreadLocals(charsetEncoder, getCharBuffer(), temp, source, destination);
064        }
065    }
066
067    private void encodeWithThreadLocals(final CharsetEncoder charsetEncoder, final CharBuffer charBuffer,
068            final ByteBuffer temp, final StringBuilder source, final ByteBufferDestination destination) {
069        try {
070            TextEncoderHelper.encodeTextWithCopy(charsetEncoder, charBuffer, temp, source, destination);
071        } catch (final Exception ex) {
072            ex.printStackTrace();
073            logEncodeTextException(ex, source, destination);
074            TextEncoderHelper.encodeTextFallBack(charset, source, destination);
075        }
076    }
077
078    private static int estimateBytes(final int charCount, final float maxBytesPerChar) {
079        return (int) (charCount * (double) maxBytesPerChar);
080    }
081
082    private void encodeSynchronized(final CharsetEncoder charsetEncoder, final CharBuffer charBuffer,
083            final StringBuilder source, final ByteBufferDestination destination) {
084        synchronized (destination) {
085            try {
086                TextEncoderHelper.encodeText(charsetEncoder, charBuffer, destination.getByteBuffer(), source,
087                        destination);
088            } catch (final Exception ex) {
089                logEncodeTextException(ex, source, destination);
090                TextEncoderHelper.encodeTextFallBack(charset, source, destination);
091            }
092        }
093    }
094
095    private CharsetEncoder getCharsetEncoder() {
096        CharsetEncoder result = charsetEncoderThreadLocal.get();
097        if (result == null) {
098            result = charset.newEncoder().onMalformedInput(CodingErrorAction.REPLACE)
099                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
100            charsetEncoderThreadLocal.set(result);
101        }
102        return result;
103    }
104
105
106    private CharBuffer getCharBuffer() {
107        CharBuffer result = charBufferThreadLocal.get();
108        if (result == null) {
109            result = CharBuffer.wrap(new char[charBufferSize]);
110            charBufferThreadLocal.set(result);
111        }
112        return result;
113    }
114
115    private ByteBuffer getByteBuffer() {
116        ByteBuffer result = byteBufferThreadLocal.get();
117        if (result == null) {
118            result = ByteBuffer.wrap(new byte[byteBufferSize]);
119            byteBufferThreadLocal.set(result);
120        }
121        return result;
122    }
123
124    private void logEncodeTextException(final Exception ex, final StringBuilder text,
125                                        final ByteBufferDestination destination) {
126        StatusLogger.getLogger().error("Recovering from StringBuilderEncoder.encode('{}') error: {}", text, ex, ex);
127    }
128}