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