From d768c5e628cd6d6e516b2a2d315daa0343fe3899 Mon Sep 17 00:00:00 2001 From: Nikolay Fedorovskikh Date: Mon, 15 May 2017 02:07:16 +0500 Subject: [PATCH] MessageFormatter improvements Motivation: `FormattingTuple.getArgArray()` is never used. In the `MessageFormatter` it is possible to make some improvements, e.g. replace `StringBuffer` with `StringBuilder`, avoid redundant allocations, etc. Modifications: - Remove `argArray` field from the `FormattingTuple`. - In `MessageFormatter`: - replace `StringBuffer` with `StringBuilder`, - replace `HashMap` with `HashSet` and make it lazy initialized. - avoid redundant allocations (`substring()`, etc.) - use appropriate StringBuilder's methods for the some `Number` values. - Porting unit tests from `slf4j`. Result: Less GC load on logging with internal `MessageFormatter`. --- .../internal/logging/FormattingTuple.java | 30 +- .../internal/logging/MessageFormatter.java | 325 ++++++++---------- .../logging/MessageFormatterTest.java | 324 +++++++++++++++++ 3 files changed, 473 insertions(+), 206 deletions(-) create mode 100644 common/src/test/java/io/netty/util/internal/logging/MessageFormatterTest.java diff --git a/common/src/main/java/io/netty/util/internal/logging/FormattingTuple.java b/common/src/main/java/io/netty/util/internal/logging/FormattingTuple.java index 4c5930f647..c968c7f80c 100644 --- a/common/src/main/java/io/netty/util/internal/logging/FormattingTuple.java +++ b/common/src/main/java/io/netty/util/internal/logging/FormattingTuple.java @@ -42,46 +42,20 @@ package io.netty.util.internal.logging; /** * Holds the results of formatting done by {@link MessageFormatter}. */ -class FormattingTuple { - - static final FormattingTuple NULL = new FormattingTuple(null); +final class FormattingTuple { private final String message; private final Throwable throwable; - private final Object[] argArray; - FormattingTuple(String message) { - this(message, null, null); - } - - FormattingTuple(String message, Object[] argArray, Throwable throwable) { + FormattingTuple(String message, Throwable throwable) { this.message = message; this.throwable = throwable; - if (throwable == null) { - this.argArray = argArray; - } else { - this.argArray = trimmedCopy(argArray); - } - } - - static Object[] trimmedCopy(Object[] argArray) { - if (argArray == null || argArray.length == 0) { - throw new IllegalStateException("nonsensical empty or null argument array"); - } - final int trimmedLen = argArray.length - 1; - Object[] trimmed = new Object[trimmedLen]; - System.arraycopy(argArray, 0, trimmed, 0, trimmedLen); - return trimmed; } public String getMessage() { return message; } - public Object[] getArgArray() { - return argArray; - } - public Throwable getThrowable() { return throwable; } diff --git a/common/src/main/java/io/netty/util/internal/logging/MessageFormatter.java b/common/src/main/java/io/netty/util/internal/logging/MessageFormatter.java index 064bab34cc..fed8cd0106 100644 --- a/common/src/main/java/io/netty/util/internal/logging/MessageFormatter.java +++ b/common/src/main/java/io/netty/util/internal/logging/MessageFormatter.java @@ -40,8 +40,8 @@ package io.netty.util.internal.logging; import java.text.MessageFormat; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; // contributors: lizongbo: proposed special treatment of array parameter values // Joern Huxhorn: pointed out double[] omission, suggested deep array copy @@ -109,9 +109,7 @@ import java.util.Map; * {@link #arrayFormat(String, Object[])} methods for more details. */ final class MessageFormatter { - static final char DELIM_START = '{'; - static final char DELIM_STOP = '}'; - static final String DELIM_STR = "{}"; + private static final String DELIM_STR = "{}"; private static final char ESCAPE_CHAR = '\\'; /** @@ -159,18 +157,6 @@ final class MessageFormatter { return arrayFormat(messagePattern, new Object[]{argA, argB}); } - static Throwable getThrowableCandidate(Object[] argArray) { - if (argArray == null || argArray.length == 0) { - return null; - } - - final Object lastEntry = argArray[argArray.length - 1]; - if (lastEntry instanceof Throwable) { - return (Throwable) lastEntry; - } - return null; - } - /** * Same principle as the {@link #format(String, Object)} and * {@link #format(String, Object, Object)} methods except that any number of @@ -183,118 +169,109 @@ final class MessageFormatter { */ static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) { + if (argArray == null || argArray.length == 0) { + return new FormattingTuple(messagePattern, null); + } - Throwable throwableCandidate = getThrowableCandidate(argArray); + int lastArrIdx = argArray.length - 1; + Object lastEntry = argArray[lastArrIdx]; + Throwable throwable = lastEntry instanceof Throwable? (Throwable) lastEntry : null; if (messagePattern == null) { - return new FormattingTuple(null, argArray, throwableCandidate); + return new FormattingTuple(null, throwable); } - if (argArray == null) { - return new FormattingTuple(messagePattern); + int j = messagePattern.indexOf(DELIM_STR); + if (j == -1) { + // this is a simple string + return new FormattingTuple(messagePattern, throwable); } + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); int i = 0; - int j; - StringBuffer sbuf = new StringBuffer(messagePattern.length() + 50); + int L = 0; + do { + boolean notEscaped = j == 0 || messagePattern.charAt(j - 1) != ESCAPE_CHAR; + if (notEscaped) { + // normal case + sbuf.append(messagePattern, i, j); + } else { + sbuf.append(messagePattern, i, j - 1); + // check that escape char is not is escaped: "abc x:\\{}" + notEscaped = j >= 2 && messagePattern.charAt(j - 2) == ESCAPE_CHAR; + } - int L; - for (L = 0; L < argArray.length; L++) { - - j = messagePattern.indexOf(DELIM_STR, i); - - if (j == -1) { - // no more variables - if (i == 0) { // this is a simple string - return new FormattingTuple(messagePattern, argArray, - throwableCandidate); - } else { // add the tail string which contains no variables and return - // the result. - sbuf.append(messagePattern.substring(i, messagePattern.length())); - return new FormattingTuple(sbuf.toString(), argArray, - throwableCandidate); + i = j + 2; + if (notEscaped) { + deeplyAppendParameter(sbuf, argArray[L], null); + L++; + if (L > lastArrIdx) { + break; } } else { - if (isEscapedDelimeter(messagePattern, j)) { - if (!isDoubleEscaped(messagePattern, j)) { - L--; // DELIM_START was escaped, thus should not be incremented - sbuf.append(messagePattern.substring(i, j - 1)); - sbuf.append(DELIM_START); - i = j + 1; - } else { - // The escape character preceding the delimiter start is - // itself escaped: "abc x:\\{}" - // we have to consume one backward slash - sbuf.append(messagePattern.substring(i, j - 1)); - deeplyAppendParameter(sbuf, argArray[L], new HashMap()); - i = j + 2; - } - } else { - // normal case - sbuf.append(messagePattern.substring(i, j)); - deeplyAppendParameter(sbuf, argArray[L], new HashMap()); - i = j + 2; - } + sbuf.append(DELIM_STR); } - } + j = messagePattern.indexOf(DELIM_STR, i); + } while (j != -1); + // append the characters following the last {} pair. - sbuf.append(messagePattern.substring(i, messagePattern.length())); - if (L < argArray.length - 1) { - return new FormattingTuple(sbuf.toString(), argArray, throwableCandidate); - } else { - return new FormattingTuple(sbuf.toString(), argArray, null); - } - } - - static boolean isEscapedDelimeter(String messagePattern, - int delimeterStartIndex) { - - if (delimeterStartIndex == 0) { - return false; - } - return messagePattern.charAt(delimeterStartIndex - 1) == ESCAPE_CHAR; - } - - static boolean isDoubleEscaped(String messagePattern, - int delimeterStartIndex) { - return delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR; + sbuf.append(messagePattern, i, messagePattern.length()); + return new FormattingTuple(sbuf.toString(), L <= lastArrIdx? throwable : null); } // special treatment of array values was suggested by 'lizongbo' - private static void deeplyAppendParameter(StringBuffer sbuf, Object o, - Map seenMap) { + private static void deeplyAppendParameter(StringBuilder sbuf, Object o, + Set seenSet) { if (o == null) { sbuf.append("null"); return; } - if (!o.getClass().isArray()) { - safeObjectAppend(sbuf, o); + Class objClass = o.getClass(); + if (!objClass.isArray()) { + if (Number.class.isAssignableFrom(objClass)) { + // Prevent String instantiation for some number types + if (objClass == Long.class) { + sbuf.append(((Long) o).longValue()); + } else if (objClass == Integer.class || objClass == Short.class || objClass == Byte.class) { + sbuf.append(((Number) o).intValue()); + } else if (objClass == Double.class) { + sbuf.append(((Double) o).doubleValue()); + } else if (objClass == Float.class) { + sbuf.append(((Float) o).floatValue()); + } else { + safeObjectAppend(sbuf, o); + } + } else { + safeObjectAppend(sbuf, o); + } } else { // check for primitive array types because they // unfortunately cannot be cast to Object[] - if (o instanceof boolean[]) { + sbuf.append('['); + if (objClass == boolean[].class) { booleanArrayAppend(sbuf, (boolean[]) o); - } else if (o instanceof byte[]) { + } else if (objClass == byte[].class) { byteArrayAppend(sbuf, (byte[]) o); - } else if (o instanceof char[]) { + } else if (objClass == char[].class) { charArrayAppend(sbuf, (char[]) o); - } else if (o instanceof short[]) { + } else if (objClass == short[].class) { shortArrayAppend(sbuf, (short[]) o); - } else if (o instanceof int[]) { + } else if (objClass == int[].class) { intArrayAppend(sbuf, (int[]) o); - } else if (o instanceof long[]) { + } else if (objClass == long[].class) { longArrayAppend(sbuf, (long[]) o); - } else if (o instanceof float[]) { + } else if (objClass == float[].class) { floatArrayAppend(sbuf, (float[]) o); - } else if (o instanceof double[]) { + } else if (objClass == double[].class) { doubleArrayAppend(sbuf, (double[]) o); } else { - objectArrayAppend(sbuf, (Object[]) o, seenMap); + objectArrayAppend(sbuf, (Object[]) o, seenSet); } + sbuf.append(']'); } } - private static void safeObjectAppend(StringBuffer sbuf, Object o) { + private static void safeObjectAppend(StringBuilder sbuf, Object o) { try { String oAsString = o.toString(); sbuf.append(oAsString); @@ -307,120 +284,112 @@ final class MessageFormatter { } } - private static void objectArrayAppend(StringBuffer sbuf, Object[] a, - Map seenMap) { - sbuf.append('['); - if (!seenMap.containsKey(a)) { - seenMap.put(a, null); - final int len = a.length; - for (int i = 0; i < len; i++) { - deeplyAppendParameter(sbuf, a[i], seenMap); - if (i != len - 1) { - sbuf.append(", "); - } + private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Set seenSet) { + if (a.length == 0) { + return; + } + if (seenSet == null) { + seenSet = new HashSet(a.length); + } + if (seenSet.add(a)) { + deeplyAppendParameter(sbuf, a[0], seenSet); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + deeplyAppendParameter(sbuf, a[i], seenSet); } // allow repeats in siblings - seenMap.remove(a); + seenSet.remove(a); } else { sbuf.append("..."); } - sbuf.append(']'); } - private static void booleanArrayAppend(StringBuffer sbuf, boolean[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void byteArrayAppend(StringBuffer sbuf, byte[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void byteArrayAppend(StringBuilder sbuf, byte[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void charArrayAppend(StringBuffer sbuf, char[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void charArrayAppend(StringBuilder sbuf, char[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void shortArrayAppend(StringBuffer sbuf, short[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void shortArrayAppend(StringBuilder sbuf, short[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void intArrayAppend(StringBuffer sbuf, int[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void intArrayAppend(StringBuilder sbuf, int[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void longArrayAppend(StringBuffer sbuf, long[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void longArrayAppend(StringBuilder sbuf, long[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void floatArrayAppend(StringBuffer sbuf, float[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void floatArrayAppend(StringBuilder sbuf, float[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } - private static void doubleArrayAppend(StringBuffer sbuf, double[] a) { - sbuf.append('['); - final int len = a.length; - for (int i = 0; i < len; i++) { - sbuf.append(a[i]); - if (i != len - 1) { - sbuf.append(", "); - } + private static void doubleArrayAppend(StringBuilder sbuf, double[] a) { + if (a.length == 0) { + return; + } + sbuf.append(a[0]); + for (int i = 1; i < a.length; i++) { + sbuf.append(", "); + sbuf.append(a[i]); } - sbuf.append(']'); } private MessageFormatter() { diff --git a/common/src/test/java/io/netty/util/internal/logging/MessageFormatterTest.java b/common/src/test/java/io/netty/util/internal/logging/MessageFormatterTest.java new file mode 100644 index 0000000000..b63d4f2a38 --- /dev/null +++ b/common/src/test/java/io/netty/util/internal/logging/MessageFormatterTest.java @@ -0,0 +1,324 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project 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. + */ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.netty.util.internal.logging; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class MessageFormatterTest { + + @Test + public void testNull() { + String result = MessageFormatter.format(null, 1).getMessage(); + assertNull(result); + } + + @Test + public void nullParametersShouldBeHandledWithoutBarfing() { + String result = MessageFormatter.format("Value is {}.", null).getMessage(); + assertEquals("Value is null.", result); + + result = MessageFormatter.format("Val1 is {}, val2 is {}.", null, null).getMessage(); + assertEquals("Val1 is null, val2 is null.", result); + + result = MessageFormatter.format("Val1 is {}, val2 is {}.", 1, null).getMessage(); + assertEquals("Val1 is 1, val2 is null.", result); + + result = MessageFormatter.format("Val1 is {}, val2 is {}.", null, 2).getMessage(); + assertEquals("Val1 is null, val2 is 2.", result); + + result = MessageFormatter.arrayFormat( + "Val1 is {}, val2 is {}, val3 is {}", new Integer[] { null, null, null }).getMessage(); + assertEquals("Val1 is null, val2 is null, val3 is null", result); + + result = MessageFormatter.arrayFormat( + "Val1 is {}, val2 is {}, val3 is {}", new Integer[] { null, 2, 3 }).getMessage(); + assertEquals("Val1 is null, val2 is 2, val3 is 3", result); + + result = MessageFormatter.arrayFormat( + "Val1 is {}, val2 is {}, val3 is {}", new Integer[] { null, null, 3 }).getMessage(); + assertEquals("Val1 is null, val2 is null, val3 is 3", result); + } + + @Test + public void verifyOneParameterIsHandledCorrectly() { + String result = MessageFormatter.format("Value is {}.", 3).getMessage(); + assertEquals("Value is 3.", result); + + result = MessageFormatter.format("Value is {", 3).getMessage(); + assertEquals("Value is {", result); + + result = MessageFormatter.format("{} is larger than 2.", 3).getMessage(); + assertEquals("3 is larger than 2.", result); + + result = MessageFormatter.format("No subst", 3).getMessage(); + assertEquals("No subst", result); + + result = MessageFormatter.format("Incorrect {subst", 3).getMessage(); + assertEquals("Incorrect {subst", result); + + result = MessageFormatter.format("Value is {bla} {}", 3).getMessage(); + assertEquals("Value is {bla} 3", result); + + result = MessageFormatter.format("Escaped \\{} subst", 3).getMessage(); + assertEquals("Escaped {} subst", result); + + result = MessageFormatter.format("{Escaped", 3).getMessage(); + assertEquals("{Escaped", result); + + result = MessageFormatter.format("\\{}Escaped", 3).getMessage(); + assertEquals("{}Escaped", result); + + result = MessageFormatter.format("File name is {{}}.", "App folder.zip").getMessage(); + assertEquals("File name is {App folder.zip}.", result); + + // escaping the escape character + result = MessageFormatter.format("File name is C:\\\\{}.", "App folder.zip").getMessage(); + assertEquals("File name is C:\\App folder.zip.", result); + } + + @Test + public void testTwoParameters() { + String result = MessageFormatter.format("Value {} is smaller than {}.", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than 2.", result); + + result = MessageFormatter.format("Value {} is smaller than {}", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than 2", result); + + result = MessageFormatter.format("{}{}", 1, 2).getMessage(); + assertEquals("12", result); + + result = MessageFormatter.format("Val1={}, Val2={", 1, 2).getMessage(); + assertEquals("Val1=1, Val2={", result); + + result = MessageFormatter.format("Value {} is smaller than \\{}", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than {}", result); + + result = MessageFormatter.format("Value {} is smaller than \\{} tail", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than {} tail", result); + + result = MessageFormatter.format("Value {} is smaller than \\{", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than \\{", result); + + result = MessageFormatter.format("Value {} is smaller than {tail", 1, 2).getMessage(); + assertEquals("Value 1 is smaller than {tail", result); + + result = MessageFormatter.format("Value \\{} is smaller than {}", 1, 2).getMessage(); + assertEquals("Value {} is smaller than 1", result); + } + + @Test + public void testExceptionIn_toString() { + Object o = new Object() { + @Override + public String toString() { + throw new IllegalStateException("a"); + } + }; + String result = MessageFormatter.format("Troublesome object {}", o).getMessage(); + assertEquals("Troublesome object [FAILED toString()]", result); + } + + @Test + public void testNullArray() { + String msg0 = "msg0"; + String msg1 = "msg1 {}"; + String msg2 = "msg2 {} {}"; + String msg3 = "msg3 {} {} {}"; + + Object[] args = null; + + String result = MessageFormatter.arrayFormat(msg0, args).getMessage(); + assertEquals(msg0, result); + + result = MessageFormatter.arrayFormat(msg1, args).getMessage(); + assertEquals(msg1, result); + + result = MessageFormatter.arrayFormat(msg2, args).getMessage(); + assertEquals(msg2, result); + + result = MessageFormatter.arrayFormat(msg3, args).getMessage(); + assertEquals(msg3, result); + } + + // tests the case when the parameters are supplied in a single array + @Test + public void testArrayFormat() { + Integer[] ia0 = { 1, 2, 3 }; + + String result = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia0).getMessage(); + assertEquals("Value 1 is smaller than 2 and 3.", result); + + result = MessageFormatter.arrayFormat("{}{}{}", ia0).getMessage(); + assertEquals("123", result); + + result = MessageFormatter.arrayFormat("Value {} is smaller than {}.", ia0).getMessage(); + assertEquals("Value 1 is smaller than 2.", result); + + result = MessageFormatter.arrayFormat("Value {} is smaller than {}", ia0).getMessage(); + assertEquals("Value 1 is smaller than 2", result); + + result = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia0).getMessage(); + assertEquals("Val=1, {, Val=2", result); + + result = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia0).getMessage(); + assertEquals("Val=1, {, Val=2", result); + + result = MessageFormatter.arrayFormat("Val1={}, Val2={", ia0).getMessage(); + assertEquals("Val1=1, Val2={", result); + } + + @Test + public void testArrayValues() { + Integer[] p1 = { 2, 3 }; + + String result = MessageFormatter.format("{}{}", 1, p1).getMessage(); + assertEquals("1[2, 3]", result); + + // Integer[] + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", p1 }).getMessage(); + assertEquals("a[2, 3]", result); + + // byte[] + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", new byte[] { 1, 2 } }).getMessage(); + assertEquals("a[1, 2]", result); + + // int[] + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", new int[] { 1, 2 } }).getMessage(); + assertEquals("a[1, 2]", result); + + // float[] + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", new float[] { 1, 2 } }).getMessage(); + assertEquals("a[1.0, 2.0]", result); + + // double[] + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", new double[] { 1, 2 } }).getMessage(); + assertEquals("a[1.0, 2.0]", result); + } + + @Test + public void testMultiDimensionalArrayValues() { + Integer[] ia0 = { 1, 2, 3 }; + Integer[] ia1 = { 10, 20, 30 }; + + Integer[][] multiIntegerA = { ia0, ia1 }; + String result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", multiIntegerA }).getMessage(); + assertEquals("a[[1, 2, 3], [10, 20, 30]]", result); + + int[][] multiIntA = { { 1, 2 }, { 10, 20 } }; + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", multiIntA }).getMessage(); + assertEquals("a[[1, 2], [10, 20]]", result); + + float[][] multiFloatA = { { 1, 2 }, { 10, 20 } }; + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", multiFloatA }).getMessage(); + assertEquals("a[[1.0, 2.0], [10.0, 20.0]]", result); + + Object[][] multiOA = { ia0, ia1 }; + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", multiOA }).getMessage(); + assertEquals("a[[1, 2, 3], [10, 20, 30]]", result); + + Object[][][] _3DOA = { multiOA, multiOA }; + result = MessageFormatter.arrayFormat("{}{}", new Object[] { "a", _3DOA }).getMessage(); + assertEquals("a[[[1, 2, 3], [10, 20, 30]], [[1, 2, 3], [10, 20, 30]]]", result); + + Byte[] ba0 = { 0, Byte.MAX_VALUE, Byte.MIN_VALUE }; + Short[] sa0 = { 0, Short.MIN_VALUE, Short.MAX_VALUE }; + result = MessageFormatter.arrayFormat("{}\\{}{}", new Object[] { new Object[] { ba0, sa0 }, ia1 }).getMessage(); + assertEquals("[[0, 127, -128], [0, -32768, 32767]]{}[10, 20, 30]", result); + } + + @Test + public void testCyclicArrays() { + Object[] cyclicA = new Object[1]; + cyclicA[0] = cyclicA; + assertEquals("[[...]]", MessageFormatter.arrayFormat("{}", cyclicA).getMessage()); + + Object[] a = new Object[2]; + a[0] = 1; + Object[] c = { 3, a }; + Object[] b = { 2, c }; + a[1] = b; + assertEquals("1[2, [3, [1, [...]]]]", + MessageFormatter.arrayFormat("{}{}", a).getMessage()); + } + + @Test + public void testArrayThrowable() { + FormattingTuple ft; + Throwable t = new Throwable(); + Object[] ia = { 1, 2, 3, t }; + + ft = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia); + assertEquals("Value 1 is smaller than 2 and 3.", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("{}{}{}", ia); + assertEquals("123", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Value {} is smaller than {}.", ia); + assertEquals("Value 1 is smaller than 2.", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Value {} is smaller than {}", ia); + assertEquals("Value 1 is smaller than 2", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia); + assertEquals("Val=1, {, Val=2", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Val={}, \\{, Val={}", ia); + assertEquals("Val=1, \\{, Val=2", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Val1={}, Val2={", ia); + assertEquals("Val1=1, Val2={", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia); + assertEquals("Value 1 is smaller than 2 and 3.", ft.getMessage()); + assertEquals(t, ft.getThrowable()); + + ft = MessageFormatter.arrayFormat("{}{}{}{}", ia); + assertEquals("123java.lang.Throwable", ft.getMessage()); + assertNull(ft.getThrowable()); + } +}