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.LogEvent;
020import org.apache.logging.log4j.core.StringLayout;
021import org.apache.logging.log4j.core.config.Configuration;
022import org.apache.logging.log4j.core.config.LoggerConfig;
023import org.apache.logging.log4j.core.util.Constants;
024import org.apache.logging.log4j.core.util.StringEncoder;
025import org.apache.logging.log4j.util.Strings;
026
027import java.io.UnsupportedEncodingException;
028import java.nio.charset.Charset;
029import java.nio.charset.StandardCharsets;
030
031/**
032 * Abstract base class for Layouts that result in a String.
033 * <p>
034 * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
035 * performance: all characters are simply cast to bytes.
036 */
037/*
038 * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
039 * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
040 */
041public abstract class AbstractStringLayout extends AbstractLayout<String> implements StringLayout {
042
043    public interface Serializer {
044        String toSerializable(final LogEvent event);
045    }
046
047    /**
048     * Variation of {@link Serializer} that avoids allocating temporary objects.
049     * @since 2.6
050     */
051    public interface Serializer2 {
052        StringBuilder toSerializable(final LogEvent event, final StringBuilder builder);
053    }
054
055    /**
056     * Default length for new StringBuilder instances: {@value} .
057     */
058    protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
059
060    private static final ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
061
062    private Encoder<StringBuilder> textEncoder;
063
064    /**
065     * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
066     *
067     * @return a {@code StringBuilder}
068     */
069    protected static StringBuilder getStringBuilder() {
070        StringBuilder result = threadLocal.get();
071        if (result == null) {
072            result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
073            threadLocal.set(result);
074        }
075        result.setLength(0);
076        return result;
077    }
078
079    // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
080    private static boolean isPreJava8() {
081        final String version = System.getProperty("java.version");
082        final String[] parts = version.split("\\.");
083        try {
084            final int major = Integer.parseInt(parts[1]);
085            return major < 8;
086        } catch (final Exception ex) {
087            return true;
088        }
089    }
090    /**
091     * The charset for the formatted message.
092     */
093    // LOG4J2-1099: charset cannot be final due to serialization needs, so we serialize as charset name instead
094    private transient Charset charset;
095
096    private final String charsetName;
097
098    private final Serializer footerSerializer;
099
100    private final Serializer headerSerializer;
101
102    private final boolean useCustomEncoding;
103
104    protected AbstractStringLayout(final Charset charset) {
105        this(charset, (byte[]) null, (byte[]) null);
106    }
107
108    /**
109     * Builds a new layout.
110     * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
111     *      converted from strings to bytes.
112     * @param header the header bytes
113     * @param footer the footer bytes
114     */
115    protected AbstractStringLayout(final Charset aCharset, final byte[] header, final byte[] footer) {
116        super(null, header, footer);
117        this.headerSerializer = null;
118        this.footerSerializer = null;
119        this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
120        this.charsetName = this.charset.name();
121        useCustomEncoding = isPreJava8()
122                && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
123        textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
124    }
125
126    /**
127     * Builds a new layout.
128     * @param config the configuration
129     * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
130     *      converted from strings to bytes.
131     * @param headerSerializer the header bytes serializer
132     * @param footerSerializer the footer bytes serializer
133     */
134    protected AbstractStringLayout(final Configuration config, final Charset aCharset,
135            final Serializer headerSerializer, final Serializer footerSerializer) {
136        super(config, null, null);
137        this.headerSerializer = headerSerializer;
138        this.footerSerializer = footerSerializer;
139        this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
140        this.charsetName = this.charset.name();
141        useCustomEncoding = isPreJava8()
142                && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
143        textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
144    }
145
146    /**
147     * Returns a {@code Encoder<StringBuilder>} that this Layout implementation can use for encoding log events.
148     *
149     * @return a {@code Encoder<StringBuilder>}
150     */
151    protected Encoder<StringBuilder> getStringBuilderEncoder() {
152        if (textEncoder == null) {
153            textEncoder = new StringBuilderEncoder(getCharset());
154        }
155        return textEncoder;
156    }
157
158    protected byte[] getBytes(final String s) {
159        if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
160            return StringEncoder.encodeSingleByteChars(s);
161        }
162        try { // LOG4J2-935: String.getBytes(String) gives better performance
163            return s.getBytes(charsetName);
164        } catch (final UnsupportedEncodingException e) {
165            return s.getBytes(charset);
166        }
167    }
168
169    @Override
170    public Charset getCharset() {
171        return charset;
172    }
173
174    /**
175     * @return The default content type for Strings.
176     */
177    @Override
178    public String getContentType() {
179        return "text/plain";
180    }
181
182    /**
183     * Returns the footer, if one is available.
184     *
185     * @return A byte array containing the footer.
186     */
187    @Override
188    public byte[] getFooter() {
189        return serializeToBytes(footerSerializer, super.getFooter());
190    }
191
192    public Serializer getFooterSerializer() {
193        return footerSerializer;
194    }
195
196    /**
197     * Returns the header, if one is available.
198     *
199     * @return A byte array containing the header.
200     */
201    @Override
202    public byte[] getHeader() {
203        return serializeToBytes(headerSerializer, super.getHeader());
204    }
205
206    public Serializer getHeaderSerializer() {
207        return headerSerializer;
208    }
209
210    protected byte[] serializeToBytes(final Serializer serializer, final byte[] defaultValue) {
211        final String serializable = serializeToString(serializer);
212        if (serializer == null) {
213            return defaultValue;
214        }
215        return StringEncoder.toBytes(serializable, getCharset());
216    }
217
218    protected String serializeToString(final Serializer serializer) {
219        if (serializer == null) {
220            return null;
221        }
222        final LoggerConfig rootLogger = getConfiguration().getRootLogger();
223        // Using "" for the FQCN, does it matter?
224        final LogEvent logEvent = rootLogger.getLogEventFactory().createEvent(rootLogger.getName(), null, Strings.EMPTY,
225                rootLogger.getLevel(), null, null, null);
226        return serializer.toSerializable(logEvent);
227    }
228
229    /**
230     * Formats the Log Event as a byte array.
231     *
232     * @param event The Log Event.
233     * @return The formatted event as a byte array.
234     */
235    @Override
236    public byte[] toByteArray(final LogEvent event) {
237        return getBytes(toSerializable(event));
238    }
239
240}