From 9fd37ac182ca6cadda020b5c3a30c7c0517b8c06 Mon Sep 17 00:00:00 2001 From: jmehrens Date: Wed, 7 Apr 2021 13:01:48 -0500 Subject: [PATCH] CompactFormatter precision and surrogate pairs #528 (#543) Signed-off-by: jmehrens --- doc/release/CHANGES.txt | 7 + .../mail/util/logging/CompactFormatter.java | 125 ++-- .../util/logging/CompactFormatterTest.java | 705 ++++++++++++++++-- 3 files changed, 714 insertions(+), 123 deletions(-) diff --git a/doc/release/CHANGES.txt b/doc/release/CHANGES.txt index b0c568675..a352a1977 100644 --- a/doc/release/CHANGES.txt +++ b/doc/release/CHANGES.txt @@ -20,6 +20,13 @@ Seven digit bug numbers are from the old Sun bug database, which is no longer available. + CHANGES IN THE 2.0.2 RELEASE + ---------------------------- +The following bugs have been fixed in the 2.0.2 release. + +E 528 CompactFormatter precision and surrogate pairs + + CHANGES IN THE 2.0.1 RELEASE ---------------------------- The following bugs have been fixed in the 2.0.1 release. diff --git a/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java b/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java index 7a1a2fb96..50d9a648f 100644 --- a/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java +++ b/mail/src/main/java/com/sun/mail/util/logging/CompactFormatter.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2013, 2019 Jason Mehrens. All rights reserved. + * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2021 Jason Mehrens. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -21,8 +21,9 @@ /** * A plain text formatter that can produce fixed width output. By default this - * formatter will produce output no greater than 160 characters wide plus the - * separator and newline characters. Only specified fields support an + * formatter will produce output no greater than 160 characters + * (Unicode code points) wide plus the separator and newline characters. Only + * specified fields support an * {@linkplain #toAlternate(java.lang.String) alternate} fixed width format. *

* By default each CompactFormatter is initialized using the @@ -33,8 +34,10 @@ * used. *

