diff --git a/modules/collect/src/main/java/com/opengamma/strata/collect/Messages.java b/modules/collect/src/main/java/com/opengamma/strata/collect/Messages.java index 20632277dc..c1a0a10d04 100644 --- a/modules/collect/src/main/java/com/opengamma/strata/collect/Messages.java +++ b/modules/collect/src/main/java/com/opengamma/strata/collect/Messages.java @@ -5,11 +5,21 @@ */ package com.opengamma.strata.collect; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.collect.ImmutableMap; +import com.opengamma.strata.collect.tuple.Pair; + /** * Contains utility methods for managing messages. */ public final class Messages { + private static final Pattern REGEX_PATTERN = Pattern.compile("\\{(\\w*)\\}"); //This will match both {}, and {anything} + /** * Restricted constructor. */ @@ -32,7 +42,7 @@ private Messages() { *

* This method is null tolerant to ensure that use in exception construction will * not throw another exception, which might hide the intended exception. - * + * * @param messageTemplate the message template with "{}" placeholders, null returns empty string * @param arg the message argument, null treated as string "null" * @return the formatted message @@ -110,4 +120,65 @@ public static String format(String messageTemplate, Object... args) { return builder.toString(); } + /** + * Formats a templated message inserting named arguments. + *

+ * Typical template would look like: + *

+   * Messages.formatWithAttributes("Foo={foo}, Bar={}", "abc", 123)
+   * 
+ * This will return a {@link Pair} with a String and a Map. + * The String will be the message, will look like: "Foo=abc, Bar=123" + * The Map will look like: {"foo": "123"} + *

+ * This method combines a template message with a list of specific arguments. + * It can be useful to delay string concatenation, which is sometimes a performance issue. + * The approach is similar to SLF4J MessageFormat, Guava Preconditions and String format(). + *

+ * The message template contains zero to many "{name}" placeholders. + * Each placeholder is replaced by the next available argument. + * If there are too few arguments, then the message will be left with placeholders. + * If there are too many arguments, then the excess arguments are ignored. + * No attempt is made to format the arguments. + *

+ * This method is null tolerant to ensure that use in exception construction will + * not throw another exception, which might hide the intended exception. + * + * @param messageTemplate the message template with "{}" placeholders, null returns empty string + * @param args the message arguments, null treated as empty array + * @return the formatted message + */ + public static Pair> formatWithAttributes(String messageTemplate, Object... args) { + if (messageTemplate == null) { + return formatWithAttributes("", args); + } + if (args == null) { + return formatWithAttributes(messageTemplate); + } + + //Do not use an ImmutableMap, as we avoid throwing exceptions in case of duplicate keys. + Map attributes = new HashMap<>(); + Matcher matcher = REGEX_PATTERN.matcher(messageTemplate); + int argIndex = 0; + + StringBuffer outputMessageBuffer = new StringBuffer(); + while (matcher.find()) { + //If the number of placeholders is greater than the number of arguments, then not all placeholders are replaced. + if (argIndex >= args.length) { + continue; + } + + String attributeName = matcher.group(1); //Extract the attribute name + String replacement = args[argIndex].toString().replace("$", "\\$"); + matcher.appendReplacement(outputMessageBuffer, replacement); + if (!attributeName.isEmpty()) { + attributes.put(attributeName, replacement); + } + argIndex++; + } + matcher.appendTail(outputMessageBuffer); + + return Pair.of(outputMessageBuffer.toString(), ImmutableMap.copyOf(attributes)); + } + } diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/MessagesTest.java b/modules/collect/src/test/java/com/opengamma/strata/collect/MessagesTest.java index 5eda94062c..6e2de6b679 100644 --- a/modules/collect/src/test/java/com/opengamma/strata/collect/MessagesTest.java +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/MessagesTest.java @@ -8,11 +8,16 @@ import static com.opengamma.strata.collect.TestHelper.assertUtilityClass; import static org.testng.Assert.assertEquals; +import java.util.Map; import java.util.Objects; +import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import com.google.common.collect.ImmutableMap; +import com.opengamma.strata.collect.tuple.Pair; + /** * Test Messages. */ @@ -134,6 +139,32 @@ public void test_formatMessage_prefixSuffix(String template, Object[] args, Stri assertEquals(Messages.format("::" + Objects.toString(template, "") + "@@", args), "::" + expMain + "@@" + expExcess); } + @DataProvider(name = "formatMessageWithAttributes") + Object[][] data_formatMessageWithAttributes() { + return new Object[][]{ + // null template + {null, null, Pair.of("", ImmutableMap.of())}, + {null, new Object[] {}, Pair.of("", ImmutableMap.of())}, + {"", new Object[] {"testValueMissingKey"}, Pair.of("", ImmutableMap.of())}, + {"{}", new Object[] {"testValue"}, Pair.of("testValue", ImmutableMap.of())}, + {"{a}", new Object[] {"testValue"}, Pair.of("testValue", ImmutableMap.of("a", "testValue"))}, + {"{a} bcd", new Object[] {"testValue"}, Pair.of("testValue bcd", ImmutableMap.of("a", "testValue"))}, + {"Test {abc} test2 {def} test3", new Object[] {"abcValue", 123456}, Pair.of("Test abcValue test2 123456 test3", ImmutableMap.of("abc", "abcValue", "def", "123456"))}, + {"Test {abc} test2 {} test3", new Object[] {"abcValue", 123456}, Pair.of("Test abcValue test2 123456 test3", ImmutableMap.of("abc", "abcValue"))}, + {"Test {abc} test2 {} test3 {} test4", new Object[] {"abcValue", 123456, 789}, Pair.of("Test abcValue test2 123456 test3 789 test4", ImmutableMap.of("abc", "abcValue"))}, + {"Test {abc} test2 {def} test3", new Object[] {"abcValue", 123456, 789}, Pair.of("Test abcValue test2 123456 test3", ImmutableMap.of("abc", "abcValue", "def", "123456"))}, + {"Test {abc} test2 {abc} test3", new Object[] {"abcValue", 123456, 789}, Pair.of("Test abcValue test2 123456 test3", ImmutableMap.of("abc", "123456"))}, + {"Test {abc} test2 {def} test3", new Object[] {"abcValue"}, Pair.of("Test abcValue test2 {def} test3", ImmutableMap.of("abc", "abcValue"))}, + {"{a} bcd", new Object[] {"$testValue"}, Pair.of("$testValue bcd", ImmutableMap.of("a", "\\$testValue"))}, //The $ must be escaped + {"Test {abc} test2 {def} test3 {ghi} test4", new Object[] {"abcValue"}, Pair.of("Test abcValue test2 {def} test3 {ghi} test4", ImmutableMap.of("abc", "abcValue"))} + }; + } + + @Test(dataProvider = "formatMessageWithAttributes") + public void test_formatMessageWithAttributes(String template, Object[] args, Pair> expectedOutput) { + assertEquals(Messages.formatWithAttributes(template, args), expectedOutput); + } + //------------------------------------------------------------------------- public void test_validUtilityClass() { assertUtilityClass(Messages.class);