View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import org.apache.logging.log4j.core.LogEvent;
20  import org.apache.logging.log4j.core.StringLayout;
21  import org.apache.logging.log4j.core.config.Configuration;
22  import org.apache.logging.log4j.core.config.LoggerConfig;
23  import org.apache.logging.log4j.core.util.Constants;
24  import org.apache.logging.log4j.core.util.StringEncoder;
25  import org.apache.logging.log4j.util.Strings;
26  
27  import java.io.UnsupportedEncodingException;
28  import java.nio.charset.Charset;
29  import java.nio.charset.StandardCharsets;
30  
31  /**
32   * Abstract base class for Layouts that result in a String.
33   * <p>
34   * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
35   * performance: all characters are simply cast to bytes.
36   */
37  /*
38   * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
39   * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
40   */
41  public abstract class AbstractStringLayout extends AbstractLayout<String> implements StringLayout {
42  
43      public interface Serializer {
44          String toSerializable(final LogEvent event);
45      }
46  
47      /**
48       * Variation of {@link Serializer} that avoids allocating temporary objects.
49       * @since 2.6
50       */
51      public interface Serializer2 {
52          StringBuilder toSerializable(final LogEvent event, final StringBuilder builder);
53      }
54  
55      /**
56       * Default length for new StringBuilder instances: {@value} .
57       */
58      protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
59  
60      private static final ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
61  
62      private Encoder<StringBuilder> textEncoder;
63  
64      /**
65       * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
66       *
67       * @return a {@code StringBuilder}
68       */
69      protected static StringBuilder getStringBuilder() {
70          StringBuilder result = threadLocal.get();
71          if (result == null) {
72              result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
73              threadLocal.set(result);
74          }
75          result.setLength(0);
76          return result;
77      }
78  
79      // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
80      private static boolean isPreJava8() {
81          final String version = System.getProperty("java.version");
82          final String[] parts = version.split("\\.");
83          try {
84              final int major = Integer.parseInt(parts[1]);
85              return major < 8;
86          } catch (final Exception ex) {
87              return true;
88          }
89      }
90      /**
91       * The charset for the formatted message.
92       */
93      // LOG4J2-1099: charset cannot be final due to serialization needs, so we serialize as charset name instead
94      private transient Charset charset;
95  
96      private final String charsetName;
97  
98      private final Serializer footerSerializer;
99  
100     private final Serializer headerSerializer;
101 
102     private final boolean useCustomEncoding;
103 
104     protected AbstractStringLayout(final Charset charset) {
105         this(charset, (byte[]) null, (byte[]) null);
106     }
107 
108     /**
109      * Builds a new layout.
110      * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
111      *      converted from strings to bytes.
112      * @param header the header bytes
113      * @param footer the footer bytes
114      */
115     protected AbstractStringLayout(final Charset aCharset, final byte[] header, final byte[] footer) {
116         super(null, header, footer);
117         this.headerSerializer = null;
118         this.footerSerializer = null;
119         this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
120         this.charsetName = this.charset.name();
121         useCustomEncoding = isPreJava8()
122                 && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
123         textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
124     }
125 
126     /**
127      * Builds a new layout.
128      * @param config the configuration
129      * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
130      *      converted from strings to bytes.
131      * @param headerSerializer the header bytes serializer
132      * @param footerSerializer the footer bytes serializer
133      */
134     protected AbstractStringLayout(final Configuration config, final Charset aCharset,
135             final Serializer headerSerializer, final Serializer footerSerializer) {
136         super(config, null, null);
137         this.headerSerializer = headerSerializer;
138         this.footerSerializer = footerSerializer;
139         this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
140         this.charsetName = this.charset.name();
141         useCustomEncoding = isPreJava8()
142                 && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
143         textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
144     }
145 
146     /**
147      * Returns a {@code Encoder<StringBuilder>} that this Layout implementation can use for encoding log events.
148      *
149      * @return a {@code Encoder<StringBuilder>}
150      */
151     protected Encoder<StringBuilder> getStringBuilderEncoder() {
152         if (textEncoder == null) {
153             textEncoder = new StringBuilderEncoder(getCharset());
154         }
155         return textEncoder;
156     }
157 
158     protected byte[] getBytes(final String s) {
159         if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
160             return StringEncoder.encodeSingleByteChars(s);
161         }
162         try { // LOG4J2-935: String.getBytes(String) gives better performance
163             return s.getBytes(charsetName);
164         } catch (final UnsupportedEncodingException e) {
165             return s.getBytes(charset);
166         }
167     }
168 
169     @Override
170     public Charset getCharset() {
171         return charset;
172     }
173 
174     /**
175      * @return The default content type for Strings.
176      */
177     @Override
178     public String getContentType() {
179         return "text/plain";
180     }
181 
182     /**
183      * Returns the footer, if one is available.
184      *
185      * @return A byte array containing the footer.
186      */
187     @Override
188     public byte[] getFooter() {
189         return serializeToBytes(footerSerializer, super.getFooter());
190     }
191 
192     public Serializer getFooterSerializer() {
193         return footerSerializer;
194     }
195 
196     /**
197      * Returns the header, if one is available.
198      *
199      * @return A byte array containing the header.
200      */
201     @Override
202     public byte[] getHeader() {
203         return serializeToBytes(headerSerializer, super.getHeader());
204     }
205 
206     public Serializer getHeaderSerializer() {
207         return headerSerializer;
208     }
209 
210     protected byte[] serializeToBytes(final Serializer serializer, byte[] defaultValue) {
211         final String serializable = serializeToString(serializer);
212         if (serializer == null) {
213             return defaultValue;
214         }
215         return StringEncoder.toBytes(serializable, getCharset());
216     }
217 
218     protected String serializeToString(final Serializer serializer) {
219         if (serializer == null) {
220             return null;
221         }
222         final LoggerConfig rootLogger = getConfiguration().getRootLogger();
223         // Using "" for the FQCN, does it matter?
224         final LogEvent logEvent = rootLogger.getLogEventFactory().createEvent(rootLogger.getName(), null, Strings.EMPTY,
225                 rootLogger.getLevel(), null, null, null);
226         return serializer.toSerializable(logEvent);
227     }
228 
229     /**
230      * Formats the Log Event as a byte array.
231      *
232      * @param event The Log Event.
233      * @return The formatted event as a byte array.
234      */
235     @Override
236     public byte[] toByteArray(final LogEvent event) {
237         return getBytes(toSerializable(event));
238     }
239 
240 }