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