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        synchronized (destination) {
040            ByteBuffer buffer = destination.getByteBuffer();
041            int offset = 0;
042            do {
043                final int length = Math.min(bytes.length - offset, buffer.remaining());
044                buffer.put(bytes, offset, length);
045                offset += length;
046                if (offset < bytes.length) {
047                    buffer = destination.drain(buffer);
048                }
049            } while (offset < bytes.length);
050        }
051    }
052
053    static void encodeTextWithCopy(final CharsetEncoder charsetEncoder, final CharBuffer charBuf, final ByteBuffer temp,
054            final StringBuilder text, final ByteBufferDestination destination) {
055
056        encodeText(charsetEncoder, charBuf, temp, text, destination);
057        copyDataToDestination(temp, destination);
058    }
059
060    private static void copyDataToDestination(final ByteBuffer temp, final ByteBufferDestination destination) {
061        synchronized (destination) {
062            ByteBuffer destinationBuffer = destination.getByteBuffer();
063            if (destinationBuffer != temp) { // still need to write to the destination
064                temp.flip();
065                if (temp.remaining() > destinationBuffer.remaining()) {
066                    destinationBuffer = destination.drain(destinationBuffer);
067                }
068                destinationBuffer.put(temp);
069                temp.clear();
070            }
071        }
072    }
073
074    static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf, final ByteBuffer byteBuf,
075            final StringBuilder text, final ByteBufferDestination destination) {
076        charsetEncoder.reset();
077        ByteBuffer temp = byteBuf; // may be the destination's buffer or a temporary buffer
078        int start = 0;
079        int todoChars = text.length();
080        boolean endOfInput = true;
081        do {
082            charBuf.clear(); // reset character buffer position to zero, limit to capacity
083            int copied = copy(text, start, charBuf);
084            start += copied;
085            todoChars -= copied;
086            endOfInput = todoChars <= 0;
087
088            charBuf.flip(); // prepare for reading: set limit to position, position to zero
089            temp = encode(charsetEncoder, charBuf, endOfInput, destination, temp);
090        } while (!endOfInput);
091    }
092
093    /**
094     * For testing purposes only.
095     */
096    @Deprecated
097    public static void encodeText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
098            final ByteBufferDestination destination) {
099        synchronized (destination) {
100            charsetEncoder.reset();
101            final ByteBuffer byteBuf = destination.getByteBuffer();
102            encode(charsetEncoder, charBuf, true, destination, byteBuf);
103        }
104    }
105
106    private static ByteBuffer encode(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
107            final boolean endOfInput, final ByteBufferDestination destination, ByteBuffer byteBuf) {
108        try {
109            byteBuf = encodeAsMuchAsPossible(charsetEncoder, charBuf, endOfInput, destination, byteBuf);
110            if (endOfInput) {
111                byteBuf = flushRemainingBytes(charsetEncoder, destination, byteBuf);
112            }
113        } catch (final CharacterCodingException ex) {
114            throw new IllegalStateException(ex);
115        }
116        return byteBuf;
117    }
118
119    private static ByteBuffer encodeAsMuchAsPossible(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
120            final boolean endOfInput, final ByteBufferDestination destination, ByteBuffer temp)
121            throws CharacterCodingException {
122        CoderResult result;
123        do {
124            result = charsetEncoder.encode(charBuf, temp, endOfInput);
125            temp = drainIfByteBufferFull(destination, temp, result);
126        } while (result.isOverflow()); // byte buffer has been drained: retry
127        if (!result.isUnderflow()) { // we should have fully read the char buffer contents
128            result.throwException();
129        }
130        return temp;
131    }
132
133    private static ByteBuffer drainIfByteBufferFull(ByteBufferDestination destination, ByteBuffer temp, CoderResult result) {
134        if (result.isOverflow()) { // byte buffer full
135
136            // SHOULD NOT HAPPEN:
137            // CALLER SHOULD ONLY PASS TEMP ByteBuffer LARGE ENOUGH TO ENCODE ALL CHARACTERS,
138            // AND LOCK ON THE DESTINATION IF THIS IS NOT POSSIBLE
139            ByteBuffer destinationBuffer = destination.getByteBuffer();
140            if (destinationBuffer != temp) {
141                temp.flip();
142                destinationBuffer.put(temp);
143                temp.clear();
144            }
145            // destination consumes contents
146            // and returns byte buffer with more capacity
147            destinationBuffer = destination.drain(destinationBuffer);
148            temp = destinationBuffer;
149        }
150        return temp;
151    }
152
153    private static ByteBuffer flushRemainingBytes(final CharsetEncoder charsetEncoder,
154            final ByteBufferDestination destination, ByteBuffer temp)
155            throws CharacterCodingException {
156        CoderResult result;
157        do {
158            // write any final bytes to the output buffer once the overall input sequence has been read
159            result = charsetEncoder.flush(temp);
160            temp = drainIfByteBufferFull(destination, temp, result);
161        } while (result.isOverflow()); // byte buffer has been drained: retry
162        if (!result.isUnderflow()) { // we should have fully flushed the remaining bytes
163            result.throwException();
164        }
165        return temp;
166    }
167
168    /**
169     * Copies characters from the StringBuilder into the CharBuffer,
170     * starting at the specified offset and ending when either all
171     * characters have been copied or when the CharBuffer is full.
172     *
173     * @return the number of characters that were copied
174     */
175    static int copy(final StringBuilder source, final int offset, final CharBuffer destination) {
176        final int length = Math.min(source.length() - offset, destination.remaining());
177        final char[] array = destination.array();
178        final int start = destination.position();
179        source.getChars(offset, offset + length, array, start);
180        destination.position(start + length);
181        return length;
182    }
183}