All Downloads are FREE. Search and download functionalities are using the official Maven repository.

es.iti.wakamiti.api.ExpressionMatcher Maven / Gradle / Ivy

The newest version!
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package es.iti.wakamiti.api;


import es.iti.wakamiti.api.plan.PlanNode;
import es.iti.wakamiti.api.util.Either;
import es.iti.wakamiti.api.util.WakamitiLogger;
import org.slf4j.Logger;

import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;


/**
 * Utility class for matching expressions in Wakamiti test plans.
 * It provides methods for generating regular expressions based on
 * translated step definitions and performing pattern matching on
 * model step names.
 *
 * @author Luis Iñesta Gelabert - [email protected]
 */
public class ExpressionMatcher {

    private static final Logger LOGGER = WakamitiLogger.forClass(ExpressionMatcher.class);

    private static final String NAMED_ARGUMENT_REGEX = "\\{(\\w++):(\\w+?-?+\\w++)\\}";
    private static final String UNNAMED_ARGUMENT_REGEX = "\\{(\\w+?-?+\\w++)\\}";

    private static final Map cache = new HashMap<>();

    private final String translatedDefinition;
    private final WakamitiDataTypeRegistry typeRegistry;
    private final Locale locale;

    private ExpressionMatcher(
            String translatedDefinition,
            WakamitiDataTypeRegistry typeRegistry,
            Locale locale
    ) {
        this.translatedDefinition = translatedDefinition;
        this.typeRegistry = typeRegistry;
        this.locale = locale;
    }

    /**
     * Retrieves a Matcher for a given translated expression, WakamitiDataTypeRegistry, locale, and model step.
     *
     * @param translatedDefinition The translated expression to create a Matcher for.
     * @param typeRegistry         The WakamitiDataTypeRegistry for type information.
     * @param locale               The locale for localization.
     * @param modelStep            The model step for which to create the Matcher.
     * @return The Matcher for the specified parameters.
     */
    public static Matcher matcherFor(
            String translatedDefinition,
            WakamitiDataTypeRegistry typeRegistry,
            Locale locale,
            Either modelStep
    ) {
        ExpressionMatcher matcher = new ExpressionMatcher(
                translatedDefinition, typeRegistry, locale
        );
        String regex = cache.computeIfAbsent(matcher, ExpressionMatcher::computeRegularExpression);
        return Pattern.compile(regex).matcher(modelStep.mapValueOrFallback(PlanNode::name));
    }

    /**
     * Computes the regular expression for the specified translated expression.
     *
     * @param translatedExpression The translated expression for which to compute the regular expression.
     * @return The computed regular expression.
     */
    public static String computeRegularExpression(String translatedExpression) {
        String regex = regexPriorAdjustments(translatedExpression);
        regex = regexFinalAdjustments(regex);
        LOGGER.trace("Expression Matcher: {} ==> {}", translatedExpression, regex);
        return regex;
    }

