com.databasesandlife.util.DomVariableExpander Maven / Gradle / Ivy
Show all versions of java-common Show documentation
package com.databasesandlife.util;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import javax.annotation.Nonnull;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Takes a XML element (containing text and sub-nodes), and expands variables like ${xyz}.
*
* Usage:
*
* try {
* Map<String, String> variables = new Map<>() {{ put("foo", "value"); }};
*
* // for example <element attr="${foo}">${foo}</element>
* Element elementWithVariables = ...
*
* Document elementExpanded = DomVariableExpander.expand(
* variableStyle, variables, elementWithVariables);
* }
* catch (VariableNotFoundException e) { .. }
*
*
* For the syntax of variables, see the {@link VariableSyntax} enum.
*
* Variables in the Map passed to the expand method should not have the dollar prefix.
*
* Variable names may contain a-z, A-Z, 0-9, hypen and underscore and are case sensitive.
*
* This class is XML namespace aware.
*
* @author This source is copyright Adrian Smith and licensed under the LGPL 3.
* @see Project on GitHub
*/
public class DomVariableExpander extends IdentityForwardingSaxHandler {
@FunctionalInterface
public interface VariableGetter {
/** Gets the variable with the given name */
@Nonnull String get(@Nonnull String name);
}
public static class VariableNotFoundException extends RuntimeException {
public VariableNotFoundException(String var) { super(var); }
}
public enum VariableSyntax {
/** Accepts variables like ${foo} but NOT $foo. Variables are any chars except close bracket "}" */
dollarThenBraces {
protected final Pattern variablePattern = Pattern.compile("\\$\\{(.+?)}");
@Override public CharSequence expand(@Nonnull VariableGetter variables, CharSequence template) throws VariableNotFoundException {
var matcher = variablePattern.matcher(template);
var result = new StringBuffer();
while (matcher.find()) {
var variable = matcher.group(1); // ${xyz}
var expansion = variables.get(variable);
if (expansion == null) throw new VariableNotFoundException(
"Variable '${" + variable + "}' is used in XML template, but is missing from map of variables");
matcher.appendReplacement(result, Matcher.quoteReplacement(expansion));
}
matcher.appendTail(result);
return result;
}
},
/** Accepts variables like ${foo} or $foo. Variables are letters, numbers, underscore, hyphen. */
dollarOrDollarThenBraces {
protected final Pattern variablePattern = Pattern.compile("\\$(([\\w-]+)|\\{([\\w-]+)})");
@Override public CharSequence expand(@Nonnull VariableGetter variables, CharSequence template) throws VariableNotFoundException {
var matcher = variablePattern.matcher(template);
var result = new StringBuffer();
while (matcher.find()) {
var variable = matcher.group(2); // $xyz
if (variable == null) variable = matcher.group(3); // ${xyz}
var expansion = variables.get(variable);
if (expansion == null) throw new VariableNotFoundException(
"Variable '$" + variable + "' is used in XML template, but is missing from map of variables");
matcher.appendReplacement(result, Matcher.quoteReplacement(expansion));
}
matcher.appendTail(result);
return result;
}
};
public abstract CharSequence expand(@Nonnull VariableGetter variables, CharSequence template) throws VariableNotFoundException;
}
protected final VariableSyntax syntax;
protected final @Nonnull VariableGetter variables;
public DomVariableExpander(VariableSyntax syntax, @Nonnull VariableGetter variables, TransformerHandler outputHandler) {
super(outputHandler);
this.syntax = syntax;
this.variables = variables;
}
@Override public void startElement(String uri, String localName, String el, Attributes templateAttributes) throws SAXException {
var expandedAttributes = new AttributesImpl(templateAttributes);
for (var a = 0; a < templateAttributes.getLength(); a++) {
var valueTemplate = templateAttributes.getValue(a);
var replacement = syntax.expand(variables, valueTemplate);
expandedAttributes.setValue(a, replacement.toString());
}
super.startElement(uri, localName, el, expandedAttributes);
}
@Override public void characters(char[] ch, int start, int length) throws SAXException {
var templateCharacters = new String(ch, start, length);
var expandedCharacters = syntax.expand(variables, templateCharacters);
super.characters(expandedCharacters.toString().toCharArray(), 0, expandedCharacters.length());
}
public static Document expand(Node prototypeElement, Function expander)
throws TransformerException {
try {
var systemProperties = System.getProperties();
systemProperties.remove("javax.xml.transform.TransformerFactory");
// The resulting DOM
var result = new DOMResult();
// SAX identity transformer to populate the resulting DOM
var writerFactory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
var toResult = writerFactory.newTransformerHandler(); // Identity transform
toResult.setResult(result);
// Our transformer which expands variables into the above identity transformer
var intoExpander = new SAXResult(expander.apply(toResult));
// Perform the chain of transformations, and populate "result"
var source = new DOMSource(prototypeElement);
var transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(source, intoExpander);
return (Document) result.getNode();
}
catch (TransformerConfigurationException e) { throw new RuntimeException(e); }
}
public static Document expand(VariableSyntax syntax, @Nonnull VariableGetter variables, Node prototypeElement)
throws VariableNotFoundException {
try {
return expand(prototypeElement, toResult -> new DomVariableExpander(syntax, variables, toResult));
}
catch (TransformerConfigurationException e) { throw new RuntimeException(e); }
catch (TransformerException e) {
if (e.getCause() instanceof VariableNotFoundException) throw (VariableNotFoundException) e.getCause();
throw new RuntimeException(e);
}
}
}