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