Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing a Messages.formatWithAttributes() method + unit tests #1649

Merged
merged 12 commits into from Jan 30, 2018
Expand Up @@ -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.
*/
Expand All @@ -32,7 +42,7 @@ private Messages() {
* <p>
* 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
Expand Down Expand Up @@ -110,4 +120,65 @@ public static String format(String messageTemplate, Object... args) {
return builder.toString();
}

/**
* Formats a templated message inserting named arguments.
Copy link
Contributor

@cjkent cjkent Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these Javadoc make it clear enough what the method does. An example would be worth a thousand words.

* <p>
* Typical template would look like:
* <pre>
* Messages.formatWithAttributes("Foo={foo}, Bar={}", "abc", 123)
* </pre>
* 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"}
* <p>
* 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().
* <p>
* 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.
* <p>
* 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<String, Map<String, String>> 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<String, String> 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));
}

}
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<String, Map<String, String>> expectedOutput) {
assertEquals(Messages.formatWithAttributes(template, args), expectedOutput);
}

//-------------------------------------------------------------------------
public void test_validUtilityClass() {
assertUtilityClass(Messages.class);
Expand Down