Skip to content

Instantly share code, notes, and snippets.

@zmeeagain
Created March 14, 2020 22:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zmeeagain/3de7b59bc057be8f45efe4cace674479 to your computer and use it in GitHub Desktop.
Save zmeeagain/3de7b59bc057be8f45efe4cace674479 to your computer and use it in GitHub Desktop.
Simple templates processor with example context.
package templates;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import javax.annotation.Nullable;
/**
* A map-backed dictionary, that skips null values.
*/
public class TemplateContext {
private Map<String, Object> variables = new HashMap<>();
public static TemplateContext from(String key, @Nullable Object value) {
TemplateContext context = new TemplateContext();
if (value != null) {
context.put(key, value);
}
return context;
}
public static TemplateContext from(Map<String, Object> map) {
return new TemplateContext().putAll(map);
}
public boolean has(String key) {
return variables.containsKey(key);
}
@Nullable
public Object get(String key) {
return variables.get(key);
}
public TemplateContext put(String key, @Nullable Object value) {
if (value != null) {
variables.put(key, value);
}
return this;
}
public TemplateContext putAll(Map<String, Object> variables) {
for (Map.Entry<String, Object> entry : variables.entrySet()) {
if (entry.getValue() != null) {
this.variables.put(entry.getKey(), entry.getValue());
}
}
return this;
}
public TemplateContext putAllFrom(TemplateContext context) {
this.variables.putAll(context.asMap());
return this;
}
public Map<String, Object> asMap() {
return Collections.unmodifiableMap(variables);
}
public TemplateContext clear() {
variables.clear();
return this;
}
}
package templates;
public final class TemplateContexts {
private TemplateContexts() {}
public static TemplateContext fromDeviceInfo(DeviceInfo info) {
return new TemplateContext()
.put("device-brand", info.getBrand())
.put("device-device", info.getDevice())
.put("device-fingerprint", info.getFingerprint())
.put("device-manufacturer", info.getManufacturer())
.put("device-model", info.getModel())
.put("device-product", info.getProduct());
}
}
package templates;
import java.io.IOException;
import java.io.Writer;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Stateful, cannot be used from multiple threads. However, it can be used to process a series of
* templates to the same output, e.g.
*
* <blockquote>
*
* <pre>
* TemplateProcessor p = new TemplateProcessor();
* p.putContext(context);
* Writer writer = ...
* p.process(template1, writer);
* p.putContext(addictionContext);
* p.process(template2, writer);
* </pre>
*
* </blockquote>
*
* Alternatively, if no out is supplied, the result will be returned as one string:
*
* <blockquote>
*
* <pre>
* TemplateProcessor p = new TemplateProcessor();
* p.putContext(context);
* String result = p.process(template);
* </pre>
*
* </blockquote>
*/
public class TemplateProcessor {
@SuppressWarnings("all" /* Android needs redundant escapes */)
private static final Pattern VAR_PATTERN =
Pattern.compile("\\$\\{(\\p{Alpha}[\\p{Graph}&&[^\\{\\}]]*)\\}");
// If null, missing variables stay in processed templates, i.e. if var is missing in context,
// then ${var} will stay as ${var} in output.
@Nullable
private Function<String, String> missingHandler = null;
private TemplateContext context = new TemplateContext();
private Matcher matcher;
private int appendPos = 0;
public TemplateProcessor putContext(TemplateContext context) {
this.context.putAllFrom(context);
return this;
}
public TemplateProcessor setContext(TemplateContext context) {
this.context = context;
return this;
}
public TemplateContext getContext() {
return context;
}
public TemplateProcessor omitMissing() {
return setMissingHandler(varName -> "");
}
public TemplateProcessor leaveMissing() {
return setMissingHandler(null);
}
public TemplateProcessor replaceMissing(String replacement) {
return setMissingHandler(varName -> replacement);
}
public String process(String template) {
StringBuilder builder = new StringBuilder();
try {
process(template, builder);
} catch (IOException e) {
// No IOException for StringBuilder
}
return builder.toString();
}
public void process(String template, Writer out) throws IOException {
process(template, (Appendable) out);
}
public void process(String template, Appendable out) throws IOException {
appendPos = 0; // reset in case processor is repeatedly called
matcher = VAR_PATTERN.matcher(template);
while (matcher.find()) {
MatchResult matchResult = matcher.toMatchResult();
String varPlaceholder = matchResult.group();
String varName = matchResult.group(1);
@Nullable Object varValue = context.get(varName);
String replacement = getReplacement(varPlaceholder, varName, varValue);
appendReplacement(template, replacement, out);
}
appendRemaining(template, out);
}
private TemplateProcessor setMissingHandler(@Nullable Function<String, String> handler) {
this.missingHandler = handler;
return this;
}
private String getReplacement(String varPlaceholder, String varName, @Nullable Object varValue) {
if (varValue == null) {
return missingHandler == null ? varPlaceholder : missingHandler.apply(varName);
}
return String.valueOf(varValue);
}
private void appendReplacement(String template, String replacement, Appendable out)
throws IOException {
out.append(template.substring(appendPos, matcher.start()));
out.append(replacement);
appendPos = matcher.end();
}
private void appendRemaining(String template, Appendable out) throws IOException {
out.append(template.substring(appendPos));
}
public static void main(String[] args) {
System.out.println(new TemplateProcessor()
.setContext(TemplateContext.from("one", "oneval"))
.omitMissing()
.process("this ${one} and the ${other} one"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment