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