-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
JBEHAVE-1388 Introduce Expressions API
- Loading branch information
Showing
32 changed files
with
1,231 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
jbehave-core/src/main/java/org/jbehave/core/expressions/BiArgExpressionProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.function.BiFunction; | ||
|
||
public class BiArgExpressionProcessor<T> extends MultiArgExpressionProcessor<T> { | ||
|
||
public BiArgExpressionProcessor(String expressionName, BiFunction<String, String, T> evaluator) { | ||
super(expressionName, 2, args -> evaluator.apply(args.get(0), args.get(1))); | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
jbehave-core/src/main/java/org/jbehave/core/expressions/DelegatingExpressionProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.Collection; | ||
import java.util.Optional; | ||
|
||
public class DelegatingExpressionProcessor implements ExpressionProcessor<Object> { | ||
|
||
private final Collection<ExpressionProcessor<?>> delegates; | ||
|
||
public DelegatingExpressionProcessor(Collection<ExpressionProcessor<?>> delegates) { | ||
this.delegates = delegates; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
@Override | ||
public Optional<Object> execute(String expression) { | ||
return (Optional<Object>) delegates.stream() | ||
.map(processor -> processor.execute(expression)) | ||
.filter(Optional::isPresent) | ||
.findFirst() | ||
.orElseGet(Optional::empty); | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionArguments.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.List; | ||
|
||
import org.apache.commons.text.StringTokenizer; | ||
import org.apache.commons.text.matcher.StringMatcher; | ||
import org.apache.commons.text.matcher.StringMatcherFactory; | ||
|
||
class ExpressionArguments { | ||
private static final char DELIMITER = ','; | ||
private static final char ESCAPE = '\\'; | ||
|
||
private static final StringMatcher QUOTE_MATCHER = StringMatcherFactory.INSTANCE.stringMatcher("\"\"\""); | ||
private static final StringMatcher TRIMMER_MATCHER = StringMatcherFactory.INSTANCE.trimMatcher(); | ||
private static final IgnoredMatcher IGNORED_MATCHER = new IgnoredMatcher(); | ||
|
||
private final List<String> arguments; | ||
|
||
ExpressionArguments(String argumentsAsString) { | ||
this(argumentsAsString, Integer.MAX_VALUE); | ||
} | ||
|
||
ExpressionArguments(String argumentsAsString, int argsLimit) { | ||
StringTokenizer argumentsTokenizer = new StringTokenizer(argumentsAsString) | ||
.setDelimiterMatcher(new DelimiterMatcher(argsLimit)) | ||
.setQuoteMatcher(QUOTE_MATCHER) | ||
.setTrimmerMatcher(TRIMMER_MATCHER) | ||
.setIgnoredMatcher(IGNORED_MATCHER) | ||
.setIgnoreEmptyTokens(false); | ||
this.arguments = argumentsTokenizer.getTokenList(); | ||
} | ||
|
||
public List<String> getArguments() { | ||
return arguments; | ||
} | ||
|
||
private static final class DelimiterMatcher implements StringMatcher { | ||
private final int resultsLimit; | ||
private int totalNumberOfMatches = 1; | ||
|
||
private DelimiterMatcher(int resultsLimit) { | ||
this.resultsLimit = resultsLimit; | ||
} | ||
|
||
@Override | ||
public int isMatch(char[] buffer, int start, int bufferStart, int bufferEnd) { | ||
if (DELIMITER == buffer[start] && (start == 0 || ESCAPE != buffer[start - 1])) { | ||
if (resultsLimit > totalNumberOfMatches) { | ||
totalNumberOfMatches++; | ||
return 1; | ||
} | ||
} | ||
return 0; | ||
} | ||
} | ||
|
||
private static final class IgnoredMatcher implements StringMatcher { | ||
@Override | ||
public int isMatch(char[] buffer, int start, int bufferStart, int bufferEnd) { | ||
int next = start + 1; | ||
return ESCAPE == buffer[start] && next < buffer.length && DELIMITER == buffer[next] ? 1 : 0; | ||
} | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.Optional; | ||
|
||
public interface ExpressionProcessor<T> { | ||
Optional<T> execute(String expression); | ||
} |
100 changes: 100 additions & 0 deletions
100
jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.Arrays; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
public class ExpressionResolver { | ||
|
||
private static final Pattern GREEDY_EXPRESSION_PATTERN = Pattern.compile("#\\{((?:(?!#\\{|\\$\\{).)*)}", | ||
Pattern.DOTALL); | ||
private static final Pattern RELUCTANT_EXPRESSION_PATTERN = Pattern.compile( | ||
"#\\{((?:(?![#{])[^)](?![()]))*?|(?:(?!#\\{|\\$\\{).)*?\\)|(?:(?!#\\{|\\$\\{).)*?)}", | ||
Pattern.DOTALL); | ||
private static final List<Pattern> PATTERNS = Arrays.asList(RELUCTANT_EXPRESSION_PATTERN, | ||
GREEDY_EXPRESSION_PATTERN); | ||
|
||
private static final String REPLACEMENT_PATTERN = "\\#\\{%s\\}"; | ||
|
||
private final Set<ExpressionProcessor<?>> expressionProcessors; | ||
private final ExpressionResolverMonitor expressionResolverMonitor; | ||
|
||
public ExpressionResolver(Set<ExpressionProcessor<?>> expressionProcessors, | ||
ExpressionResolverMonitor expressionResolverMonitor) { | ||
this.expressionProcessors = expressionProcessors; | ||
this.expressionResolverMonitor = expressionResolverMonitor; | ||
} | ||
|
||
/** | ||
* Evaluates expressions including nested ones. | ||
* <br> | ||
* Syntax: | ||
* <br> | ||
* <code> | ||
* #{expression(arguments...)} | ||
* #{expression(arguments..., #{expression(arguments...)})} | ||
* #{expression(arguments..., #{expression})} | ||
* </code> | ||
* <br> | ||
* Example: | ||
* <br> | ||
* <code> | ||
* #{shiftDate("1942-12-02T01:23:40+04:00", "yyyy-MM-dd'T'HH:mm:ssz", "P43Y4M3W3D")} | ||
* <br> | ||
* #{encodeToBase64(#{fromEpochSecond(-523641111)})} | ||
* </code> | ||
* | ||
* @param stringWithExpressions the string with expressions to evaluate | ||
* @return the resulting string with expression placeholders replaced with expressions evaluation results | ||
*/ | ||
public Object resolveExpressions(boolean dryRun, String stringWithExpressions) { | ||
if (dryRun) { | ||
return stringWithExpressions; | ||
} | ||
try { | ||
return resolveExpressions(stringWithExpressions, PATTERNS.iterator()); | ||
} catch (RuntimeException e) { | ||
expressionResolverMonitor.onExpressionProcessingError(stringWithExpressions, e); | ||
throw e; | ||
} | ||
} | ||
|
||
private Object resolveExpressions(String value, Iterator<Pattern> expressionPatterns) { | ||
String processedValue = value; | ||
Matcher expressionMatcher = expressionPatterns.next().matcher(processedValue); | ||
boolean expressionFound = false; | ||
while (expressionMatcher.find()) { | ||
expressionFound = true; | ||
String expression = expressionMatcher.group(1); | ||
Object expressionResult = apply(expression); | ||
if (!(expressionResult instanceof String) && ("#{" + expression + "}").equals(processedValue)) { | ||
return expressionResult; | ||
} | ||
if (!expressionResult.equals(expression)) { | ||
String regex = String.format(REPLACEMENT_PATTERN, Pattern.quote(expression)); | ||
processedValue = processedValue.replaceFirst(regex, | ||
Matcher.quoteReplacement(String.valueOf(expressionResult))); | ||
expressionFound = false; | ||
expressionMatcher.reset(processedValue); | ||
} | ||
} | ||
if (expressionFound && expressionPatterns.hasNext()) { | ||
return resolveExpressions(processedValue, expressionPatterns); | ||
} | ||
return processedValue; | ||
} | ||
|
||
private Object apply(String expression) { | ||
for (ExpressionProcessor<?> processor : expressionProcessors) { | ||
Optional<?> optional = processor.execute(expression); | ||
if (optional.isPresent()) { | ||
return optional.get(); | ||
} | ||
} | ||
return expression; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolverMonitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
public interface ExpressionResolverMonitor { | ||
|
||
void onExpressionProcessingError(String stringWithExpressions, RuntimeException error); | ||
} |
91 changes: 91 additions & 0 deletions
91
jbehave-core/src/main/java/org/jbehave/core/expressions/MultiArgExpressionProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package org.jbehave.core.expressions; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import java.util.function.Function; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
public class MultiArgExpressionProcessor<T> implements ExpressionProcessor<T> { | ||
|
||
private static final int ARGS_GROUP = 1; | ||
|
||
private final Pattern pattern; | ||
private final String expressionName; | ||
private final int minArgNumber; | ||
private final int maxArgNumber; | ||
private final Function<String, ExpressionArguments> argsParser; | ||
private final Function<List<String>, T> evaluator; | ||
|
||
public MultiArgExpressionProcessor(String expressionName, int minArgNumber, int maxArgNumber, | ||
Function<String, ExpressionArguments> argsParser, Function<List<String>, T> evaluator) { | ||
this.pattern = Pattern.compile("^" + expressionName + "\\((.*)\\)$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); | ||
this.expressionName = expressionName; | ||
this.minArgNumber = minArgNumber; | ||
this.maxArgNumber = maxArgNumber; | ||
this.argsParser = argsParser; | ||
this.evaluator = evaluator; | ||
} | ||
|
||
public MultiArgExpressionProcessor(String expressionName, int minArgNumber, int maxArgNumber, | ||
Function<List<String>, T> evaluator) { | ||
this(expressionName, minArgNumber, maxArgNumber, ExpressionArguments::new, evaluator); | ||
} | ||
|
||
public MultiArgExpressionProcessor(String expressionName, int expectedArgNumber, | ||
Function<List<String>, T> evaluator) { | ||
this(expressionName, expectedArgNumber, expectedArgNumber, evaluator); | ||
} | ||
|
||
public MultiArgExpressionProcessor(String expressionName, int expectedArgNumber, | ||
Function<String, ExpressionArguments> argsParser, Function<List<String>, T> evaluator) { | ||
this(expressionName, expectedArgNumber, expectedArgNumber, argsParser, evaluator); | ||
} | ||
|
||
@Override | ||
public Optional<T> execute(String expression) { | ||
Matcher expressionMatcher = pattern.matcher(expression); | ||
if (expressionMatcher.find()) { | ||
List<String> args = parseArgs(expressionMatcher.group(ARGS_GROUP)); | ||
T expressionResult = evaluator.apply(args); | ||
return Optional.of(expressionResult); | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
private List<String> parseArgs(String argsAsString) { | ||
if (minArgNumber == 1 && maxArgNumber == 1) { | ||
return Collections.singletonList(argsAsString); | ||
} | ||
List<String> args = argsParser.apply(argsAsString).getArguments(); | ||
int argsNumber = args.size(); | ||
if (minArgNumber == maxArgNumber) { | ||
if (argsNumber != minArgNumber) { | ||
throwException(argsAsString, argsNumber, error -> error.append(minArgNumber)); | ||
} | ||
} else if (argsNumber < minArgNumber || argsNumber > maxArgNumber) { | ||
throwException(argsAsString, argsNumber, error -> error.append("from ").append(minArgNumber).append(" to " | ||
).append(maxArgNumber)); | ||
} | ||
return args; | ||
} | ||
|
||
private void throwException(String argsAsString, int argsNumber, Consumer<StringBuilder> expectationsAppender) { | ||
StringBuilder errorMessageBuilder = new StringBuilder("The expected number of arguments for '") | ||
.append(expressionName) | ||
.append("' expression is "); | ||
expectationsAppender.accept(errorMessageBuilder); | ||
errorMessageBuilder.append(", but found ") | ||
.append(argsNumber) | ||
.append(" argument"); | ||
if (argsNumber != 1) { | ||
errorMessageBuilder.append('s'); | ||
} | ||
if (argsNumber > 0) { | ||
errorMessageBuilder.append(": '").append(argsAsString).append('\''); | ||
} | ||
throw new IllegalArgumentException(errorMessageBuilder.toString()); | ||
} | ||
} |
Oops, something went wrong.