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