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.PropertiesUtil;
26  import org.apache.logging.log4j.util.Strings;
27  
28  import java.io.UnsupportedEncodingException;
29  import java.nio.charset.Charset;
30  import java.nio.charset.StandardCharsets;
31  
32  /**
33   * Abstract base class for Layouts that result in a String.
34   * <p>
35   * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
36   * performance: all characters are simply cast to bytes.
37   */
38  /*
39   * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
40   * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
41   */
42  public abstract class AbstractStringLayout extends AbstractLayout<String> implements StringLayout {
43  
44      public interface Serializer {
45          String toSerializable(final LogEvent event);
46      }
47  
48      /**
49       * Variation of {@link Serializer} that avoids allocating temporary objects.
50       * @since 2.6
51       */
52      public interface Serializer2 {
53          StringBuilder toSerializable(final LogEvent event, final StringBuilder builder);
54      }
55  
56      /**
57       * Default length for new StringBuilder instances: {@value} .
58       */
59      protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
60  
61      protected static final int MAX_STRING_BUILDER_SIZE = Math.max(DEFAULT_STRING_BUILDER_SIZE,
62              size("log4j.layoutStringBuilder.maxSize", 2 * 1024));
63  
64      private static final ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
65  
66      private Encoder<StringBuilder> textEncoder;
67  
68      private static int size(final String property, final int defaultValue) {
69          return PropertiesUtil.getProperties().getIntegerProperty(property, defaultValue);
70      }
71  
72      /**
73       * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
74       *
75       * @return a {@code StringBuilder}
76       */
77      protected static StringBuilder getStringBuilder() {
78          StringBuilder result = threadLocal.get();
79          if (result == null) {
80              result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
81              threadLocal.set(result);
82          }
83          trimToMaxSize(result);
84          result.setLength(0);
85          return result;
86      }
87  
88      protected static void trimToMaxSize(final StringBuilder stringBuilder) {
89          if (stringBuilder.length() > MAX_STRING_BUILDER_SIZE) {
90              stringBuilder.setLength(MAX_STRING_BUILDER_SIZE);
91              stringBuilder.trimToSize();
92          }
93      }
94  
95      // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
96      private static boolean isPreJava8() {
97          final String version = System.getProperty("java.version");
98          final String[] parts = version.split("\\.");
99          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 }