13

Given the following variables

templateText = "Hi ${name}";
variables.put("name", "Joe");

I would like to replace the placeholder ${name} with the value "Joe" using the following code (that does not work)

 variables.keySet().forEach(k -> templateText.replaceAll("\\${\\{"+ k +"\\}"  variables.get(k)));

However, if I do the "old-style" way, everything works perfectly:

for (Entry<String, String> entry : variables.entrySet()){
    String  regex = "\\$\\{" + entry.getKey() + "\\}";          
    templateText =  templateText.replaceAll(regex, entry.getValue());           
   }

Surely I am missing something here :)

9
  • 5
    forEach runs your command on each element, but you are not capturing the result.
    – khelwood
    Commented Apr 12, 2017 at 13:52
  • 1
    Also, if you're not trying to use a regular expression, it would be easier to use replace, not replaceAll
    – khelwood
    Commented Apr 12, 2017 at 13:53
  • Thanks. So can you show me the right way to do it with Java8? Commented Apr 12, 2017 at 13:54
  • 2
    Note that in your Java 8 version, there also seems to be a typo in the prefix of the regex: "\\{$\\{" instead of "\\$\\{".
    – Didier L
    Commented Apr 12, 2017 at 13:57
  • You are right. I've just edited. Commented Apr 12, 2017 at 13:58

6 Answers 6

19

Java 9 and above

Java 9 introduced Matcher.replaceAll(Function) which basically implements the same thing as the functional version for Java 8 below. See Jesse Glick's answer for more details.

Java 8

The proper way to implement this did not change in Java 8, it is based on appendReplacement()/appendTail():

Pattern variablePattern = Pattern.compile("\\$\\{(.+?)\\}");
Matcher matcher = variablePattern.matcher(templateText);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
    matcher.appendReplacement(result, variables.get(matcher.group(1)));
}
matcher.appendTail(result);
System.out.println(result);

Note that, as mentioned by drrob in the comments, the replacement String of appendReplacement() may contain group references using the $ sign, and escaping using \. If this is not desired, or if your replacement String can potentially contain those characters, you should escape them using Matcher.quoteReplacement().

Being more functional in Java 8

If you want a more Java-8-style version, you can extract the search-and-replace boiler plate code into a generalized method that takes a replacement Function:

private static StringBuffer replaceAll(String templateText, Pattern pattern,
                                       Function<Matcher, String> replacer) {
    Matcher matcher = pattern.matcher(templateText);
    StringBuffer result = new StringBuffer();
    while (matcher.find()) {
        matcher.appendReplacement(result, replacer.apply(matcher));
    }
    matcher.appendTail(result);
    return result;
}

and use it as

Pattern variablePattern = Pattern.compile("\\$\\{(.+?)\\}");
StringBuffer result = replaceAll(templateText, variablePattern,
                                 m -> variables.get(m.group(1)));

Note that having a Pattern as parameter (instead of a String) allows it to be stored as a constant instead of recompiling it every time.

Same remark applies as above concerning $ and \ – you may want to enforce the quoteReplacement() inside the replaceAll() method if you don't want your replacer function to handle it.

3
  • 3
    this is actually much nicer IMO then both broken versions of the up-voted answers. plus one
    – Eugene
    Commented Apr 12, 2017 at 17:12
  • 2
    You almost certainly want to wrap the replacer return value in Matcher.quoteReplacement(). The replacement string interprets slashes and dollar signs as a way of refering to other groups. If each variable being substituted is independent, and should be treated literally, then this behaviour is undesirable. Subtle bugs will ensue.
    – drrob
    Commented Jun 7, 2018 at 10:43
  • 1
    Thanks @drrob, I have included notes about it in the answer.
    – Didier L
    Commented Jun 7, 2018 at 12:17
10

you also can using Stream.reduce(identity,accumulator,combiner).

identity

identity is the initial value for reducing function which is accumulator.

accumulator

accumulator reducing identity to result, which is the identity for the next reducing if the stream is sequentially.

combiner

this function never be called in sequentially stream. it calculate the next identity from identity & result in parallel stream.

BinaryOperator<String> combinerNeverBeCalledInSequentiallyStream=(identity,t) -> {
   throw new IllegalStateException("Can't be used in parallel stream");
};

String result = variables.entrySet().stream()
            .reduce(templateText
                   , (it, var) -> it.replaceAll(format("\\$\\{%s\\}", var.getKey())
                                               , var.getValue())
                   , combinerNeverBeCalledInSequentiallyStream);
4
  • 3
    @holi-java you have broken associativity here, this does not work correctly in parallel; even if you have provided the combiner.
    – Eugene
    Commented Apr 12, 2017 at 17:01
  • 1
    The definitions of identity and accumulator given here are completely wrong. From the javadoc: The identity value must be an identity for the combiner function. This means that for all u, combiner(identity, u) is equal to u. Additionally, […] for all u and t, the following must hold: combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
    – Didier L
    Commented Apr 13, 2017 at 7:52
  • @DidierL well the identity looks ok to me. u == combiner(identity, u) is in fact true for (identity, result) -> result; but the associativity is indeed wrong.
    – Eugene
    Commented Apr 13, 2017 at 11:24
  • @Eugene yes,you are right, the combiner can't run in parallel stream, I only want to tell the OP combiner never be called in sequentially stream.
    – holi-java
    Commented Apr 13, 2017 at 11:26
