From 128274a2776e85da27f34704bad5a004d1bff1dd Mon Sep 17 00:00:00 2001
From: Paul Nyheim <paul.nyheim@gmail.com>
Date: Mon, 9 Mar 2015 12:40:02 +0100
Subject: [PATCH] [LOG4J2-926] Implemented "Truncate from end" using the minus
 (-) character as modifier.

---
 .../logging/log4j/core/pattern/FormattingInfo.java | 27 ++++++++--
 .../logging/log4j/core/pattern/PatternParser.java  | 32 +++++++-----
 .../log4j/core/pattern/FormattingInfoTest.java     | 57 ++++++++++++++++++++++
 .../log4j/core/pattern/PatternParserTest.java      | 37 +++++++++++++-
 src/site/xdoc/manual/layouts.xml.vm                | 15 ++++++
 5 files changed, 152 insertions(+), 16 deletions(-)
 create mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/FormattingInfoTest.java

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FormattingInfo.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FormattingInfo.java
index 950a7d5..5d69f9c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FormattingInfo.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FormattingInfo.java
@@ -29,7 +29,7 @@ public final class FormattingInfo {
     /**
      * Default instance.
      */
-    private static final FormattingInfo DEFAULT = new FormattingInfo(false, 0, Integer.MAX_VALUE);
+    private static final FormattingInfo DEFAULT = new FormattingInfo(false, 0, Integer.MAX_VALUE, true);
 
     /**
      * Minimum length.
@@ -47,6 +47,11 @@ public final class FormattingInfo {
     private final boolean leftAlign;
 
     /**
+     * Truncation.
+     */
+    private boolean leftTruncate = true;
+
+    /**
      * Creates new instance.
      *
      * @param leftAlign
@@ -56,10 +61,11 @@ public final class FormattingInfo {
      * @param maxLength
      *            maximum length.
      */
-    public FormattingInfo(final boolean leftAlign, final int minLength, final int maxLength) {
+    public FormattingInfo(final boolean leftAlign, final int minLength, final int maxLength, final boolean leftTruncate) {
         this.leftAlign = leftAlign;
         this.minLength = minLength;
         this.maxLength = maxLength;
+        this.leftTruncate = leftTruncate;
     }
 
     /**
@@ -81,6 +87,15 @@ public final class FormattingInfo {
     }
 
     /**
+     * Determine if left truncated.
+     *
+     * @return true if left truncated.
+     */
+    public boolean isLeftTruncate() {
+		return leftTruncate;
+	}
+
+    /**
      * Get minimum length.
      *
      * @return minimum length.
@@ -110,7 +125,11 @@ public final class FormattingInfo {
         final int rawLength = buffer.length() - fieldStart;
 
         if (rawLength > maxLength) {
-            buffer.delete(fieldStart, buffer.length() - maxLength);
+			if (leftTruncate) {
+				buffer.delete(fieldStart, buffer.length() - maxLength);
+			} else {
+				buffer.delete(fieldStart + maxLength, fieldStart + buffer.length());
+			}
         } else if (rawLength < minLength) {
             if (leftAlign) {
                 final int fieldEnd = buffer.length();
@@ -146,6 +165,8 @@ public final class FormattingInfo {
         sb.append(maxLength);
         sb.append(", minLength=");
         sb.append(minLength);
+        sb.append(", leftTruncate=");
+        sb.append(leftTruncate);
         sb.append(']');
         return sb.toString();
     }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
index c0a7b9e..b8b4db3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
@@ -362,7 +362,7 @@ public final class PatternParser {
                 switch (c) {
                 case '-':
                     formattingInfo = new FormattingInfo(true, formattingInfo.getMinLength(),
-                            formattingInfo.getMaxLength());
+                            formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
                     break;
 
                 case '.':
@@ -373,7 +373,7 @@ public final class PatternParser {
 
                     if (c >= '0' && c <= '9') {
                         formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), c - '0',
-                                formattingInfo.getMaxLength());
+                                formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
                         state = ParserState.MIN_STATE;
                     } else {
                         i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
@@ -394,7 +394,7 @@ public final class PatternParser {
                 if (c >= '0' && c <= '9') {
                     // Multiply the existing value and add the value of the number just encountered.
                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength()
-                            * DECIMAL + c - '0', formattingInfo.getMaxLength());
+                            * DECIMAL + c - '0', formattingInfo.getMaxLength(), formattingInfo.isLeftTruncate());
                 } else if (c == '.') {
                     state = ParserState.DOT_STATE;
                 } else {
@@ -409,16 +409,24 @@ public final class PatternParser {
 
             case DOT_STATE:
                 currentLiteral.append(c);
-
-                if (c >= '0' && c <= '9') {
+                switch (c) {
+                case '-':
                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
-                            c - '0');
-                    state = ParserState.MAX_STATE;
-                } else {
-                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
-                            + "\".");
+                            formattingInfo.getMaxLength(),false);
+                    break;
 
-                    state = ParserState.LITERAL_STATE;
+                default:
+
+	                if (c >= '0' && c <= '9') {
+	                    formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
+	                            c - '0', formattingInfo.isLeftTruncate());
+	                    state = ParserState.MAX_STATE;
+	                } else {
+	                    LOGGER.error("Error occurred in position " + i + ".\n Was expecting digit, instead got char \"" + c
+	                            + "\".");
+
+	                    state = ParserState.LITERAL_STATE;
+	                }
                 }
 
                 break;
@@ -429,7 +437,7 @@ public final class PatternParser {
                 if (c >= '0' && c <= '9') {
                     // Multiply the existing value and add the value of the number just encountered.
                     formattingInfo = new FormattingInfo(formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
-                            formattingInfo.getMaxLength() * DECIMAL + c - '0');
+                            formattingInfo.getMaxLength() * DECIMAL + c - '0', formattingInfo.isLeftTruncate());
                 } else {
                     i = finalizeConverter(c, pattern, i, currentLiteral, formattingInfo, converterRules,
                             patternConverters, formattingInfos, noConsoleNoAnsi, convertBackslashes);
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/FormattingInfoTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/FormattingInfoTest.java
new file mode 100644
index 0000000..e2f9621
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/FormattingInfoTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.pattern;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Testing FormattingInfo.
+ */
+public class FormattingInfoTest {
+
+	@Test
+	public void testFormatTruncateFromBeginning() {
+		StringBuilder message = new StringBuilder("Hello, world");
+
+		FormattingInfo formattingInfo = new FormattingInfo(false, 0, 5, true);
+		formattingInfo.format(0, message);
+
+		assertEquals("world", message.toString());
+	}
+
+	@Test
+	public void testFormatTruncateFromEnd() {
+		StringBuilder message = new StringBuilder("Hello, world");
+
+		FormattingInfo formattingInfo = new FormattingInfo(false, 0, 5, false);
+		formattingInfo.format(0, message);
+
+		assertEquals("Hello", message.toString());
+	}
+
+	@Test
+	public void testFormatTruncateFromEndGivenFieldStart() {
+		StringBuilder message = new StringBuilder("2015-03-09 11:49:28,295; INFO  org.apache.logging.log4j.PatternParserTest");
+
+		FormattingInfo formattingInfo = new FormattingInfo(false, 0, 5, false);
+		formattingInfo.format(31, message);
+
+		assertEquals("2015-03-09 11:49:28,295; INFO  org.a", message.toString());
+	}
+}
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
index 075bb19..9bd118b 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
@@ -54,6 +54,8 @@ public class PatternParserTest {
 
     private static String badPattern = "[%d{yyyyMMdd HH:mm:ss,SSS] %-5p [%c{10}] - %m%n";
     private static String customPattern = "[%d{yyyyMMdd HH:mm:ss,SSS}] %-5p [%-25.25c{1}:%-4L] - %m%n";
+    private static String patternTruncateFromEnd = "%d; %-5p %5.-5c %m%n";
+    private static String patternTruncateFromBeginning = "%d; %-5p %5.5c %m%n";
     private static String nestedPatternHighlight =
             "%highlight{%d{dd MMM yyyy HH:mm:ss,SSS}{GMT+0} [%t] %-5level: %msg%n%throwable}";
 
@@ -102,9 +104,42 @@ public class PatternParserTest {
             formatter.format(event, buf);
         }
         final String str = buf.toString();
-        final String expected = "INFO  [PatternParserTest        :95  ] - Hello, world" + Constants.LINE_SEPARATOR;
+        final String expected = "INFO  [PatternParserTest        :97  ] - Hello, world" + Constants.LINE_SEPARATOR;
         assertTrue("Expected to end with: " + expected + ". Actual: " + str, str.endsWith(expected));
     }
+
+    @Test
+    public void testPatternTruncateFromBeginning() {
+        final List<PatternFormatter> formatters = parser.parse(patternTruncateFromBeginning);
+        assertNotNull(formatters);
+        final LogEvent event = new Log4jLogEvent("org.apache.logging.log4j.PatternParserTest", null,
+            Logger.class.getName(), Level.INFO, new SimpleMessage("Hello, world"), null,
+            null, null, "Thread1", null, System.currentTimeMillis());
+        final StringBuilder buf = new StringBuilder();
+        for (final PatternFormatter formatter : formatters) {
+            formatter.format(event, buf);
+        }
+        final String str = buf.toString();
+        final String expected = "INFO  rTest Hello, world" + Constants.LINE_SEPARATOR;
+        assertTrue("Expected to end with: " + expected + ". Actual: " + str, str.endsWith(expected));
+    }
+
+    @Test
+    public void testPatternTruncateFromEnd() {
+        final List<PatternFormatter> formatters = parser.parse(patternTruncateFromEnd);
+        assertNotNull(formatters);
+        final LogEvent event = new Log4jLogEvent("org.apache.logging.log4j.PatternParserTest", null,
+            Logger.class.getName(), Level.INFO, new SimpleMessage("Hello, world"), null,
+            null, null, "Thread1", null, System.currentTimeMillis());
+        final StringBuilder buf = new StringBuilder();
+        for (final PatternFormatter formatter : formatters) {
+            formatter.format(event, buf);
+        }
+        final String str = buf.toString();
+        final String expected = "INFO  org.a Hello, world" + Constants.LINE_SEPARATOR;
+        assertTrue("Expected to end with: " + expected + ". Actual: " + str, str.endsWith(expected));
+    }
+
     
     @Test
     public void testBadPattern() {
diff --git a/src/site/xdoc/manual/layouts.xml.vm b/src/site/xdoc/manual/layouts.xml.vm
index dd4683a..cedf18e 100644
--- a/src/site/xdoc/manual/layouts.xml.vm
+++ b/src/site/xdoc/manual/layouts.xml.vm
@@ -1147,6 +1147,11 @@ WARN  [main]: Message 2</pre>
             are dropped. This behavior deviates from the printf function in C
             where truncation is done from the end.
           </p>
+          <p>Truncation from the end is possible by appending a minus character
+            right after the period. In that case, if the maximum field width
+            is eight and the data item is ten characters long, then the last
+            two characters of the data item are dropped.
+          </p>
           <p>Below are various format modifier examples for the category
             conversion specifier.
           </p>
@@ -1205,6 +1210,16 @@ WARN  [main]: Message 2</pre>
                 then truncate from the beginning.
               </td>
             </tr>
+            <tr>
+              <td align="center">%-20.-30c</td>
+              <td align="center">true</td>
+              <td align="center">20</td>
+              <td align="center">30</td>
+              <td>Right pad with spaces if the category name is shorter than 20
+                characters. However, if category name is longer than 30 characters,
+                then truncate from the end.
+              </td>
+            </tr>
             <caption align="top">Pattern Converters</caption>
           </table>
           <h4>ANSI Styling on Windows</h4>
-- 
1.8.5.1