* * @author Jason Mehrens @@ -112,10 +115,12 @@ public CompactFormatter(final String format) { * and a relevant stack trace element if available; otherwise, an empty * string is used. *
  • {@code message|thrown} The message and the thrown properties joined - * as one parameter. This parameter supports + * as one parameter. Width and precision are by Unicode code points. This + * parameter supports * {@linkplain #toAlternate(java.lang.String) alternate} form.
  • *
  • {@code thrown|message} The thrown and message properties joined as - * one parameter. This parameter supports + * one parameter. Width and precision are by Unicode code points. This + * parameter supports * {@linkplain #toAlternate(java.lang.String) alternate} form.
  • *
  • {@code sequence} the * {@linkplain LogRecord#getSequenceNumber() sequence number} if the given @@ -128,10 +133,12 @@ public CompactFormatter(final String format) { * {@linkplain #formatError(LogRecord) error message} without any stack * trace.
  • *
  • {@code message|error} The message and error properties joined as one - * parameter. This parameter supports + * parameter. Width and precision are by Unicode code points. This parameter + * supports * {@linkplain #toAlternate(java.lang.String) alternate} form.
  • *
  • {@code error|message} The error and message properties joined as one - * parameter. This parameter supports + * parameter. Width and precision are by Unicode code points. This parameter + * supports * {@linkplain #toAlternate(java.lang.String) alternate} form.
  • *
  • {@code backtrace} only the * {@linkplain #formatBackTrace(LogRecord) stack trace} of the given @@ -148,17 +155,19 @@ public CompactFormatter(final String format) { *
      *
    • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.160s%n} *

      - * This prints only 160 characters of the message|thrown ({@code 7$}) using - * the {@linkplain #toAlternate(java.lang.String) alternate} form. The - * separator is not included as part of the total width. + * This prints only 160 characters (Unicode code points) of the + * message|thrown ({@code 7$}) using the + * {@linkplain #toAlternate(java.lang.String) alternate} form. The separator + * is not included as part of the total width. *

            * Encoding failed.|NullPointerException: null String.getBytes(:913)
            * 
      * *
    • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.20s%n} *

      - * This prints only 20 characters of the message|thrown ({@code 7$}) using - * the {@linkplain #toAlternate(java.lang.String) alternate} form. This will + * This prints only 20 characters (Unicode code points) of the + * message|thrown ({@code 7$}) using the + * {@linkplain #toAlternate(java.lang.String) alternate} form. This will * perform a weighted truncation of both the message and thrown properties * of the log record. The separator is not included as part of the total * width. @@ -179,8 +188,9 @@ public CompactFormatter(final String format) { * *

    • {@code com.sun.mail.util.logging.CompactFormatter.format=%4$s: %12$#.160s%n} *

      - * This prints the log level ({@code 4$}) and only 160 characters of the - * message|error ({@code 12$}) using the alternate form. + * This prints the log level ({@code 4$}) and only 160 characters + * (Unicode code points) of the message|error ({@code 12$}) using the + * alternate form. *

            * SEVERE: Unable to send notification.|SocketException: Permission denied: connect
            * 
      @@ -474,12 +484,7 @@ private String findAndFormat(final StackTraceElement[] trace) { */ private String formatStackTraceElement(final StackTraceElement s) { String v = simpleClassName(s.getClassName()); - String result; - if (v != null) { - result = s.toString().replace(s.getClassName(), v); - } else { - result = s.toString(); - } + String result = s.toString().replace(s.getClassName(), v); //If the class name contains the simple file name then remove file name. v = simpleFileName(s.getFileName()); @@ -753,7 +758,7 @@ private static String simpleFileName(String name) { * @return true if null or spaces. */ private static boolean isNullOrSpaces(final String s) { - return s == null || s.trim().length() == 0; + return s == null || s.trim().isEmpty(); } /** @@ -799,41 +804,58 @@ public void formatTo(java.util.Formatter formatter, int flags, r = toAlternate(r); } - if (precision <= 0) { - precision = Integer.MAX_VALUE; - } + int lc = 0; + int rc = 0; + if (precision >= 0) { + lc = minCodePointCount(l, precision); + rc = minCodePointCount(r, precision); - int fence = Math.min(l.length(), precision); - if (fence > (precision >> 1)) { - fence = Math.max(fence - r.length(), fence >> 1); - } - - if (fence > 0) { - if (fence > l.length() - && Character.isHighSurrogate(l.charAt(fence - 1))) { - --fence; + if (lc > (precision >> 1)) { + lc = Math.max(lc - rc, lc >> 1); } - l = l.substring(0, fence); + rc = Math.min(precision - lc, rc); + + l = l.substring(0, l.offsetByCodePoints(0, lc)); + r = r.substring(0, r.offsetByCodePoints(0, rc)); } - r = r.substring(0, Math.min(precision - fence, r.length())); if (width > 0) { + if (precision < 0) { + lc = minCodePointCount(l, width); + rc = minCodePointCount(r, width); + } + final int half = width >> 1; - if (l.length() < half) { - l = pad(flags, l, half); + if (lc < half) { + l = pad(flags, l, half - lc); } - if (r.length() < half) { - r = pad(flags, r, half); + if (rc < half) { + r = pad(flags, r, half - rc); } } - Object[] empty = Collections.emptySet().toArray(); - formatter.format(l, empty); - if (l.length() != 0 && r.length() != 0) { - formatter.format("|", empty); + formatter.format(l); + if (!l.isEmpty() && !r.isEmpty()) { + formatter.format("|"); + } + formatter.format(r); + } + + /** + * Counts the number code points with an upper bound. + * + * @param s the string to count, never null. + * @param limit the max number of code points needed. + * @return the number of code points, never greater than the limit. + */ + private int minCodePointCount(String s, final int limit) { + //assert limit >= 0 : limit; + final int len = s.length(); + if ((len - limit) >= limit) { + return limit; } - formatter.format(r, empty); + return Math.min(s.codePointCount(0, len), limit); } /** @@ -841,12 +863,13 @@ public void formatTo(java.util.Formatter formatter, int flags, * * @param flags the formatter flags. * @param s the string to pad. - * @param length the final string length. + * @param padding the number of spaces to add. * @return the padded string. */ - private String pad(int flags, String s, int length) { - final int padding = length - s.length(); - final StringBuilder b = new StringBuilder(length); + private String pad(int flags, String s, int padding) { + //assert padding >= 0 : padding; + final StringBuilder b = new StringBuilder( + Math.max(s.length() + padding, padding)); if ((flags & java.util.FormattableFlags.LEFT_JUSTIFY) == java.util.FormattableFlags.LEFT_JUSTIFY) { for (int i = 0; i < padding; ++i) { diff --git a/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java b/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java index 41f57bd31..f937234d8 100644 --- a/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java +++ b/mail/src/test/java/com/sun/mail/util/logging/CompactFormatterTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.net.SocketException; import java.util.*; +import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; @@ -153,14 +154,16 @@ public void testNewFormatterWithPattern() { @Test public void testNewFormatterNullPattern() { CompactFormatter cf = new CompactFormatter((String) null); - assertEquals(CompactFormatter.class, cf.getClass()); + LogRecord r = new LogRecord(Level.SEVERE, "message"); + String result = cf.format(r); + assertTrue(result, result.contains(r.getMessage())); } @Test public void testGetHeadAndGetTail() { CompactFormatter cf = new CompactFormatter(); - assertEquals("", cf.getHead(null)); - assertEquals("", cf.getTail(null)); + assertEquals("", cf.getHead((Handler) null)); + assertEquals("", cf.getTail((Handler) null)); } @Test @@ -189,7 +192,8 @@ public void testFormatWithMessageLeftJustifiedPad() { CompactFormatter cf = new CompactFormatter("%7$#-12.6s%n"); String result = cf.format(record); assertTrue(result, result.startsWith("messag|")); - assertTrue(result, result.contains("\u0020\u0020\u0020\u0020\u0020\u0020")); + assertTrue(result, result.contains("\u0020\u0020\u0020" + + "\u0020\u0020\u0020")); assertTrue(result, result.endsWith(LINE_SEP)); } @@ -211,25 +215,457 @@ public void testFormatWithMessageWidthOverPrecision() { assertEquals("mes\u0020\u0020\u0020|Thr\u0020\u0020\u0020", result); } + @Test + public void testFormatWithMessageNoSurrogateWidthLess() { + testFormatWithMessageWidthLess("message"); + } + + @Test + public void testFormatWithMessageSurrogateWidthLess() { + testFormatWithMessageWidthLess( + "m\ud801\udc00ss\ud801\udc00g\ud801\udc00"); + } + + private void testFormatWithMessageWidthLess(String message) { + assertEquals(7, message.codePointCount(0, message.length())); + LogRecord record = new LogRecord(Level.SEVERE, message); + Throwable t = new Throwable(record.getMessage()); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + t.setStackTrace(new StackTraceElement[]{frame}); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%7$#90s"); + String result = cf.format(record); + final int fence = result.indexOf('|'); + assertEquals(90 / 2, result.codePointCount(0, fence)); + assertEquals(90 / 2, result.codePointCount(fence + 1, result.length())); + assertTrue(result, result.startsWith(record.getMessage())); + assertTrue(result, result.charAt(fence - 1) == '\u0020'); + assertTrue(result, result.endsWith("\u0020")); + String l = result.substring(0, fence); + String r = result.substring(fence + 1); + String lt = l.trim(); + String rt = r.trim(); + assertTrue(r, r.startsWith(t.getClass().getSimpleName())); + assertEquals(l.codePointCount(0, l.length()), + lt.codePointCount(0, lt.length()) + 38); + assertEquals(r.codePointCount(0, r.length()), + rt.codePointCount(0, rt.length()) + 5); + } + + @Test + public void testFormatWithMessageNoSurrogateWidthEqual() { + testFormatWithMessageWidthEqual("message"); + } + + @Test + public void testFormatWithMessageSurrogateWidthEqual() { + testFormatWithMessageWidthEqual( + "m\ud801\udc00ss\ud801\udc00g\ud801\udc00"); + } + + private void testFormatWithMessageWidthEqual(String message) { + assertEquals(7, message.codePointCount(0, message.length())); + LogRecord record = new LogRecord(Level.SEVERE, message); + Throwable t = new Throwable(message); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + t.setStackTrace(new StackTraceElement[]{frame}); + record.setThrown(t); + + CompactFormatter cf = new CompactFormatter("%7$#80s"); + record.setMessage(cf.formatThrown(record)); + + String result = cf.format(record); + final int fence = result.indexOf('|'); + assertEquals(80 / 2, result.codePointCount(0, fence)); + assertEquals(80 / 2, result.codePointCount(fence + 1, result.length())); + assertTrue(result, result.startsWith(record.getMessage())); + String l = result.substring(0, fence); + String r = result.substring(fence + 1); + String lt = l.trim(); + String rt = r.trim(); + assertTrue(r, r.startsWith(t.getClass().getSimpleName())); + assertEquals(l.codePointCount(0, l.length()), + lt.codePointCount(0, lt.length())); + assertEquals(r.codePointCount(0, r.length()), + rt.codePointCount(0, rt.length())); + } + + @Test + public void testFormatWithMessageNoSurrogateWidthMore() { + testFormatWithMessageWidthMore("message" + "message"); + } + + @Test + public void testFormatWithMessageSurrogateWidthMore() { + testFormatWithMessageWidthMore( + "m\ud801\udc00ss\ud801\udc00g\ud801\udc00" + + "m\ud801\udc00ss\ud801\udc00g\ud801\udc00"); + } + + private void testFormatWithMessageWidthMore(String message) { + assertEquals(14, message.codePointCount(0, message.length())); + LogRecord record = new LogRecord(Level.SEVERE, message); + Throwable t = new Throwable(message); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + t.setStackTrace(new StackTraceElement[]{frame}); + record.setThrown(t); + + CompactFormatter cf = new CompactFormatter("%7$#90s"); + record.setMessage(cf.formatThrown(record)); + + String result = cf.format(record); + final int fence = result.indexOf('|'); + assertEquals(94 / 2, result.codePointCount(0, fence)); + assertEquals(94 / 2, result.codePointCount(fence + 1, result.length())); + assertTrue(result, result.startsWith(record.getMessage())); + String l = result.substring(0, fence); + String r = result.substring(fence + 1); + String lt = l.trim(); + String rt = r.trim(); + assertTrue(r, r.startsWith(t.getClass().getSimpleName())); + assertEquals(l.codePointCount(0, l.length()), + lt.codePointCount(0, lt.length())); + assertEquals(r.codePointCount(0, r.length()), + rt.codePointCount(0, rt.length())); + } + + @Test + public void testFormatWithMessageNoSurrogateWidthHuge() { + testFormatWithMessageWidthHuge(rpad("a", 160 * 2, "b")); + testFormatWithMessageWidthHuge(rpad("a", 160 * 3, "b")); + } + + @Test + public void testFormatWithMessageSurrogateWidthHuge() { + testFormatWithMessageWidthHuge(rpad("a", 160 * 2, "\ud801\udc00")); + testFormatWithMessageWidthHuge(rpad("a", 160 * 3, "\ud801\udc00")); + } + + private void testFormatWithMessageWidthHuge(String message) { + LogRecord record = new LogRecord(Level.SEVERE, message); + Throwable t = new Throwable(message); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + t.setStackTrace(new StackTraceElement[]{frame}); + record.setThrown(t); + + CompactFormatter cf = new CompactFormatter("%7$#160s"); + record.setMessage(cf.formatThrown(record)); + + String result = cf.format(record); + final int fence = result.indexOf('|'); + assertTrue(result.codePointCount(0, fence) > 160 / 2); + assertTrue(result.codePointCount(fence + 1, result.length()) > 160 / 2); + assertTrue(result, result.startsWith(record.getMessage())); + String l = result.substring(0, fence); + String r = result.substring(fence + 1); + String lt = l.trim(); + String rt = r.trim(); + assertTrue(r, r.startsWith(t.getClass().getSimpleName())); + assertEquals(l.codePointCount(0, l.length()), + lt.codePointCount(0, lt.length())); + assertEquals(r.codePointCount(0, r.length()), + rt.codePointCount(0, rt.length())); + } + @Test public void testFormatWithMessageEmpty() { LogRecord record = new LogRecord(Level.SEVERE, ""); CompactFormatter cf = new CompactFormatter(); String result = cf.format(record); - assertEquals(result, LINE_SEP); + assertEquals(LINE_SEP, result); } @Test - public void testFormatMessageSurrogate() { + public void testFormatMessageSurrogateEvenLess() { LogRecord record = new LogRecord(Level.SEVERE, "a\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"); - record.setThrown(new Throwable("thrown")); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); assertTrue(result, result.startsWith("a\ud801\udc00")); + assertTrue(result, result.endsWith("|Thro" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateEvenLess() { + LogRecord record = new LogRecord(Level.SEVERE, + "abbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("ab")); + assertTrue(result, result.endsWith("|Thro" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateOddEqual() { + LogRecord record = new LogRecord(Level.SEVERE, + "abbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.5s%n"); + String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("ab")); assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); } + @Test + public void testFormatMessageSurrogateOddLess() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.7s%n"); + String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); + assertTrue(result, result.startsWith("a\ud801\udc00")); + assertTrue(result, result.endsWith("|Throw" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateOddEqual() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.5s%n"); + String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateOddMore() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.7s%n"); + String result = cf.format(record); + + assertEquals(8, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00\ud801\udc00")); + assertTrue(result, result.endsWith("|Thro" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateEvenEqual() { + LogRecord record = new LogRecord(Level.SEVERE, + "abbbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(6, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("abb")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateEvenEqual() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(6, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateEvenMore() { + LogRecord record = new LogRecord(Level.SEVERE, "abbbbbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(8, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("abb")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateEvenMore() { + LogRecord record = new LogRecord(Level.SEVERE, + "a\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(8, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateEvenHuge() { + int cap = 202; + LogRecord record = new LogRecord(Level.SEVERE, + rpad("a", cap, "b")); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(cap, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("abb")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateEvenHuge() { + int cap = 202; + LogRecord record = new LogRecord(Level.SEVERE, + rpad("a", cap, "\ud801\udc00")); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.6s%n"); + String result = cf.format(record); + + assertEquals(cap, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateOddHuge() { + int cap = 201; + LogRecord record = new LogRecord(Level.SEVERE, + rpad("a", cap, "b")); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.5s%n"); + String result = cf.format(record); + + assertEquals(cap, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("ab")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateOddLess() { + LogRecord record = new LogRecord(Level.SEVERE, + "abbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.7s%n"); + String result = cf.format(record); + + assertEquals(5, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("ab")); + assertTrue(result, result.endsWith("|Throw" + LINE_SEP)); + } + + @Test + public void testFormatMessageNoSurrogateOddMore() { + LogRecord record = new LogRecord(Level.SEVERE, "abbbbbbb"); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.7s%n"); + String result = cf.format(record); + + assertEquals(8, m.codePointCount(0, m.length())); + assertEquals(3, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("abb")); + assertTrue(result, result.endsWith("|Thro" + LINE_SEP)); + } + + @Test + public void testFormatMessageSurrogateOddHuge() { + int cap = 201; + LogRecord record = new LogRecord(Level.SEVERE, + rpad("a", cap, "\ud801\udc00")); + String m = record.getMessage(); + record.setThrown(new Throwable(m)); + CompactFormatter cf = new CompactFormatter("%7$#.5s%n"); + String result = cf.format(record); + + assertEquals(cap, m.codePointCount(0, m.length())); + assertEquals(2, result.codePointCount(0, result.indexOf('|'))); + assertTrue(result, result.startsWith("a\ud801\udc00")); + assertTrue(result, result.endsWith("|Thr" + LINE_SEP)); + } + + @Test(expected = IllegalArgumentException.class) + public void testFormatMessageZeroWidth() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#0s%n"); + assertNotNull(cf.format(record)); + //Zero width is not allowed + //java.util.FormatFlagsConversionMismatchException: + //Conversion = s, Flags = 0 + } + + @Test + public void testFormatMessageZeroPrecision() { + LogRecord record = new LogRecord(Level.SEVERE, "message"); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#.0s%n"); + String result = cf.format(record); + assertEquals(LINE_SEP, result); + } + + @Test + public void testFormatMessageNullRecordMessagePrecision() { + LogRecord record = new LogRecord(Level.SEVERE, (String) null); + record.setThrown(new Throwable("thrown")); + CompactFormatter cf = new CompactFormatter("%7$#.0s%n"); + String result = cf.format(record); + assertEquals(LINE_SEP, result); + } + + @Test + public void testFormatMessageNullRecordMessage() { + LogRecord record = new LogRecord(Level.SEVERE, (String) null); + Throwable t = new Throwable(record.getMessage()); + StackTraceElement frame = new StackTraceElement("java.lang.String", + "getBytes", "String.java", 913); + t.setStackTrace(new StackTraceElement[]{frame}); + record.setThrown(t); + CompactFormatter cf = new CompactFormatter("%7$#s"); + String result = cf.format(record); + assertEquals("null|Throwable String.getBytes(:913)", result); + } + @Test public void testFormatWithMessageAndThrownLeftToRight() { LogRecord record = new LogRecord(Level.SEVERE, "message"); @@ -264,8 +700,7 @@ private void testFormatWithThrown(String fmt) { @Test(expected = NullPointerException.class) public void testFormatMessageNull() { CompactFormatter cf = new CompactFormatter(); - cf.formatMessage((LogRecord) null); - fail(cf.toString()); + assertNotNull(cf.formatMessage((LogRecord) null)); } @Test @@ -326,8 +761,10 @@ private void testFormatMessage_LogRecord(String fmt) { public void testFormatMessage_LogRecordEvil() { LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(createEvilThrowable()); - CompactFormatter cf = new CompactFormatter(); - cf.formatMessage(record); + CompactFormatter cf = new CompactFormatter("%5$s"); + String result = cf.formatMessage(record); + assertNotNull(result); + assertEquals(result, cf.format(record)); } @Test @@ -763,34 +1200,53 @@ public void testFormatMessage_ThrowableNullMessage() { @Test(timeout = 30000) public void testFormatMessage_ThrowableEvil() { - CompactFormatter cf = new CompactFormatter(); - cf.formatMessage(createEvilThrowable()); + CompactFormatter cf = new CompactFormatter("%6$s"); + LogRecord r = new LogRecord(Level.SEVERE, ""); + r.setThrown(createEvilThrowable()); + String result = cf.formatMessage(r.getThrown()); + assertNotNull(result); + assertTrue(cf.format(r).contains(result)); } @Test public void testFormatLevel() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%4$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); String result = cf.formatLevel(record); assertEquals(record.getLevel().getLocalizedName(), result); + assertEquals(result, cf.format(record)); } @Test(expected = NullPointerException.class) public void testFormatLevelNull() { - CompactFormatter cf = new CompactFormatter(); - cf.formatLevel((LogRecord) null); - fail(cf.toString()); + CompactFormatter cf = new CompactFormatter("%4$s"); + assertNotNull(cf.formatLevel((LogRecord) null)); } @Test public void testFormatLogger() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%3$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setSourceMethodName(null); record.setSourceClassName(null); record.setLoggerName(Object.class.getName()); String result = cf.formatLoggerName(record); + assertNotNull(result); assertEquals(Object.class.getSimpleName(), result); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatLoggerSurrogate() { + CompactFormatter cf = new CompactFormatter("%3$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setSourceMethodName(null); + record.setSourceClassName(null); + record.setLoggerName("mail.Foo\ud801\udc00$\ud801\udc00Holder"); + String result = cf.formatLoggerName(record); + assertNotNull(result); + assertEquals("\ud801\udc00Holder", result); + assertEquals(result, cf.format(record)); } @Test @@ -819,23 +1275,20 @@ public void testFormatLoggerColonSpace() { } private void testFormatLoggerNonClassName(String name) { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%3$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setSourceMethodName(null); record.setSourceClassName(null); record.setLoggerName(name); String result = cf.formatLoggerName(record); assertEquals(name, result); - - cf = new CompactFormatter("%3$s"); assertEquals(result, cf.format(record)); } @Test(expected = NullPointerException.class) public void testFormatLoggerNull() { - CompactFormatter cf = new CompactFormatter(); - cf.formatLoggerName((LogRecord) null); - fail(cf.toString()); + CompactFormatter cf = new CompactFormatter("%3$s"); + assertNotNull(cf.formatLoggerName((LogRecord) null)); } @Test @@ -863,8 +1316,7 @@ public void testFormatMillisByParts() { String p = "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp"; CompactFormatter cf = new CompactFormatter(p); LogRecord r = new LogRecord(Level.SEVERE, ""); - assertEquals(String.format(p, r.getMillis()), - cf.format(r)); + assertEquals(String.format(p, r.getMillis()), cf.format(r)); } @Test @@ -883,8 +1335,7 @@ public void testFormatMillisAsLong() { String p = "%1$tQ"; CompactFormatter cf = new CompactFormatter(p); LogRecord r = new LogRecord(Level.SEVERE, ""); - assertEquals(String.format(p, r.getMillis()), - cf.format(r)); + assertEquals(String.format(p, r.getMillis()), cf.format(r)); } @Test @@ -907,7 +1358,7 @@ public void testFormatZoneDateTime() throws Exception { @Test(expected = NullPointerException.class) public void testFormatNull() { CompactFormatter cf = new CompactFormatter(); - cf.format((LogRecord) null); + assertNotNull(cf.format((LogRecord) null)); } @Test @@ -922,8 +1373,8 @@ public void testFormatResourceBundleName() { @Test public void testFormatKey() { CompactFormatter cf = new CompactFormatter("%16$s"); - LogRecord r = new LogRecord(Level.SEVERE, "message {0}"); - r.setParameters(new Object[]{2}); + LogRecord r = new LogRecord(Level.SEVERE, "message {0}{1}{2}"); + r.setParameters(new Object[]{null, "", cf}); String output = cf.format(r); assertEquals(r.getMessage(), output); assertFalse(output.equals(cf.formatMessage(r))); @@ -938,63 +1389,80 @@ public void testFormatSequence() { assertEquals(expect, output); } + @Test + public void testFormatParameterSimpleName() { + CompactFormatter cf = new CompactFormatter("%5$s"); + LogRecord r = new LogRecord(Level.SEVERE, cf.getClass().getName() + + " {0},{1},{2}"); + r.setParameters(new Object[]{null, "", cf}); + String output = cf.format(r); + assertTrue(output, output.startsWith( + "CompactFormatter null,,CompactFormatter@")); + } + @Test public void testFormatSourceByLogger() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%2$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setSourceMethodName(null); record.setSourceClassName(null); record.setLoggerName(Object.class.getName()); String result = cf.formatSource(record); assertEquals(Object.class.getSimpleName(), result); + assertEquals(result, cf.format(record)); } @Test(expected = NullPointerException.class) public void testFormatSourceNull() { - CompactFormatter cf = new CompactFormatter(); - cf.formatSource((LogRecord) null); - fail(cf.toString()); + CompactFormatter cf = new CompactFormatter("%2$s"); + assertNotNull(cf.formatSource((LogRecord) null)); } @Test public void testFormatSourceByClass() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%2$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setSourceMethodName(null); record.setSourceClassName(Object.class.getName()); record.setLoggerName(""); String result = cf.formatSource(record); assertEquals(Object.class.getSimpleName(), result); + assertEquals(result, cf.format(record)); } @Test public void testFormatSourceByClassAndMethod() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%2$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setSourceMethodName("method"); record.setSourceClassName(Object.class.getName()); record.setLoggerName(""); String result = cf.formatSource(record); - assertFalse(result, record.getSourceClassName().equals(record.getSourceMethodName())); + assertFalse(result, record.getSourceClassName() + .equals(record.getSourceMethodName())); assertTrue(result, result.startsWith(Object.class.getSimpleName())); assertTrue(result, result.endsWith(record.getSourceMethodName())); + assertEquals(result, cf.format(record)); } @Test public void testFormatThrownNullThrown() { - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%6$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); String result = cf.formatThrown(record); assertTrue(result, result.startsWith(cf.formatMessage(record.getThrown()))); assertTrue(result, result.endsWith(cf.formatBackTrace(record))); + assertEquals(result, cf.format(record)); } @Test(timeout = 30000) public void testFormatThrownEvilThrown() { LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(createEvilThrowable()); - CompactFormatter cf = new CompactFormatter(); - cf.formatThrown(record); + CompactFormatter cf = new CompactFormatter("%6$s"); + String result = cf.formatThrown(record); + assertNotNull(result); + assertEquals(result, cf.format(record)); } @Test @@ -1003,13 +1471,32 @@ public void testFormatThrown() { e = new Exception(e.toString(), e); assertNotNull(e.getMessage(), e.getMessage()); - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%6$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(e); String result = cf.formatThrown(record); assertTrue(result, result.startsWith(e.getCause().getClass().getSimpleName())); assertTrue(result, result.contains(cf.formatMessage(record.getThrown()))); assertTrue(result, result.endsWith(cf.formatBackTrace(record))); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatThrownEmptyEmpty() { + //E[0] -> IOE[0] + Exception e = new IOException("Fake I/O"); + e.setStackTrace(new StackTraceElement[0]); + e = new Exception(e.toString(), e); + e.setStackTrace(new StackTraceElement[0]); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter("%6$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + + String result = cf.formatThrown(record); + assertEquals("IOException: Fake I/O", result); + assertEquals(result, cf.format(record)); } @Test @@ -1039,8 +1526,7 @@ public void testInheritsFormatMessage() { @Test(expected = NullPointerException.class) public void testFormatThrownNullRecord() { CompactFormatter cf = new CompactFormatter(); - cf.formatThrown((LogRecord) null); - fail(cf.toString()); + assertNotNull(cf.formatThrown((LogRecord) null)); } @Test @@ -1066,7 +1552,7 @@ public void testFormatThreadID() { @Test(expected = NullPointerException.class) public void testFormatThreadIDNull() { CompactFormatter cf = new CompactFormatter(); - cf.formatThreadID((LogRecord) null); + assertNotNull(cf.formatThreadID((LogRecord) null)); } @Test @@ -1088,12 +1574,13 @@ public void testFormatError() { assertTrue(output.startsWith(record.getThrown() .getClass().getSimpleName())); assertTrue(output.endsWith(record.getThrown().getMessage())); + assertEquals(output, cf.formatError(record)); } @Test(expected = NullPointerException.class) public void testFormatErrorNull() { - CompactFormatter cf = new CompactFormatter(); - cf.formatError((LogRecord) null); + CompactFormatter cf = new CompactFormatter("%11$s"); + assertNotNull(cf.formatError((LogRecord) null)); } @Test @@ -1103,6 +1590,7 @@ public void testFormatErrorNullMessage() { record.setThrown(new Throwable()); String output = cf.format(record); assertNotNull(output); + assertEquals(output, cf.formatError(record)); } @Test(expected = NullPointerException.class) @@ -1131,6 +1619,7 @@ public void testFormatMessageError() { assertTrue(output, t > f); assertTrue(output, f < m); assertTrue(output, output.startsWith(record.getMessage())); + assertTrue(output, output.endsWith(cf.formatError(record))); } @Test @@ -1150,6 +1639,7 @@ public void testFormatErrorMessage() { assertTrue(output, t < f); assertTrue(output, f > m); assertTrue(output, output.endsWith(record.getMessage())); + assertTrue(output, output.startsWith(cf.formatError(record))); } @Test(expected = NullPointerException.class) @@ -1249,14 +1739,12 @@ public void testFormatExample5() { assertNotNull(output); } - @Test + @Test(expected = IllegalArgumentException.class) public void testFormatIllegalPattern() { CompactFormatter f = new CompactFormatter("%1$#tc"); - try { - f.format(new LogRecord(Level.SEVERE, "")); - fail("Expected format exception."); - } catch (java.util.IllegalFormatException expect) { - } + assertNotNull(f.format(new LogRecord(Level.SEVERE, ""))); + //java.util.FormatFlagsConversionMismatchException: + //Conversion = c, Flags = # } @Test(expected = NullPointerException.class) @@ -1309,12 +1797,13 @@ public void testFormatBackTraceUnknown() { "testFormatBackTrace", null, -2)}); assertNotNull(e.getMessage(), e.getMessage()); - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%14$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(e); String result = cf.formatBackTrace(record); assertTrue(result, result.startsWith("CompactFormatterTest")); assertTrue(result, result.contains("testFormatBackTrace")); + assertEquals(result, cf.format(record)); } @Test @@ -1325,12 +1814,13 @@ public void testFormatBackTracePunt() { new StackTraceElement(k.getName(), "newSetFromMap", null, 3878)}); assertNotNull(e.getMessage(), e.getMessage()); - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%14$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(e); String result = cf.formatBackTrace(record); assertTrue(result, result.startsWith(k.getSimpleName())); assertTrue(result, result.contains("newSetFromMap")); + assertEquals(result, cf.format(record)); } @Test @@ -1340,30 +1830,97 @@ public void testFormatBackTraceChainPunt() { e.setStackTrace(new StackTraceElement[0]); e = new RuntimeException(e); e.setStackTrace(new StackTraceElement[]{ - new StackTraceElement(k.getName(), "newSetFromMap", null, 3878)}); + new StackTraceElement(k.getName(), "newSetFromMap", + k.getSimpleName() +".java", 3878)}); assertNotNull(e.getMessage(), e.getMessage()); - CompactFormatter cf = new CompactFormatter(); + CompactFormatter cf = new CompactFormatter("%14$s"); LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(e); String result = cf.formatBackTrace(record); assertTrue(result, result.startsWith(k.getSimpleName())); assertTrue(result, result.contains("newSetFromMap")); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatBackTraceEmptyEmpty() { + //E[0] -> IOE[0] + Exception e = new IOException("Fake I/O"); + e.setStackTrace(new StackTraceElement[0]); + e = new Exception(e.toString(), e); + e.setStackTrace(new StackTraceElement[0]); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter("%14$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + + String result = cf.formatBackTrace(record); + assertTrue(result, result.isEmpty()); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatBackTraceNonEmptyEmpty() { + final Class k = Collection.class; + //RE[1] -> NPE[0] + Throwable e = new NullPointerException("Fake NPE"); + e.setStackTrace(new StackTraceElement[0]); + e = new RuntimeException(e); + e.setStackTrace(new StackTraceElement[]{ + new StackTraceElement(k.getName(), "contains", + k.getSimpleName() +".java", 288)}); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter("%14$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + + //When root trace is empty the parent trace is used. + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith(k.getSimpleName())); + assertTrue(result, result.contains("contains")); + assertTrue(result, result.contains(":288")); + assertEquals(result, cf.format(record)); + } + + @Test + public void testFormatBackTraceNoFileExt() { + final Class k = Collection.class; + //RE[1] -> NPE[0] + Throwable e = new NullPointerException("Fake NPE"); + e.setStackTrace(new StackTraceElement[0]); + e = new RuntimeException(e); + e.setStackTrace(new StackTraceElement[]{ + new StackTraceElement(k.getName(), "contains", + "Foo", 288)}); + assertNotNull(e.getMessage(), e.getMessage()); + + CompactFormatter cf = new CompactFormatter("%14$s"); + LogRecord record = new LogRecord(Level.SEVERE, ""); + record.setThrown(e); + String result = cf.formatBackTrace(record); + assertTrue(result, result.startsWith(k.getSimpleName())); + assertTrue(result, result.contains("contains")); + assertTrue(result, result.contains(":288")); + assertEquals(result, cf.format(record)); } @Test(expected = NullPointerException.class) public void testFormatBackTraceNull() { - CompactFormatter cf = new CompactFormatter(); - cf.formatBackTrace((LogRecord) null); - fail(cf.toString()); + CompactFormatter cf = new CompactFormatter("%14$s"); + assertNotNull(cf.formatBackTrace((LogRecord) null)); } @Test(timeout = 30000) public void testFormatBackTraceEvil() { LogRecord record = new LogRecord(Level.SEVERE, ""); record.setThrown(createEvilThrowable()); - CompactFormatter cf = new CompactFormatter(); - cf.formatBackTrace(record); + CompactFormatter cf = new CompactFormatter("%14$s"); + String result = cf.formatBackTrace(record); + assertNotNull(result); + assertEquals(result, cf.format(record)); } @Test(timeout = 30000) @@ -1377,8 +1934,10 @@ public void testFormatBackTraceEvilIgnore() { new StackTraceElement(CompactFormatterTest.class.getName(), "dummy$bridge", null, -1)}); record.setThrown(first); - CompactFormatter cf = new CompactFormatter(); - cf.formatBackTrace(record); + CompactFormatter cf = new CompactFormatter("%14$s"); + String result = cf.formatBackTrace(record); + assertNotNull(result); + assertEquals(result, cf.format(record)); } @Test @@ -1412,8 +1971,7 @@ private Throwable createEvilThrowable() { @Test(expected = NullPointerException.class) public void testIgnoreNull() { CompactFormatter cf = new CompactFormatter(); - cf.ignore((StackTraceElement) null); - fail(cf.toString()); + assertNotNull(cf.ignore((StackTraceElement) null)); } @Test @@ -1581,7 +2139,8 @@ private void testNullPointerFor(String method) throws Exception { m.invoke(new CompactFormatter(), (StackTraceElement) null); fail("Null was allowed."); } catch (InvocationTargetException expect) { - assertEquals(NullPointerException.class, expect.getCause().getClass()); + assertEquals(NullPointerException.class, + expect.getCause().getClass()); } } @@ -1612,12 +2171,14 @@ public void testWebappClassLoaderFieldNames() throws Exception { testWebappClassLoaderFieldNames(CompactFormatter.class); } - private static String rpad(String s, int len, String p) { - if (s.length() < len) { - StringBuilder sb = new StringBuilder(len); + private static String rpad(String s, final int len, String p) { + final int existing = s.codePointCount(0, s.length()); + if (existing < len) { + final int step = p.codePointCount(0, p.length()); + StringBuilder sb = new StringBuilder(); sb.append(s); - for (int i = sb.length(); i < len; ++i) { - sb.append(p, 0, 1); + for (int i = existing; i < len; i += step) { + sb.append(p); } return sb.toString(); } else {