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.IOException;
020import java.io.ObjectInputStream;
021import java.io.ObjectOutputStream;
022import java.io.UnsupportedEncodingException;
023import java.nio.charset.Charset;
024import java.nio.charset.StandardCharsets;
025
026import org.apache.logging.log4j.core.LogEvent;
027import org.apache.logging.log4j.core.StringLayout;
028import org.apache.logging.log4j.core.util.StringEncoder;
029
030/**
031 * Abstract base class for Layouts that result in a String.
032 * <p>
033 * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
034 * performance: all characters are simply cast to bytes.
035 */
036/*
037 * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
038 * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
039 */
040public abstract class AbstractStringLayout extends AbstractLayout<String> implements StringLayout {
041
042    /**
043     * Default length for new StringBuilder instances: {@value} .
044     */
045    protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
046
047    private final static ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
048
049    private static final long serialVersionUID = 1L;
050
051    /**
052     * The charset for the formatted message.
053     */
054    // LOG4J2-1099: charset cannot be final due to serialization needs, so we serialize as charset name instead
055    private transient Charset charset;
056    private final String charsetName;
057    private final boolean useCustomEncoding;
058
059    protected AbstractStringLayout(final Charset charset) {
060        this(charset, null, null);
061    }
062
063    /**
064     * Builds a new layout.
065     * @param charset the charset used to encode the header bytes, footer bytes and anything else that needs to be 
066     *      converted from strings to bytes.
067     * @param header the header bytes
068     * @param footer the footer bytes
069     */
070    protected AbstractStringLayout(final Charset charset, final byte[] header, final byte[] footer) {
071        super(header, footer);
072        this.charset = charset == null ? StandardCharsets.UTF_8 : charset;
073        this.charsetName = this.charset.name();
074        useCustomEncoding = isPreJava8()
075                && (StandardCharsets.ISO_8859_1.equals(charset) || StandardCharsets.US_ASCII.equals(charset));
076    }
077
078    // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
079    private static boolean isPreJava8() {
080        final String version = System.getProperty("java.version");
081        final String[] parts = version.split("\\.");
082        try {
083            int major = Integer.parseInt(parts[1]);
084            return major < 8;
085        } catch (Exception ex) {
086            return true;
087        }
088    }
089
090    private void writeObject(final ObjectOutputStream out) throws IOException {
091        out.defaultWriteObject();
092        out.writeUTF(charset.name());
093    }
094
095    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
096        in.defaultReadObject();
097        final String csName = in.readUTF();
098        charset = Charset.forName(csName);
099    }
100
101    /**
102     * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
103     * 
104     * @return a {@code StringBuilder}
105     */
106    protected StringBuilder getStringBuilder() {
107        StringBuilder result = threadLocal.get();
108        if (result == null) {
109            result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
110            threadLocal.set(result);
111        }
112        result.setLength(0);
113        return result;
114    }
115
116    protected byte[] getBytes(final String s) {
117        if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
118            return StringEncoder.encodeSingleByteChars(s);
119        }
120        try { // LOG4J2-935: String.getBytes(String) gives better performance
121            return s.getBytes(charsetName);
122        } catch (UnsupportedEncodingException e) {
123            return s.getBytes(charset);
124        }
125    }
126
127    @Override
128    public Charset getCharset() {
129        return charset;
130    }
131
132    /**
133     * @return The default content type for Strings.
134     */
135    @Override
136    public String getContentType() {
137        return "text/plain";
138    }
139
140    /**
141     * Formats the Log Event as a byte array.
142     *
143     * @param event The Log Event.
144     * @return The formatted event as a byte array.
145     */
146    @Override
147    public byte[] toByteArray(final LogEvent event) {
148        return getBytes(toSerializable(event));
149    }
150
151}