package io.github.azagniotov.stubby4j.stubs;
import io.github.azagniotov.stubby4j.annotations.VisibleForTesting;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static io.github.azagniotov.stubby4j.utils.StringUtils.buildToken;
import static java.util.regex.Pattern.quote;
enum RegexParser {
INSTANCE;
@VisibleForTesting
static final Map<Integer, Pattern> PATTERN_CACHE = new ConcurrentHashMap<>();
// A very primitive way to test if string is *maybe* a regex pattern, instead of compiling a Pattern
@VisibleForTesting
static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile(String.format(".*([%s%s%s%s%s%s%s%s%s%s]).*",
quote("^"),
quote("$"),
quote("["),
quote("]"),
quote("{"),
quote("}"),
quote("*"),
quote("|"),
quote("\\"),
quote("?")));
void compilePatternAndCache(final String value) {
compilePatternAndCache(value, Pattern.MULTILINE);
}
private void compilePatternAndCache(final String value, final int flags) {
try {
if (SPECIAL_REGEX_CHARS.matcher(value).matches()) {
PATTERN_CACHE.computeIfAbsent(value.hashCode(), hashCode -> Pattern.compile(value, flags));
}
} catch (final PatternSyntaxException e) {
// We could not compile, probably because of some characters that are special for Pattern
compilePatternAndCache(value, Pattern.LITERAL | Pattern.MULTILINE);
}
}
boolean match(final String patternCandidate, final String subject, final String templateTokenName, final Map<String, String> regexGroups) {
// Pattern.MULTILINE changes the behavior of '^' and '$' characters,
// it does not mean that newline feeds and carriage return will be matched by default
// You need to make sure that you regex pattern covers both \r (carriage return) and \n (linefeed).
// It is achievable by using symbol '\s+' which covers both \r (carriage return) and \n (linefeed).
return match(patternCandidate, subject, templateTokenName, regexGroups, Pattern.MULTILINE);
}
private boolean match(final String patternCandidate, final String subject, final String templateTokenName, final Map<String, String> regexGroups, final int flags) {
try {
final Pattern pattern = PATTERN_CACHE.computeIfAbsent(
patternCandidate.hashCode(), hashCode -> Pattern.compile(patternCandidate, flags));
final Matcher matcher = pattern.matcher(subject);
final boolean isMatch = matcher.matches();
if (isMatch) {
// group(0) holds the full regex matchStubByIndex
regexGroups.put(buildToken(templateTokenName, 0), matcher.group(0));
//Matcher.groupCount() returns the number of explicitly defined capturing groups in the pattern regardless
// of whether the capturing groups actually participated in the matchStubByIndex. It does not include matcher.group(0)
final int groupCount = matcher.groupCount();
if (groupCount > 0) {
for (int idx = 1; idx <= groupCount; idx++) {
regexGroups.put(buildToken(templateTokenName, idx), matcher.group(idx));
}
}
}
return isMatch;
} catch (final PatternSyntaxException e) {
// We could not compile, probably because of some characters that are special for Pattern
return match(patternCandidate, subject, templateTokenName, regexGroups, Pattern.LITERAL | Pattern.MULTILINE);
}
}
}