Skip to content

Commit

Permalink
make placeholder argument list parsing more configurable
Browse files Browse the repository at this point in the history
see #178
  • Loading branch information
bodewig committed May 6, 2020
1 parent 121b535 commit 22b014a
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 33 deletions.
Expand Up @@ -44,19 +44,36 @@
* (PlaceholderDifferenceEvaluatorTest).</p>
*
* <p>Default delimiters for placeholder are <code>${</code> and
* <code>}</code>. To use custom delimiters (in regular expression),
* create instance with the
* <code>PlaceholderDifferenceEvaluator(String
* placeholderOpeningDelimiterRegex, String
* placeholderClosingDelimiterRegex)</code> constructor.</p>
* <code>}</code>. Arguments to placeholders are by default enclosed
* in {@code (} and {@code )} and separated by {@code ,} - whitespace
* is significant, arguments are not quoted.</p>
*
* <p>To use custom delimiters (in regular expression), create
* instance with the {@link PlaceholderDifferenceEvaluator(String,
* String)} or {@link PlaceholderDifferenceEvaluator(String, String,
* String, String, String)} constructors.</p>
*
* @since 2.6.0
*/
public class PlaceholderDifferenceEvaluator implements DifferenceEvaluator {
public static final String PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX = Pattern.quote("${");
public static final String PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX = Pattern.quote("}");
/**
* @since 2.7.0
*/
public static final String PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX = Pattern.quote("(");
/**
* @since 2.7.0
*/
public static final String PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX = Pattern.quote(")");
/**
* @since 2.7.0
*/
public static final String PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX = Pattern.quote(",");

private static final String PLACEHOLDER_PREFIX_REGEX = Pattern.quote("xmlunit.");
private static final Map<String, PlaceholderHandler> KNOWN_HANDLERS;
private static final String[] NO_ARGS = new String[0];

static {
Map<String, PlaceholderHandler> m = new HashMap<String, PlaceholderHandler>();
Expand All @@ -67,6 +84,8 @@ public class PlaceholderDifferenceEvaluator implements DifferenceEvaluator {
}

private final Pattern placeholderRegex;
private final Pattern argsRegex;
private final String argsSplitter;

/**
* Creates a PlaceholderDifferenceEvaluator with default
Expand All @@ -88,8 +107,42 @@ public PlaceholderDifferenceEvaluator() {
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
*/
public PlaceholderDifferenceEvaluator(final String placeholderOpeningDelimiterRegex,
final String placeholderClosingDelimiterRegex) {
this(placeholderOpeningDelimiterRegex, placeholderClosingDelimiterRegex, null, null, null);
}

/**
* Creates a PlaceholderDifferenceEvaluator with custom delimiters.
* @param placeholderOpeningDelimiterRegex regular expression for
* the opening delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderClosingDelimiterRegex regular expression for
* the closing delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsOpeningDelimiterRegex regular expression for
* the opening delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsClosingDelimiterRegex regular expression for
* the closing delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsSeparatorRegex regular expression for the
* delimiter between arguments inside of the placeholder's
* argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX}
* if the parameter is null or blank
*
* @since 2.7.0
*/
public PlaceholderDifferenceEvaluator(String placeholderOpeningDelimiterRegex,
String placeholderClosingDelimiterRegex) {
String placeholderClosingDelimiterRegex,
String placeholderArgsOpeningDelimiterRegex,
String placeholderArgsClosingDelimiterRegex,
String placeholderArgsSeparatorRegex) {
if (placeholderOpeningDelimiterRegex == null
|| placeholderOpeningDelimiterRegex.trim().length() == 0) {
placeholderOpeningDelimiterRegex = PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX;
Expand All @@ -98,10 +151,26 @@ public PlaceholderDifferenceEvaluator(String placeholderOpeningDelimiterRegex,
|| placeholderClosingDelimiterRegex.trim().length() == 0) {
placeholderClosingDelimiterRegex = PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX;
}
if (placeholderArgsOpeningDelimiterRegex == null
|| placeholderArgsOpeningDelimiterRegex.trim().length() == 0) {
placeholderArgsOpeningDelimiterRegex = PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX;
}
if (placeholderArgsClosingDelimiterRegex == null
|| placeholderArgsClosingDelimiterRegex.trim().length() == 0) {
placeholderArgsClosingDelimiterRegex = PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX;
}
if (placeholderArgsSeparatorRegex == null
|| placeholderArgsSeparatorRegex.trim().length() == 0) {
placeholderArgsSeparatorRegex = PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX;
}

placeholderRegex = Pattern.compile("(\\s*" + placeholderOpeningDelimiterRegex
+ "\\s*" + PLACEHOLDER_PREFIX_REGEX + "(.+)" + "\\s*"
+ placeholderClosingDelimiterRegex + "\\s*)");
argsRegex = Pattern.compile("((.*)\\s*" + placeholderArgsOpeningDelimiterRegex
+ "(.+)"
+ "\\s*" + placeholderArgsClosingDelimiterRegex + "\\s*)");
argsSplitter = placeholderArgsSeparatorRegex;
}

public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
Expand Down Expand Up @@ -227,39 +296,36 @@ private ComparisonResult evaluateAttributeListLengthConsideringPlaceholders(Comp

private ComparisonResult evaluateConsideringPlaceholders(String controlText, String testText,
ComparisonResult outcome) {
Matcher m = placeholderRegex.matcher(controlText);
if (m.find()) {
String keyword = m.group(2).trim();
final Matcher placeholderMatcher = placeholderRegex.matcher(controlText);
if (placeholderMatcher.find()) {
final String content = placeholderMatcher.group(2).trim();
final Matcher argsMatcher = argsRegex.matcher(content);
final String keyword;
final String[] args;
if (argsMatcher.find()) {
keyword = argsMatcher.group(2).trim();
args = argsMatcher.group(3).split(argsSplitter);
} else {
keyword = content;
args = NO_ARGS;
}
if (isKnown(keyword)) {
if (!m.group(1).trim().equals(controlText.trim())) {
if (!placeholderMatcher.group(1).trim().equals(controlText.trim())) {
throw new RuntimeException("The placeholder must exclusively occupy the text node.");
}
return evaluate(keyword, testText);
return evaluate(keyword, testText, args);
}
}

// no placeholder at all or unknown keyword
return outcome;
}

private boolean isKnown(String keyword) {
// extract placeholder name if parameters present
Pattern pattern = Pattern.compile("(\\w+)\\(?");
Matcher matcher = pattern.matcher(keyword);
if (matcher.find()) {
return KNOWN_HANDLERS.containsKey(matcher.group(1));
}
return false;
private boolean isKnown(final String keyword) {
return KNOWN_HANDLERS.containsKey(keyword);
}

private ComparisonResult evaluate(String keyword, String testText) {
Pattern pattern = Pattern.compile("^(\\w+)(?:\\((.+)\\))?$");
Matcher matcher = pattern.matcher(keyword);
String placeholderParam = "";
if (matcher.find()) {
keyword = matcher.group(1);
placeholderParam = matcher.group(2);
}
return KNOWN_HANDLERS.get(keyword).evaluate(testText, placeholderParam);
private ComparisonResult evaluate(final String keyword, final String testText, final String[] args) {
return KNOWN_HANDLERS.get(keyword).evaluate(testText, args);
}
}
Expand Up @@ -57,8 +57,47 @@ D withPlaceholderSupport(D configurer) {
public static <D extends DifferenceEngineConfigurer<D>>
D withPlaceholderSupportUsingDelimiters(D configurer, String placeholderOpeningDelimiterRegex,
String placeholderClosingDelimiterRegex) {
return withPlaceholderSupportUsingDelimiters(configurer, placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex, null, null, null);
}

/**
* Adds placeholder support to a {@link DifferenceEngineConfigurer}.
* @param configurer the configurer to add support to
* @param placeholderOpeningDelimiterRegex regular expression for
* the opening delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderClosingDelimiterRegex regular expression for
* the closing delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsOpeningDelimiterRegex regular expression for
* the opening delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsClosingDelimiterRegex regular expression for
* the closing delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsSeparatorRegex regular expression for the
* delimiter between arguments inside of the placeholder's
* argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX}
* if the parameter is null or blank
* @return the configurer with placeholder support added in
* @since 2.7.0
*/
public static <D extends DifferenceEngineConfigurer<D>>
D withPlaceholderSupportUsingDelimiters(final D configurer,
final String placeholderOpeningDelimiterRegex,
final String placeholderClosingDelimiterRegex,
final String placeholderArgsOpeningDelimiterRegex,
final String placeholderArgsClosingDelimiterRegex,
final String placeholderArgsSeparatorRegex) {
return configurer.withDifferenceEvaluator(new PlaceholderDifferenceEvaluator(placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex));
placeholderClosingDelimiterRegex, placeholderArgsOpeningDelimiterRegex,
placeholderArgsClosingDelimiterRegex, placeholderArgsSeparatorRegex));
}

/**
Expand Down Expand Up @@ -93,8 +132,50 @@ D withPlaceholderSupportChainedAfter(D configurer, DifferenceEvaluator evaluator
public static <D extends DifferenceEngineConfigurer<D>>
D withPlaceholderSupportUsingDelimitersChainedAfter(D configurer, String placeholderOpeningDelimiterRegex,
String placeholderClosingDelimiterRegex, DifferenceEvaluator evaluator) {
return withPlaceholderSupportUsingDelimitersChainedAfter(configurer, placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex, null, null, null, evaluator);
}

/**
* Adds placeholder support to a {@link DifferenceEngineConfigurer} considering an additional {@link DifferenceEvaluator}.
*
* @param configurer the configurer to add support to
* @param placeholderOpeningDelimiterRegex regular expression for
* the opening delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderClosingDelimiterRegex regular expression for
* the closing delimiter of placeholder, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param evaluator the additional evaluator - placeholder support is
* {@link DifferenceEvaluators#chain chain}ed after the given
* evaluator
* @param placeholderArgsOpeningDelimiterRegex regular expression for
* the opening delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsClosingDelimiterRegex regular expression for
* the closing delimiter of the placeholder's argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX}
* if the parameter is null or blank
* @param placeholderArgsSeparatorRegex regular expression for the
* delimiter between arguments inside of the placeholder's
* argument list, defaults to {@link
* PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX}
* if the parameter is null or blank
*/
public static <D extends DifferenceEngineConfigurer<D>>
D withPlaceholderSupportUsingDelimitersChainedAfter(D configurer,
final String placeholderOpeningDelimiterRegex,
final String placeholderClosingDelimiterRegex,
final String placeholderArgsOpeningDelimiterRegex,
final String placeholderArgsClosingDelimiterRegex,
final String placeholderArgsSeparatorRegex,
final DifferenceEvaluator evaluator) {
return configurer.withDifferenceEvaluator(DifferenceEvaluators.chain(
evaluator, new PlaceholderDifferenceEvaluator(placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex)));
placeholderClosingDelimiterRegex, placeholderArgsOpeningDelimiterRegex,
placeholderArgsClosingDelimiterRegex, placeholderArgsSeparatorRegex)));
}
}
Expand Up @@ -21,7 +21,7 @@
* and any API may change between releases of XMLUnit.</b></p>
*
* <p>The placeholder feature allows a placeholder sequence of {@code
* ${xmlunit.KEYWORD}} to be used as nested text in elements or as
* ${xmlunit.KEYWORD(args...)}} to be used as nested text in elements or as
* attribute values of the control document and trigger special
* handling based on the keyword.</p>
*
Expand All @@ -31,17 +31,32 @@
* {@code java.util.ServiceLoader} so it is possible to extend the set
* of handlers via your own modules.</p>
*
* <p>The placeholder sequence can take any number of string values as
* arguments in the form {@code ${xmlunit.KEYWORD(args1,arg2)}} - if
* no arguments are used the parentheses can be omitted
* completely. Arguments are not quoted, whitespace inside of the
* argument list is significant. All separators (by default
* <code>${</code>, <code>}</code>, {@code (}, {@code )}, and {@code
* ,}) can be configured explicitly.</p>
*
* <p>Keywords currently supported by built-in handlers are:</p>
*
* <ul>
*
* <li>{@code ${xmlunit.ignore}} which makes XMLUnit ignore the nested
* text or attribute completely. This is handled by {@link
* IgnorePlaceholderHandler}.</li>
*
* <li>{@code ${xmlunit.isNumber}} makes the comparison pass if the
* textual content of the element or attributes looks like a
* number. This is handled by {@link IsNumberPlaceholderHandler}.</li>
*
* <li>{@code ${xmlunit.matchesRegex}} makes the comparison pass if
* the textual content of the element or attribute matches the regular
* expression specified as the first (and only) argument. If there is
* no argument at all, the comparison will fail. This is handled by
* {@link MatchesRegexPlaceholderHandler}.</li>
*
* </ul>
*
* <p>The default delimiters of <code>${</code> and <code>}</code> can
Expand Down
Expand Up @@ -20,6 +20,7 @@

import javax.xml.namespace.QName;
import java.util.Iterator;
import java.util.regex.Pattern;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*;
Expand Down Expand Up @@ -393,10 +394,10 @@ public void hasMatchesRegexPlaceholder_Element_NotMatches() {

@Test
public void hasMatchesRegexPlaceholder_Element_Exception_MalformedRegex() {
String control = "<elem1>${xmlunit.matchesRegex(^(\\d+$)}</elem1>";
String control = "<elem1>${xmlunit.matchesRegex[^(\\d+$]}</elem1>";
String test = "<elem1>23abc</elem1>";
DiffBuilder diffBuilder = DiffBuilder.compare(control).withTest(test)
.withDifferenceEvaluator(new PlaceholderDifferenceEvaluator());
.withDifferenceEvaluator(new PlaceholderDifferenceEvaluator(null, null, Pattern.quote("["), Pattern.quote("]"), null));

try {
diffBuilder.build();
Expand Down

0 comments on commit 22b014a

Please sign in to comment.