While reviewing Sending templatized e-mail to a million contacts, I wrote this implementation to illustrate an alternate approach. It is designed to be the fastest possible way to generate templated text repeatedly. Is it?
I used the in-memory Java compiler featured in this Stack Overflow answer.
I think that the stringLiteral()
function and the try-catch block that performs the compilation are rather ugly.
Template.java
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
public interface Template {
public void write(Writer out, Map<String, String> params) throws IOException;
}
TemplateCompiler.java
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.tools.*;
import org.mdkt.compiler.InMemoryJavaCompiler;
public class TemplateCompiler {
private static final Pattern SUBST_PAT = Pattern.compile(
"(?<LITERAL>.*?)(?:\\{\\{(?<SUBST>[^}]*)\\}\\})"
);
/**
* Instantiates a <code>Template</code> that performs simple
* text substitutions for <code>{{PLACEHOLDERS}}</code>.
*/
public static Template compile(String templateText) {
int rest = 0;
StringBuilder script = new StringBuilder(
"import java.io.IOException;\n" +
"import java.io.Writer;\n" +
"import java.util.Map;\n" +
"public class C implements Template {\n" +
" public void write(Writer out, Map<String, String> params) throws IOException {\n"
);
for (Matcher m = SUBST_PAT.matcher(templateText); m.find(); rest = m.end()) {
script.append("out.write(")
.append(stringLiteral(m.group("LITERAL")))
.append(");\nout.write(params.get(")
.append(stringLiteral(m.group("SUBST")))
.append("));\n");
}
script.append("out.write(")
.append(stringLiteral(templateText.substring(rest)))
.append(");\n");
script.append("out.flush();\n")
.append("}}");
try {
@SuppressWarnings("unchecked")
Class <? extends Template> c = (Class <? extends Template>)InMemoryJavaCompiler.compile("C", script.toString());
Constructor<? extends Template> ctr = c.getConstructor();
return ctr.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static final Pattern UNSAFE_CHARS = Pattern.compile("[^A-Za-z0-9 ]");
private static String stringLiteral(String s) {
StringBuffer result = new StringBuffer("\"");
Matcher matcher = UNSAFE_CHARS.matcher(s);
while (matcher.find()) {
char c = matcher.group().charAt(0);
switch (c) {
// JLS SE7 3.10.5:
// It is a compile-time error for a line terminator to appear
case '\r':
matcher.appendReplacement(result, "\\r");
break;
case '\n':
matcher.appendReplacement(result, "\\n");
break;
default:
String.format("\\\\u%04x", (int)c);
}
}
matcher.appendTail(result);
result.append("\"");
return result.toString();
}
}
Sample usage
Template t = TemplateCompiler.compile( "Dear {{USER_NAME}},\n\n" + "According to our records, your phone number is {{USER_PHONE}} and " + "your e-mail address is {{USER_EMAIL}}. If this is incorrect, please " + "go to {{LOGIN_URL}} and update your contact information." ); for (Contact c : contacts) { Map<String, String> params = new HashMap<>(); params.put("USER_NAME", c.getUserName()); params.put("USER_EMAIL", c.getEmail()); params.put("USER_PHONE", c.getPhone()); params.put("LOGIN_URL", c.getLoginUrl()); StringWriter sw = new StringWriter(); t.write(sw, params); sw.toString(); }
{{}}
have been set/replaced. One step further would be to test whether there are some malformed instances like{{}
. On an easier level this could be checked with a (unit)test dataset against your expectation which could even have caught the errors @rofl reported. – Nobody Aug 31 at 12:41