Skip to content

Commit

Permalink
JBEHAVE-1388 Introduce Expressions API
Browse files Browse the repository at this point in the history
  • Loading branch information
valfirst authored Mar 14, 2023
1 parent 131587a commit 9510692
Show file tree
Hide file tree
Showing 32 changed files with 1,231 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.jbehave.core.condition.StepConditionMatcher;
import org.jbehave.core.embedder.Embedder;
import org.jbehave.core.embedder.StoryControls;
import org.jbehave.core.expressions.ExpressionResolver;
import org.jbehave.core.expressions.NullExpressionResolverMonitor;
import org.jbehave.core.failures.FailingUponPendingStep;
import org.jbehave.core.failures.FailureStrategy;
import org.jbehave.core.failures.PassingUponPendingStep;
Expand Down Expand Up @@ -187,6 +189,11 @@ public abstract class Configuration {
*/
protected ParameterConverters parameterConverters;

/**
* Use default built-in expression resolver
*/
protected ExpressionResolver expressionResolver;

/**
* Use default built-in ExamplesTable parsers
*/
Expand Down Expand Up @@ -409,6 +416,13 @@ public ParameterConverters parameterConverters() {
return parameterConverters;
}

public ExpressionResolver expressionResolver() {
if (expressionResolver == null) {
expressionResolver = new ExpressionResolver(Collections.emptySet(), new NullExpressionResolverMonitor());
}
return expressionResolver;
}

public TableParsers tableParsers() {
if (tableParsers == null) {
tableParsers = new TableParsers(keywords(), parameterConverters(), Optional.empty());
Expand Down Expand Up @@ -580,6 +594,11 @@ public Configuration useParameterConverters(ParameterConverters parameterConvert
return this;
}

public Configuration useExpressionResolver(ExpressionResolver expressionResolver) {
this.expressionResolver = expressionResolver;
return this;
}

public Configuration useTableTransformers(TableTransformers tableTransformers) {
this.tableTransformers = tableTransformers;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jbehave.core.condition.StepConditionMatcher;
import org.jbehave.core.embedder.StoryControls;
import org.jbehave.core.expressions.ExpressionResolver;
import org.jbehave.core.failures.FailureStrategy;
import org.jbehave.core.failures.PendingStepStrategy;
import org.jbehave.core.io.StoryLoader;
Expand Down Expand Up @@ -89,6 +90,11 @@ public ParameterConverters parameterConverters() {
return delegate.parameterConverters();
}

@Override
public ExpressionResolver expressionResolver() {
return delegate.expressionResolver();
}

@Override
public ParameterControls parameterControls() {
return delegate.parameterControls();
Expand Down Expand Up @@ -228,7 +234,12 @@ public Configuration useDefaultStoryReporter(StoryReporter storyReporter) {
public Configuration useParameterConverters(ParameterConverters parameterConverters) {
throw notAllowed();
}


@Override
public Configuration useExpressionResolver(ExpressionResolver expressionResolver) {
throw notAllowed();
}

@Override
public Configuration useParameterControls(ParameterControls parameterControls) {
throw notAllowed();
Expand Down
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)));
}
}
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);
}
}
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;
}
}
}
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);
}
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;
}
}
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);
}
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());
}
}
Loading

0 comments on commit 9510692

Please sign in to comment.