118

I have the following template String: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

I also have String variables for name, invoice number and due date - what's the best way to replace the tokens in the template with the variables?

(Note that if a variable happens to contain a token it should NOT be replaced).


EDIT

With thanks to @laginimaineb and @alan-moore, here's my solution:

public static String replaceTokens(String text, 
                                   Map<String, String> replacements) {
    Pattern pattern = Pattern.compile("\\[(.+?)\\]");
    Matcher matcher = pattern.matcher(text);
    StringBuffer buffer = new StringBuffer();

    while (matcher.find()) {
        String replacement = replacements.get(matcher.group(1));
        if (replacement != null) {
            // matcher.appendReplacement(buffer, replacement);
            // see comment 
            matcher.appendReplacement(buffer, "");
            buffer.append(replacement);
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
Raman Sahasi
  • 30,180
  • 9
  • 58
  • 71
Mark
  • 4,749
  • 7
  • 44
  • 53
  • One thing to note, though, is that StringBuffer is the same as StringBuilder just synchronized. However, since in this example you don't need to synchronize the building of the String you might be better off using StringBuilder (even though acquiring locks is nearly a zero-cost operation). – laginimaineb Jun 06 '09 at 16:50
  • 1
    Unfortunately, you have to use StringBuffer in this case; it's what the appendXXX() methods expect. They've been around since Java 4, and StringBuilder wasn't added until Java 5. As you said though, it's no big deal, just annoying. – Alan Moore Jun 07 '09 at 01:39
  • 4
    One more thing: appendReplacement(), like the replaceXXX() methods, looks for capture-group references like $1, $2, etc., and replaces them with the text from the associated capture groups. If your replacement text might contain dollar signs or backslashes (which are used to escape dollar signs), you could have a problem. The easiest way to deal with that is to break the append operation into two steps as I've done in the code above. – Alan Moore Jun 07 '09 at 04:20
  • Alan - very impressed you spotted that. I didn't think such a simple problem would be so difficult to solve! – Mark Jun 07 '09 at 21:45
  • use http://java.sun.com/javase/6/docs/api/java/util/regex/Matcher.html#quoteReplacement%28java.lang.String%29 – Jason S Oct 01 '09 at 16:10
  • FYI - Spring's UriTemplate.expand(Object...) uses exactly the above code except the fix added by @AlanMoore . http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/web/util/UriTemplate.html#expand(java.lang.Object...) – Somu Sep 06 '12 at 10:54
  • Wow, that's very, very cool. A clean, minimal, efficient and complete solution. Thanks y'all. One of the few examples where regexp DO make sense. – Franz D. Nov 22 '17 at 14:14
  • See `java.text.MessageFormat.format("Foo {} {}", "bar", 1);` – Ondra Žižka Jan 16 '18 at 17:19
  • 1
    note: overloaded methods with StringBuilder were added to Matcher in java9. From Java9 onward, you'll want to use StringBuilder instead of StringBuffer. – Erik Lievaart Mar 24 '21 at 07:17

16 Answers16

117

I really don't think you need to use a templating engine or anything like that for this. You can use the String.format method, like so:

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
  • 15,528
  • 9
  • 52
  • 57
74

The most efficient way would be using a matcher to continually find the expressions and replace them, then append the text to a string builder:

Pattern pattern = Pattern.compile("\\[(.+?)\\]");
Matcher matcher = pattern.matcher(text);
HashMap<String,String> replacements = new HashMap<String,String>();
//populate the replacements map ...
StringBuilder builder = new StringBuilder();
int i = 0;
while (matcher.find()) {
    String replacement = replacements.get(matcher.group(1));
    builder.append(text.substring(i, matcher.start()));
    if (replacement == null)
        builder.append(matcher.group(0));
    else
        builder.append(replacement);
    i = matcher.end();
}
builder.append(text.substring(i, text.length()));
return builder.toString();
laginimaineb
  • 8,245
  • 1
  • 22
  • 14
  • 10
    This is how I would do it, except I would use Matcher's appendReplacement() and appendTail() methods to copy the unmatched text; there's no need to do that by hand. – Alan Moore Jun 06 '09 at 14:43
  • 6
    Actually the appendReplacement() and appentTail() methods require a StringBuffer, which is snychronized (which is of no use here). The given answer uses a StringBuilder, which is 20% faster in my tests. – dube Jul 23 '14 at 13:49
52

Unfortunately the comfortable method String.format mentioned above is only available starting with Java 1.5 (which should be pretty standard nowadays, but you never know). Instead of that you might also use Java's class MessageFormat for replacing the placeholders.

It supports placeholders in the form '{number}', so your message would look like "Hello {0} Please find attached {1} which is due on {2}". These Strings can easily be externalized using ResourceBundles (e. g. for localization with multiple locales). The replacing would be done using the static'format' method of class MessageFormat:

String msg = "Hello {0} Please find attached {1} which is due on {2}";
String[] values = {
  "John Doe", "invoice #123", "2009-06-30"
};
System.out.println(MessageFormat.format(msg, values));
toolkit
  • 49,809
  • 17
  • 109
  • 135
  • 5
    I could not remember the name of MessageFormat, and it's kind of silly how much Googling I had to do to find even this answer. Everyone acts like it's either String.format or use a 3rd-party, forgetting this incredibly useful utility. – Patrick Jan 31 '14 at 15:00
  • 1
    This has been available since 2004 - why am I only just learning about in now, in 2017? I'm refactoring some code that's covered in `StringBuilder.append()`s and I was thinking "Surely there's a better way... something more Pythonic..." - and holy crap, I think this method may predate Python's formatting methods. Actually... this may be older than 2002... I can't find when this actually came into existence... – ArtOfWarfare Jan 11 '17 at 21:49
44

You could try using a templating library like Apache Velocity.

http://velocity.apache.org/

Here is an example:

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.StringWriter;

public class TemplateExample {
    public static void main(String args[]) throws Exception {
        Velocity.init();

        VelocityContext context = new VelocityContext();
        context.put("name", "Mark");
        context.put("invoiceNumber", "42123");
        context.put("dueDate", "June 6, 2009");

        String template = "Hello $name. Please find attached invoice" +
                          " $invoiceNumber which is due on $dueDate.";
        StringWriter writer = new StringWriter();
        Velocity.evaluate(context, writer, "TemplateName", template);

        System.out.println(writer);
    }
}

The output would be:

Hello Mark. Please find attached invoice 42123 which is due on June 6, 2009.
hallidave
  • 9,579
  • 6
  • 31
  • 27
26

You can use template library for complex template replacement.

FreeMarker is a very good choice.

http://freemarker.sourceforge.net/

But for simple task, there is a simple utility class can help you.

org.apache.commons.lang3.text.StrSubstitutor

It is very powerful, customizable, and easy to use.

This class takes a piece of text and substitutes all the variables within it. The default definition of a variable is ${variableName}. The prefix and suffix can be changed via constructors and set methods.

Variable values are typically resolved from a map, but could also be resolved from system properties, or by supplying a custom variable resolver.

For example, if you want to substitute system environment variable into a template string, here is the code:

public class SysEnvSubstitutor {
    public static final String replace(final String source) {
        StrSubstitutor strSubstitutor = new StrSubstitutor(
                new StrLookup<Object>() {
                    @Override
                    public String lookup(final String key) {
                        return System.getenv(key);
                    }
                });
        return strSubstitutor.replace(source);
    }
}
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
Li Ying
  • 2,261
  • 27
  • 17
20
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Output: Hello Join! You have 10 messages"

Nazik
  • 8,696
  • 27
  • 77
  • 123
user2845137
  • 201
  • 2
  • 2
11
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Pablo Fernandez
  • 103,170
  • 56
  • 192
  • 232
Bruno Ranschaert
  • 7,428
  • 5
  • 36
  • 46
  • 2
    Thanks - but in my case the template string can be modified by the user, so I can't be sure of the order of tokens – Mark Jun 06 '09 at 15:07
9

It depends of where the actual data that you want to replace is located. You might have a Map like this:

Map<String, String> values = new HashMap<String, String>();

containing all the data that can be replaced. Then you can iterate over the map and change everything in the String as follows:

String s = "Your String with [Fields]";
for (Map.Entry<String, String> e : values.entrySet()) {
  s = s.replaceAll("\\[" + e.getKey() + "\\]", e.getValue());
}

You could also iterate over the String and find the elements in the map. But that is a little bit more complicated because you need to parse the String searching for the []. You could do it with a regular expression using Pattern and Matcher.

Community
  • 1
  • 1
Ricardo Marimon
  • 10,339
  • 9
  • 52
  • 59
5

My solution for replacing ${variable} style tokens (inspired by the answers here and by the Spring UriTemplate):

public static String substituteVariables(String template, Map<String, String> variables) {
    Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}");
    Matcher matcher = pattern.matcher(template);
    // StringBuilder cannot be used here because Matcher expects StringBuffer
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
        if (variables.containsKey(matcher.group(1))) {
            String replacement = variables.get(matcher.group(1));
            // quote to work properly with $ and {,} signs
            matcher.appendReplacement(buffer, replacement != null ? Matcher.quoteReplacement(replacement) : "null");
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
mihu86
  • 969
  • 8
  • 9
3

You can use Apache Commons StringSubstitutor:

For example:

 // Build map
 Map<String, String> valuesMap = new HashMap<>();
 valuesMap.put("animal", "quick brown fox");
 valuesMap.put("target", "lazy dog");
 String templateString = "The ${animal} jumped over the ${target}.";

 // Build StringSubstitutor
 StringSubstitutor sub = new StringSubstitutor(valuesMap);

 // Replace
 String resolvedString = sub.replace(templateString);

yielding:

 "The quick brown fox jumped over the lazy dog."

You can also customize the prefix and suffix delimiters (${ and } respectively in the example above) by using:

You can also specify a default value using syntax like below:

String templateString = "The ${animal:giraffe} jumped over the ${target}.";

which would yield "The giraffe jumped over the lazy dog." when no animal parameter was supplied.

GreenGiant
  • 4,930
  • 1
  • 46
  • 76
2

With Apache Commons Library, you can simply use Stringutils.replaceEach:

public static String replaceEach(String text,
                             String[] searchList,
                             String[] replacementList)

From the documentation:

Replaces all occurrences of Strings within another String.

A null reference passed to this method is a no-op, or if any "search string" or "string to replace" is null, that replace will be ignored. This will not repeat. For repeating replaces, call the overloaded method.

 StringUtils.replaceEach(null, *, *)        = null

  StringUtils.replaceEach("", *, *)          = ""

  StringUtils.replaceEach("aba", null, null) = "aba"

  StringUtils.replaceEach("aba", new String[0], null) = "aba"

  StringUtils.replaceEach("aba", null, new String[0]) = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"

  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"

  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
  (example of how it does not repeat)

StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
AR1
  • 4,507
  • 4
  • 26
  • 42
1

http://github.com/niesfisch/tokenreplacer

Marcel
  • 710
  • 8
  • 23
1

FYI

In the new language Kotlin, you can use "String Templates" in your source code directly, no 3rd party library or template engine need to do the variable replacement.

It is a feature of the language itself.

See: https://kotlinlang.org/docs/reference/basic-types.html#string-templates

Li Ying
  • 2,261
  • 27
  • 17
0

I used

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
  • 1
  • 2
    That would work, but in my case the template string is customisable by the user, so I don't know in what order the tokens will appear. – Mark Jan 29 '11 at 11:44
0

The following replaces variables of the form <<VAR>>, with values looked up from a Map. You can test it online here

For example, with the following input string

BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70
Hi there <<Weight>> was here

and the following variable values

Weight, 42
Height, HEIGHT 51

outputs the following

BMI=(42/(HEIGHT 51*HEIGHT 51)) * 70

Hi there 42 was here

Here's the code

  static Pattern pattern = Pattern.compile("<<([a-z][a-z0-9]*)>>", Pattern.CASE_INSENSITIVE);

  public static String replaceVarsWithValues(String message, Map<String,String> varValues) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = pattern.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = varValues.get(keyName)+"";
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }


  public static void main(String args[]) throws Exception {
      String testString = "BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70\n\nHi there <<Weight>> was here";
      HashMap<String,String> values = new HashMap<>();
      values.put("Weight", "42");
      values.put("Height", "HEIGHT 51");
      System.out.println(replaceVarsWithValues(testString, values));
  }

and although not requested, you can use a similar approach to replace variables in a string with properties from your application.properties file, though this may already be being done:

private static Pattern patternMatchForProperties =
      Pattern.compile("[$][{]([.a-z0-9_]*)[}]", Pattern.CASE_INSENSITIVE);

protected String replaceVarsWithProperties(String message) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = patternMatchForProperties.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = System.getProperty(keyName);
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }
Brad Parks
  • 66,836
  • 64
  • 257
  • 336
0

In the past, I've solved this kind of problem with StringTemplate and Groovy Templates.

Ultimately, the decision of using a templating engine or not should be based on the following factors:

  • Will you have many of these templates in the application?
  • Do you need the ability to modify the templates without restarting the application?
  • Who will be maintaining these templates? A Java programmer or a business analyst involved on the project?
  • Will you need to the ability to put logic in your templates, like conditional text based on values in the variables?
  • Will you need the ability to include other templates in a template?

If any of the above applies to your project, I would consider using a templating engine, most of which provide this functionality, and more.

Francois Gravel
  • 461
  • 3
  • 3