/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>TemplateParser</code> is a simple string parser that will substitute tokens in a string with values supplied in a Map.
*/
public final class TemplateParser {
public static final String ANT_TEMPLATE_STYLE = "ant";
public static final String SQUARE_TEMPLATE_STYLE = "square";
public static final String CURLY_TEMPLATE_STYLE = "curly";
public static final String WIGGLY_MULE_TEMPLATE_STYLE = "mule";
private static final String NULL_AS_STRING = "null";
private static final Map<String, PatternInfo> patterns = new HashMap<>();
static {
patterns.put(ANT_TEMPLATE_STYLE, new PatternInfo(ANT_TEMPLATE_STYLE, "\\$\\{[^\\{\\}]+\\}", "${", "}"));
patterns.put(SQUARE_TEMPLATE_STYLE, new PatternInfo(SQUARE_TEMPLATE_STYLE, "\\[[^\\[\\]]+\\]", "[", "]"));
patterns.put(CURLY_TEMPLATE_STYLE, new PatternInfo(CURLY_TEMPLATE_STYLE, "\\{[^\\{\\}}]+\\}", "{", "}"));
// Such a complex regex is needed to support nested expressions, otherwise we
// have to do this manually or using an ANTLR grammar etc.
// Support for 6 levels (5 nested)
patterns.put(WIGGLY_MULE_TEMPLATE_STYLE,
new PatternInfo(WIGGLY_MULE_TEMPLATE_STYLE,
"#\\[((?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[.*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?)\\]",
"#[", "]"));
}
/**
* logger used by this class
*/
protected static final Logger logger = LoggerFactory.getLogger(TemplateParser.class);
private final Pattern pattern;
private final int pre;
private final int post;
private final PatternInfo style;
public static TemplateParser createAntStyleParser() {
return new TemplateParser(ANT_TEMPLATE_STYLE);
}
public static TemplateParser createSquareBracesStyleParser() {
return new TemplateParser(SQUARE_TEMPLATE_STYLE);
}
public static TemplateParser createMuleStyleParser() {
return new TemplateParser(WIGGLY_MULE_TEMPLATE_STYLE);
}
private TemplateParser(String styleName) {
this.style = patterns.get(styleName);
if (this.style == null) {
throw new IllegalArgumentException("Unknown template style: " + styleName);
}
pattern = style.getPattern();
pre = style.getPrefix().length();
post = style.getSuffix().length();
}
/**
* Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
* template is left as is in the return String
*
* @param props the key/value pairs to match against
* @param template the string containing the template place holders i.e. My name is ${name}
* @return the parsed String
*/
public String parse(Map<?, ?> props, String template) {
return parse(props, template, null);
}
/**
* Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
* template is left as is in the return String
*
* @param callback a callback used to resolve the property name
* @param template the string containing the template place holders i.e. My name is ${name}
* @return the parsed String
*/
public String parse(TemplateCallback callback, String template) {
return parse(null, template, callback);
}
protected String parse(Map<?, ?> props, String template, TemplateCallback callback) {
String result = template;
Map<?, ?> newProps = props;
if (props != null && !(props instanceof CaseInsensitiveHashMap)) {
newProps = new CaseInsensitiveHashMap(props);
}
Matcher m = pattern.matcher(result);
while (m.find()) {
Object value = null;
String match = m.group();
String propname = match.substring(pre, match.length() - post);
if (callback != null) {
value = callback.match(propname);
if (value == null) {
value = NULL_AS_STRING;
}
} else if (newProps != null) {
value = newProps.get(propname);
}
if (value == null) {
if (logger.isDebugEnabled()) {
logger.debug("Value " + propname + " not found in context");
}
} else {
String matchRegex = Pattern.quote(match);
String valueString = value.toString();
valueString = replaceBackSlash(valueString);
valueString = replaceDollarSign(valueString);
result = result.replaceAll(matchRegex, valueString);
}
}
return result;
}
private String replaceDollarSign(String valueString) {
if (valueString.indexOf('$') != -1) {
valueString = valueString.replace("$", "\\$");
}
return valueString;
}
private String replaceBackSlash(String valueString) {
if (valueString.indexOf("\\") != -1) {
valueString = valueString.replace("\\", "\\\\");
}
return valueString;
}
/**
* Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
* template is left as is in the return String
*
* @param props the key/value pairs to match against
* @param templates A List of templates
* @return the parsed String
*/
public List<?> parse(Map<?, ?> props, List<?> templates) {
if (templates == null) {
return new ArrayList<>();
}
List<String> list = new ArrayList<>(templates.size());
templates.stream().map(tmpl -> parse(props, tmpl.toString())).forEach(list::add);
return list;
}
/**
* Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
* template is left as is in the return String
*
* @param props the key/value pairs to match against
* @param templates A Map of templates. The values for each map entry will be parsed
* @return the parsed String
*/
public Map<?, ?> parse(final Map<?, ?> props, Map<?, ?> templates) {
return parse(token -> props.get(token), templates);
}
public Map<?, ?> parse(TemplateCallback callback, Map<?, ?> templates) {
if (templates == null) {
return new HashMap<>();
}
Map<Object, String> map = new HashMap<>(templates.size());
for (Map.Entry<?, ?> entry : templates.entrySet()) {
map.put(entry.getKey(), parse(callback, entry.getValue().toString()));
}
return map;
}
public PatternInfo getStyle() {
return style;
}
public boolean isContainsTemplate(String value) {
if (value == null) {
return false;
}
Matcher m = pattern.matcher(value);
return m.find();
}
public boolean isValid(String expression) {
try {
style.validate(expression);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public void validate(String expression) throws IllegalArgumentException {
style.validate(expression);
}
@FunctionalInterface
public interface TemplateCallback {
Object match(String token);
}
public static class PatternInfo {
String name;
String regEx;
String prefix;
String suffix;
PatternInfo(String name, String regEx, String prefix, String suffix) {
this.name = name;
this.regEx = regEx;
if (prefix.length() < 1 || prefix.length() > 2) {
throw new IllegalArgumentException("Prefix can only be one or two characters long: " + prefix);
}
this.prefix = prefix;
if (suffix.length() != 1) {
throw new IllegalArgumentException("Suffix can only be one character long: " + suffix);
}
this.suffix = suffix;
}
public String getRegEx() {
return regEx;
}
public String getPrefix() {
return prefix;
}
public String getSuffix() {
return suffix;
}
public String getName() {
return name;
}
public Pattern getPattern() {
return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
}
public void validate(String expression) throws IllegalArgumentException {
String currentExpression = expression;
int lastMatchIdx = 0;
while (lastMatchIdx < expression.length()) {
int start = currentExpression.indexOf(prefix);
if (start == -1) {
// no more expressions to validate
break;
}
lastMatchIdx += start;
currentExpression = currentExpression.substring(start);
Matcher m = getPattern().matcher(currentExpression);
boolean found = m.find();
if (found) {
if (!currentExpression.startsWith(m.group())) {
throw new IllegalArgumentException("Invalid Expression");
}
int matchSize = m.group().length();
lastMatchIdx += matchSize;
currentExpression = currentExpression.substring(matchSize);
} else {
throw new IllegalArgumentException("Invalid Expression");
}
}
}
}
}