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