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 java.io.UnsupportedEncodingException;
20  import java.nio.charset.Charset;
21  import java.nio.charset.StandardCharsets;
22  
23  import org.apache.logging.log4j.core.LogEvent;
24  
25  /**
26   * Abstract base class for Layouts that result in a String.
27   * <p>
28   * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
29   * performance: all characters are simply cast to bytes.
30   */
31  /*
32   * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
33   * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
34   */
35  public abstract class AbstractStringLayout extends AbstractLayout<String> {
36  
37      /**
38       * Default length for new StringBuilder instances: {@value} .
39       */
40      protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
41  
42      private final static ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
43  
44      private static final long serialVersionUID = 1L;
45  
46      /**
47       * The charset for the formatted message.
48       */
49      // TODO: Charset is not serializable. Implement read/writeObject() ?
50      private final Charset charset;
51      private final String charsetName;
52      private final boolean useCustomEncoding;
53  
54      protected AbstractStringLayout(final Charset charset) {
55          this(charset, null, null);
56      }
57  
58      protected AbstractStringLayout(final Charset charset, final byte[] header, final byte[] footer) {
59          super(header, footer);
60          this.charset = charset == null ? StandardCharsets.UTF_8 : charset;
61          this.charsetName = this.charset.name();
62          useCustomEncoding = isPreJava8()
63                  && (StandardCharsets.ISO_8859_1.equals(charset) || StandardCharsets.US_ASCII.equals(charset));
64      }
65  
66      // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
67      private static boolean isPreJava8() {
68          final String version = System.getProperty("java.version");
69          final String[] parts = version.split("\\.");
70          try {
71              int major = Integer.parseInt(parts[1]);
72              return major < 8;
73          } catch (Exception ex) {
74              return true;
75          }
76      }
77  
78      /**
79       * Converts a String to a byte[].
80       * 
81       * @param str if null, return null.
82       * @param charset if null, use the default charset.
83       * @return a byte[]
84       */
85      static byte[] toBytes(final String str, final Charset charset) {
86          if (str != null) {
87              if (StandardCharsets.ISO_8859_1.equals(charset)) {
88                  return encodeSingleByteChars(str);
89              }
90              final Charset actual = charset != null ? charset : Charset.defaultCharset();
91              try { // LOG4J2-935: String.getBytes(String) gives better performance
92                  return str.getBytes(actual.name());
93              } catch (UnsupportedEncodingException e) {
94                  return str.getBytes(actual);
95              }
96          }
97          return null;
98      }
99  
100     /**
101      * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
102      * 
103      * @return a {@code StringBuilder}
104      */
105     protected StringBuilder getStringBuilder() {
106         StringBuilder result = threadLocal.get();
107         if (result == null) {
108             result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
109             threadLocal.set(result);
110         }
111         result.setLength(0);
112         return result;
113     }
114 
115     protected byte[] getBytes(final String s) {
116         if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
117             return encodeSingleByteChars(s);
118         }
119         try { // LOG4J2-935: String.getBytes(String) gives better performance
120             return s.getBytes(charsetName);
121         } catch (UnsupportedEncodingException e) {
122             return s.getBytes(charset);
123         }
124     }
125 
126     /**
127      * Encode the specified string by casting each character to a byte.
128      * 
129      * @param s the string to encode
130      * @return the encoded String
131      * @see https://issues.apache.org/jira/browse/LOG4J2-1151
132      */
133     private static byte[] encodeSingleByteChars(String s) {
134         final int length = s.length();
135         final byte[] result = new byte[length];
136         encodeString(s, 0, length, result);
137         return result;
138     }
139 
140     // LOG4J2-1151
141     /*
142      * Implementation note: this is the fast path. If the char array contains only ISO-8859-1 characters, all the work
143      * will be done here.
144      */
145     private static int encodeIsoChars(String charArray, int charIndex, byte[] byteArray, int byteIndex, int length) {
146         int i = 0;
147         for (; i < length; i++) {
148             char c = charArray.charAt(charIndex++);
149             if (c > 255) {
150                 break;
151             }
152             byteArray[(byteIndex++)] = ((byte) c);
153         }
154         return i;
155     }
156 
157     // LOG4J2-1151
158     private static int encodeString(String charArray, int charOffset, int charLength, byte[] byteArray) {
159         int byteOffset = 0;
160         int length = Math.min(charLength, byteArray.length);
161         int charDoneIndex = charOffset + length;
162         while (charOffset < charDoneIndex) {
163             int done = encodeIsoChars(charArray, charOffset, byteArray, byteOffset, length);
164             charOffset += done;
165             byteOffset += done;
166             if (done != length) {
167                 char c = charArray.charAt(charOffset++);
168                 if ((Character.isHighSurrogate(c)) && (charOffset < charDoneIndex)
169                         && (Character.isLowSurrogate(charArray.charAt(charOffset)))) {
170                     if (charLength > byteArray.length) {
171                         charDoneIndex++;
172                         charLength--;
173                     }
174                     charOffset++;
175                 }
176                 byteArray[(byteOffset++)] = '?';
177                 length = Math.min(charDoneIndex - charOffset, byteArray.length - byteOffset);
178             }
179         }
180         return byteOffset;
181     }
182 
183     protected Charset getCharset() {
184         return charset;
185     }
186 
187     /**
188      * @return The default content type for Strings.
189      */
190     @Override
191     public String getContentType() {
192         return "text/plain";
193     }
194 
195     /**
196      * Formats the Log Event as a byte array.
197      *
198      * @param event The Log Event.
199      * @return The formatted event as a byte array.
200      */
201     @Override
202     public byte[] toByteArray(final LogEvent event) {
203         return getBytes(toSerializable(event));
204     }
205 
206 }