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

Amit
  • 30,756
  • 6
  • 57
  • 88
user3727540
  • 922
  • 3
  • 11
  • 30
  • 5
    `forEach` runs your command on each element, but you are not capturing the result. – khelwood Apr 12 '17 at 13:52
  • 1
    Also, if you're not trying to use a regular expression, it would be easier to use [`replace`](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#replace(java.lang.CharSequence,%20java.lang.CharSequence)), not [`replaceAll`](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#replaceAll(java.lang.String,%20java.lang.String)) – khelwood Apr 12 '17 at 13:53
  • Thanks. So can you show me the right way to do it with Java8? – user3727540 Apr 12 '17 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 Apr 12 '17 at 13:57
  • You are right. I've just edited. – user3727540 Apr 12 '17 at 13:58
  • @khelwood how do you capture the result inside the forEach in java8? – user3727540 Apr 12 '17 at 14:03
  • replaceAll will not alter the actual value. In java 8 you can pass only the final or effective final value for processing. You cannot alter the out side variable in foreach. – Vijayakumar Apr 12 '17 at 14:04
  • @Vijay thanks for clarifying this. Can you show me how to achieve the result I want with java8? – user3727540 Apr 12 '17 at 14:07
  • JavaScript has this from the beginning, unfortunately, Java doesn't. Actually Java's `String.repalceAll` only needs a small upgrade then we can have it :( – Zhou Oct 31 '20 at 16:55

6 Answers6

20

Java 8

The proper way to implement this has not changed 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.

Java 9 and above

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

Didier L
  • 18,905
  • 10
  • 61
  • 103
  • 3
    this is actually much nicer IMO then both *broken* versions of the up-voted answers. plus one – Eugene Apr 12 '17 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 Jun 07 '18 at 10:43
  • 1
    Thanks @drrob, I have included notes about it in the answer. – Didier L Jun 07 '18 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);
holi-java
  • 29,655
  • 7
  • 72
  • 83
  • 3
    @holi-java you have broken associativity here, this does not work correctly in parallel; even if you have provided the `combiner`. – Eugene Apr 12 '17 at 17:01
  • 1
    The definitions of `identity` and `accumulator` given here are completely wrong. [From the javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-U-java.util.function.BiFunction-java.util.function.BinaryOperator-): _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 Apr 13 '17 at 07: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 Apr 13 '17 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 Apr 13 '17 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);
Community
  • 1
  • 1
Mikhail Antonov
  • 1,297
  • 3
  • 21
  • 29
  • Thanks, can you please explain what (Function) stand for? – user3727540 Apr 12 '17 at 14:14
  • Moreover, can you also explain the reduce part? Thanks :) – user3727540 Apr 12 '17 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) – Jorn Vernee Apr 12 '17 at 14:18
  • @JornVernee it is indeed! Fixed. – Mikhail Antonov Apr 12 '17 at 14:21
  • 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 Apr 12 '17 at 14:26
  • What kind of a monstrous string would you need to handle it in parallel :) – Mikhail Antonov Apr 12 '17 at 14:31
  • 4
    The solution with [`reduce()`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-T-java.util.function.BinaryOperator-) 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](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Associativity) function._ – Didier L Apr 12 '17 at 16:14
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)));
Vijayakumar
  • 303
  • 4
  • 10
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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 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 Apr 13 '17 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 Apr 18 '17 at 11:04
  • I agree with your statement. – holi-java Apr 19 '17 at 01:58
1

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)))
Jesse Glick
  • 24,539
  • 10
  • 90
  • 112