Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

StrSubstitutor replacement with JRE libraries

Tags:

java

string

text

At the moment I am using org.apache.commons.lang.text.StrSubstitutor for doing:

Map m = ...
substitutor = new StrSubstitutor(m);

result = substitutor.replace(input);

Given the fact I want to remove commons-lang dependency from my project what would be a working and minimalistic implementation of StrSubstitutor using standard JRE libraries?

Note:

StrSubstitutor works like this:

Map map = new HashMap();
map.put("animal", "quick brown fox");
map.put("target", "lazy dog");
StrSubstitutor sub = new StrSubstitutor(map);
String resolvedString = sub.replace("The ${animal} jumped over the ${target}.");

yielding resolvedString = "The quick brown fox jumped over the lazy dog."

like image 821
Luigi R. Viggiano Avatar asked Dec 26 '12 18:12

Luigi R. Viggiano


2 Answers

If performance is not a priority, you can use the appendReplacement method of the Matcher class:

public class StrSubstitutor {
    private Map<String, String> map;
    private static final Pattern p = Pattern.compile("\\$\\{(.+?)\\}");

    public StrSubstitutor(Map<String, String> map) {
        this.map = map;
    }

    public String replace(String str) {
        Matcher m = p.matcher(str);
        StringBuilder sb = new StringBuilder();
        while (m.find()) {
            String var = m.group(1);
            String replacement = map.get(var);
            m.appendReplacement(sb, replacement);
        }
        m.appendTail(sb);
        return sb.toString();
    }
}

A more performant but uglier version, just for fun :)

    public String replace(String str) {
        StringBuilder sb = new StringBuilder();
        char[] strArray = str.toCharArray();
        int i = 0;
        while (i < strArray.length - 1) {
            if (strArray[i] == '$' && strArray[i + 1] == '{') {
                i = i + 2;
                int begin = i;
                while (strArray[i] != '}') ++i;
                sb.append(map.get(str.substring(begin, i++)));
            } else {
                sb.append(strArray[i]);
                ++i;
            }
        }
        if (i < strArray.length) sb.append(strArray[i]);
        return sb.toString();
    }

It's about 2x as fast as the regex version and 3x faster than the apache commons version as per my tests. So the normal regex stuff is actually more optimized than the apache version. Usually not worth it of course. Just for fun though, let me know if you can make it more optimized.

Edit: As @kmek points out, there is a caveat. Apache version will resolve transitively. e.g, If ${animal} maps to ${dog} and dog maps to Golden Retriever, apache version will map ${animal} to Golden Retriever. As I said, you should use libraries as far as possible. The above solution is only to be used if you have a special constraint which does not allow you to use a library.

like image 57
Hari Menon Avatar answered Oct 26 '22 22:10

Hari Menon


there's nothing like this that i know of in the JRE, but writing one is simple enough.

Pattern p = Pattern.compile("${([a-zA-Z]+)}";
Matcher m = p.matcher(inputString);
int lastEnd = -1;
while (m.find(lastEnd+1)) {
   int startIndex = m.start();
   String varName = m.group(1);
   //lookup value in map and substitute
   inputString = inputString.substring(0,m.start())+replacement+inputString.substring(m.end());
   lastEnt = m.start() + replacement.size();
}

this is of course horribly inefficient and you should probably write the result into a StringBuilder instead of replacing inputString all the time

like image 44
radai Avatar answered Oct 27 '22 00:10

radai