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);