    /**
     * Adjusts the regular expression prior to final adjustments.
     *
     * @param sourceExpression The source expression to adjust.
     * @return The adjusted regular expression.
     */
    protected static String regexPriorAdjustments(String sourceExpression) {
        String regex = sourceExpression;
        // a|b|c -> (a|b|c)
        regex = regex.replaceAll("[^ |(]*?(\\|[^ |)]+)++", "($0)");
        // (( -> ( and )) -> (
        regex = regex.replaceAll("\\(\\(([^()]*)\\)\\)", "($1)");
        // * -> any value
        regex = regex.replaceAll("(? ...
        regex = regexBracketedAdjustments(regex);

        return regex;
    }

    private static String regexBracketedAdjustments(String regex) {
        Pattern bracketed = Pattern.compile("(?\\((?:(?!(? doReplace = (string, pattern) -> {
            while (string.matches(".*" + pattern.pattern() + ".*")) {
                List parts = new LinkedList<>();
                Matcher matcher = pattern.matcher(string);
                while (matcher.find()) {
                    parts.add(matcher.group("x"));
                }
                string = regexBracketedAdjustments(string, parts);
            }
            return string;
        };

        regex = doReplace.apply(regex, nested);
        regex = doReplace.apply(regex, bracketed);

        return regex.replace("[", "(").replace("]", ")");
    }

    private static String regexBracketedAdjustments(String regex, List texts) {
        texts = texts.stream().distinct().collect(Collectors.toList());
        Collections.reverse(texts);
        for (String text : texts) {
            String aux = text;

            // ( ) -> optional
            aux = aux.replaceAll("(? ((?!a).)*
            aux = aux.replaceAll("(? (?:.*)?
            aux = aux.replaceAll(" (\\(\\?:.+\\*\\)\\?)", "$1");

            aux = aux.replace("(", "[").replace(")", "]");
            regex = regex.replace(text, aux);

            if (regex.contains(aux + " ")) {
                regex = regex.replace(aux + " ", "[?:" + aux + " ]?");
            } else if (regex.contains(" " + aux)) {
                regex = regex.replace(" " + aux, "[?: " + aux + "]?");
            }
        }
        return regex;
    }

    /**
     * Performs final adjustments on the regular expression.
     *
     * @param computingRegex The intermediate regular expression.
     * @return The final adjusted regular expression.
     */
    protected static String regexFinalAdjustments(String computingRegex) {
        String regex = computingRegex;
        regex = regex.replace(" $", "$");
        regex = regex.replace("((?!\\).)$", "\1\\s*$");
        return regex;
    }

    /**
     * Computes the regular expression for the translated expression.
     *
     * @return The computed regular expression.
     */
    protected String computeRegularExpression() {
        String regex = regexPriorAdjustments(translatedDefinition);
        regex = regexArgumentSubstitution(regex);
        regex = regexFinalAdjustments(regex);
        LOGGER.trace("Expression Matcher: {} ==> {}", translatedDefinition, regex);
        return regex;
    }

    /**
     * Substitutes arguments in the regular expression based on the translated definition.
     *
     * @param computingRegex The intermediate regular expression.
     * @return The final regular expression with arguments substituted.
     */
    protected String regexArgumentSubstitution(String computingRegex) {
        String regex = computingRegex;
        // unnamed arguments
        Matcher unnamedArgs = Pattern.compile(UNNAMED_ARGUMENT_REGEX).matcher(regex);
        while (unnamedArgs.find()) {
            String typeName = unnamedArgs.group(1);
            WakamitiDataType type = typeRegistry.getType(typeName);
            if (type == null) {
                throwTypeNotRegistered(typeName);
            } else {
                regex = regex.replace(
                        "{" + typeName + "}",
                        "(?<" + Backend.UNNAMED_ARG + ">" + type.getRegex(locale) + ")"
                );
            }
        }
        // named arguments
        Matcher namedArgs = Pattern.compile(NAMED_ARGUMENT_REGEX).matcher(regex);
        while (namedArgs.find()) {
            String argName = namedArgs.group(1);
            String argType = namedArgs.group(2);
            WakamitiDataType type = typeRegistry.getType(argType);
            if (type == null) {
                throwTypeNotRegistered(argType);
            } else {
                regex = regex.replace(
                        "{" + argName + ":" + argType + "}",
                        "(?<" + argName + ">" + type.getRegex(locale) + ")"
                );
            }
        }
        return regex;
    }

    /**
     * Throws a WakamitiException for a type that is not registered in the WakamitiDataTypeRegistry.
     *
     * @param type The type that is not registered.
     */
    protected void throwTypeNotRegistered(String type) {
        throw new WakamitiException(
                "Wrong step definition '{}' : unknown argument type '{}'\nAvailable types are: {}",
                translatedDefinition, type,
                typeRegistry.allTypeNames().sorted().collect(Collectors.joining(", "))
        );
    }

    /**
     * Computes the hash code for this {@code ExpressionMatcher}.
     *
     * @return The computed hash code.
     */
    @Override
    public int hashCode() {
        return Objects.hash(translatedDefinition, typeRegistry, locale);
    }

    /**
     * Checks if this {@code ExpressionMatcher} is equal to another object.
     *
     * @param obj The object to compare.
     * @return {@code true} if equal, {@code false} otherwise.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof ExpressionMatcher) {
            ExpressionMatcher other = (ExpressionMatcher) obj;
            return other.typeRegistry == this.typeRegistry &&
                    other.locale.equals(this.locale) &&
                    other.translatedDefinition.equals(this.translatedDefinition);
        }
        return false;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy