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        return org.apache.logging.log4j.util.Constants.JAVA_MAJOR_VERSION < 8;
135    }
136
137    private static int size(final String property, final int defaultValue) {
138        return PropertiesUtil.getProperties().getIntegerProperty(property, defaultValue);
139    }
140
141    protected static void trimToMaxSize(final StringBuilder stringBuilder) {
142        StringBuilders.trimToMaxSize(stringBuilder, MAX_STRING_BUILDER_SIZE);
143    }
144
145    private Encoder<StringBuilder> textEncoder;
146    /**
147     * The charset for the formatted message.
148     */
149    // LOG4J2-1099: Charset cannot be final due to serialization needs, so we serialize as Charset name instead
150    private transient Charset charset;
151
152    private final String charsetName;
153
154    private final Serializer footerSerializer;
155
156    private final Serializer headerSerializer;
157
158    private final boolean useCustomEncoding;
159
160    protected AbstractStringLayout(final Charset charset) {
161        this(charset, (byte[]) null, (byte[]) null);
162    }
163
164    /**
165     * Builds a new layout.
166     * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
167     *      converted from strings to bytes.
168     * @param header the header bytes
169     * @param footer the footer bytes
170     */
171    protected AbstractStringLayout(final Charset aCharset, final byte[] header, final byte[] footer) {
172        super(null, header, footer);
173        this.headerSerializer = null;
174        this.footerSerializer = null;
175        this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
176        this.charsetName = this.charset.name();
177        useCustomEncoding = isPreJava8()
178                && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
179        textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
180    }
181
182    /**
183     * Builds a new layout.
184     * @param config the configuration
185     * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
186     *      converted from strings to bytes.
187     * @param headerSerializer the header bytes serializer
188     * @param footerSerializer the footer bytes serializer
189     */
190    protected AbstractStringLayout(final Configuration config, final Charset aCharset,
191            final Serializer headerSerializer, final Serializer footerSerializer) {
192        super(config, null, null);
193        this.headerSerializer = headerSerializer;
194        this.footerSerializer = footerSerializer;
195        this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
196        this.charsetName = this.charset.name();
197        useCustomEncoding = isPreJava8()
198                && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
199        textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
200    }
201
202    protected byte[] getBytes(final String s) {
203        if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
204            return StringEncoder.encodeSingleByteChars(s);
205        }
206        try { // LOG4J2-935: String.getBytes(String) gives better performance
207            return s.getBytes(charsetName);
208        } catch (final UnsupportedEncodingException e) {
209            return s.getBytes(charset);
210        }
211    }
212
213    @Override
214    public Charset getCharset() {
215        return charset;
216    }
217
218    /**
219     * @return The default content type for Strings.
220     */
221    @Override
222    public String getContentType() {
223        return "text/plain";
224    }
225
226    /**
227     * Returns the footer, if one is available.
228     *
229     * @return A byte array containing the footer.
230     */
231    @Override
232    public byte[] getFooter() {
233        return serializeToBytes(footerSerializer, super.getFooter());
234    }
235
236    public Serializer getFooterSerializer() {
237        return footerSerializer;
238    }
239
240    /**
241     * Returns the header, if one is available.
242     *
243     * @return A byte array containing the header.
244     */
245    @Override
246    public byte[] getHeader() {
247        return serializeToBytes(headerSerializer, super.getHeader());
248    }
249
250    public Serializer getHeaderSerializer() {
251        return headerSerializer;
252    }
253
254    private DefaultLogEventFactory getLogEventFactory() {
255        return DefaultLogEventFactory.getInstance();
256    }
257
258    /**
259     * Returns a {@code Encoder<StringBuilder>} that this Layout implementation can use for encoding log events.
260     *
261     * @return a {@code Encoder<StringBuilder>}
262     */
263    protected Encoder<StringBuilder> getStringBuilderEncoder() {
264        if (textEncoder == null) {
265            textEncoder = new StringBuilderEncoder(getCharset());
266        }
267        return textEncoder;
268    }
269
270    protected byte[] serializeToBytes(final Serializer serializer, final byte[] defaultValue) {
271        final String serializable = serializeToString(serializer);
272        if (serializer == null) {
273            return defaultValue;
274        }
275        return StringEncoder.toBytes(serializable, getCharset());
276    }
277
278    protected String serializeToString(final Serializer serializer) {
279        if (serializer == null) {
280            return null;
281        }
282        final LoggerConfig rootLogger = getConfiguration().getRootLogger();
283        // Using "" for the FQCN, does it matter?
284        final LogEvent logEvent = getLogEventFactory().createEvent(rootLogger.getName(), null, Strings.EMPTY,
285                rootLogger.getLevel(), null, null, null);
286        return serializer.toSerializable(logEvent);
287    }
288
289    /**
290     * Formats the Log Event as a byte array.
291     *
292     * @param event The Log Event.
293     * @return The formatted event as a byte array.
294     */
295    @Override
296    public byte[] toByteArray(final LogEvent event) {
297        return getBytes(toSerializable(event));
298    }
299
300}