4
import java.util.HashMap;
import java.util.Map;

public class Repl {

    public static void main(String[] args) {
        Map<String, String> variables = new HashMap<>();
        String templateText = "Hi, ${name} ${secondname}! My name is ${name} too :)";
        variables.put("name", "Joe");
        variables.put("secondname", "White");

        templateText = variables.keySet().stream().reduce(templateText, (acc, e) -> acc.replaceAll("\\$\\{" + e + "\\}", variables.get(e)));
        System.out.println(templateText);
    }

}

output:

Hi, Joe White! My name is Joe too :)

However, it's not the best idea to reinvent the wheel and the preferred way to achieve what you want would be to use apache commons lang as stated here.

 Map<String, String> valuesMap = new HashMap<String, String>();
 valuesMap.put("animal", "quick brown fox");
 valuesMap.put("target", "lazy dog");
 String templateString = "The ${animal} jumped over the ${target}.";
 StrSubstitutor sub = new StrSubstitutor(valuesMap);
 String resolvedString = sub.replace(templateString);
7
  • Thanks, can you please explain what (Function<String, String>) stand for? Commented Apr 12, 2017 at 14:14
  • Moreover, can you also explain the reduce part? Thanks :) Commented Apr 12, 2017 at 14:15
  • 1
    This is needlessly complex, you can do just .reduce(templateText, (acc, e) -> acc.replaceAll("\\$\\{" + e + "\\}", variables.get(e))); (just make sure not to use a parallel stream) Commented Apr 12, 2017 at 14:18
  • 1
    but that way you can not easily parallelize your stream, whereas that would have worked with your previous version ;-) however, that wasn't even a requirement, so it's probably ok that way :-)
    – Roland
    Commented Apr 12, 2017 at 14:26
  • 4
    The solution with reduce() violates the requirements of that method: The identity value must be an identity for the accumulator function. This means that for all t, accumulator.apply(identity, t) is equal to t. The accumulator function must be an associative function.
    – Didier L
    Commented Apr 12, 2017 at 16:14
3

To update @didier-l’s answer, in Java 9 this is a one-liner!

Pattern.compile("[$][{](.+?)[}]").matcher(templateText).replaceAll(m -> variables.get(m.group(1)))
2

Your code should be changed like below,

String templateText = "Hi ${name}";
Map<String,String> variables = new HashMap<>();
variables.put("name", "Joe");
templateText = variables.keySet().stream().reduce(templateText, (originalText, key) -> originalText.replaceAll("\\$\\{" + key + "\\}", variables.get(key)));
1
  • This code will not work for parallel streams. Use this code only with single stream. Commented Apr 12, 2017 at 14:39
1

Performing replaceAll repeatedly, i.e. for every replaceable variable, can become quiet expensive, especially as the number of variables might grow. This doesn’t become more efficient when using the Stream API. The regex package contains the necessary building blocks to do this more efficiently:

public static String replaceAll(String template, Map<String,String> variables) {
    String pattern = variables.keySet().stream()
        .map(Pattern::quote)
        .collect(Collectors.joining("|", "\\$\\{(", ")\\}"));
    Matcher m = Pattern.compile(pattern).matcher(template);
    if(!m.find()) {
        return template;
    }
    StringBuffer sb = new StringBuffer();
    do {
        m.appendReplacement(sb, Matcher.quoteReplacement(variables.get(m.group(1))));
    } while(m.find());
    m.appendTail(sb);
    return sb.toString();
}

If you are performing the operation with the same Map very often, you may consider keeping the result of Pattern.compile(pattern), as it is immutable and safely shareable.

On the other hand, if you are using this operation with different maps frequently, it might be an option to use a generic pattern instead, combined with handling the possibility that the particular variable is not in the map. The adds the option to report occurrences of the ${…} pattern with an unknown variable:

private static Pattern VARIABLE = Pattern.compile("\\$\\{([^}]*)\\}");
public static String replaceAll(String template, Map<String,String> variables) {
    Matcher m = VARIABLE.matcher(template);
    if(!m.find())
        return template;
    StringBuffer sb = new StringBuffer();
    do {
        m.appendReplacement(sb,
            Matcher.quoteReplacement(variables.getOrDefault(m.group(1), m.group(0))));
    } while(m.find());
    m.appendTail(sb);
    return sb.toString();
}

m.group(0) is the actual match, so using this as a fall-back for the replacement string establishes the original behavior of not replacing ${…} occurrences when the key is not in the map. As said, alternative behaviors, like reporting the absent key or using a different fall-back text, are possible.

3
  • the primary problem using replaceAll recursive is: maybe replacing a replaced variable. I'm not good at english, so I take an example: replacing "${a}:${b}" with {"a":"${b}","b":"c"} will output: "c:c" not "${b}:c".
    – holi-java
    Commented Apr 13, 2017 at 18:27
  • @holi-java: that’s indeed the primary problem of this pattern, even if this might not apply to the OP’s special case, as ${…} is a pattern that you might know not to appear within the replacements. But since software may change, it is correct to mention this potential problem, just like I mentioned the potential performance problem, which might not be in the current situation with small strings and small maps, but is waiting in the dark…
    – Holger
    Commented Apr 18, 2017 at 11:04
  • I agree with your statement.
    – holi-java
    Commented Apr 19, 2017 at 1:58

Not the answer you're looking for? Browse other questions tagged or ask your own question.