package io.vertx.ext.web.api.contract.openapi3.impl;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.vertx.ext.web.api.contract.RouterFactoryException;
import static io.vertx.ext.web.api.contract.openapi3.impl.OpenApi3Utils.safeBoolean;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class OpenAPI3PathResolver {
String oasPath;
List<Parameter> parameters;
Pattern resolvedPattern;
Map<String, String> mappedGroups;
public static final Pattern OAS_PATH_PARAMETERS_PATTERN = Pattern.compile("\\{{1}[.;?*+]*([^\\{\\}.;?*+]+)[^\\}]*\\}{1}");
public static final Pattern ILLEGAL_PATH_MATCHER = Pattern.compile("\\{[^\\/]*\\/[^\\/]*\\}");
private boolean shouldThreatDotAsReserved;
public OpenAPI3PathResolver(String oasPath, List<Parameter> parameters) {
this.oasPath = oasPath;
if (parameters != null)
this.parameters = parameters.stream().filter(parameter -> parameter.getIn().equals("path")).collect(Collectors.toList());
else
this.parameters = new ArrayList<>();
this.shouldThreatDotAsReserved = hasParameterWithStyle("label");
this.mappedGroups = new HashMap<>();
}
public Optional<Pattern> solve() {
if (ILLEGAL_PATH_MATCHER.matcher(oasPath).matches())
throw new RouterFactoryException("Path template not supported", RouterFactoryException.ErrorType.INVALID_SPEC_PATH);
Matcher parametersMatcher = OAS_PATH_PARAMETERS_PATTERN.matcher(oasPath);
if (!parameters.isEmpty() && parametersMatcher.find()) {
StringBuilder regex = new StringBuilder();
int lastMatchEnd = 0;
boolean endSlash = oasPath.charAt(oasPath.length() - 1) == '/';
parametersMatcher.reset();
int i = 0;
while (parametersMatcher.find()) {
String toQuote = oasPath.substring(lastMatchEnd, parametersMatcher.start());
if (toQuote.length() != 0)
regex.append(Pattern.quote(toQuote));
lastMatchEnd = parametersMatcher.end();
String paramName = parametersMatcher.group(1);
Optional<Parameter> parameterOptional = parameters.stream().filter(p -> p.getName().equals(paramName)).findFirst();
if (parameterOptional.isPresent()) {
Parameter parameter = parameterOptional.get();
String style = solveParamStyle(parameter);
boolean explode = solveParamExplode(parameter);
boolean isObject = OpenApi3Utils.isParameterObjectOrAllOfType(parameter);
boolean isArray = OpenApi3Utils.isParameterArrayType(parameter);
String groupName = "p" + i;
if (style.equals("simple")) {
regex.append(
RegexBuilder.create().namedGroup(
groupName,
RegexBuilder.create().notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "+", "$", "/", "?", "#", "[", "]", (shouldThreatDotAsReserved) ? "." : null
).zeroOrMore()
).zeroOrOne()
);
mappedGroups.put(groupName, paramName);
} else if (style.equals("label")) {
if (isObject && explode) {
Map<String, OpenApi3Utils.ObjectField> properties = OpenApi3Utils.solveObjectParameters(parameter.getSchema());
for (Map.Entry<String, OpenApi3Utils.ObjectField> entry : properties.entrySet()) {
groupName = "p" + i;
regex.append(
RegexBuilder.create().optionalGroup(
RegexBuilder.create()
.escapeCharacter(".").zeroOrOne().quote(entry.getKey()).append("=")
.namedGroup(groupName,
RegexBuilder.create().notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "+", "$", "/", "?", "#", "[", "]", ".", "="
).zeroOrMore()
)
)
);
mappedGroups.put(groupName, entry.getKey());
i++;
}
} else {
regex.append(
RegexBuilder.create()
.escapeCharacter(".").zeroOrOne()
.namedGroup(groupName,
RegexBuilder.create().notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "=", "+", "$", ",", "/", "?", "#", "[", "]"
).zeroOrMore()
).zeroOrOne()
);
mappedGroups.put(groupName, paramName);
}
} else if (style.equals("matrix")) {
if (isObject && explode) {
Map<String, OpenApi3Utils.ObjectField> properties = OpenApi3Utils.solveObjectParameters(parameter.getSchema());
for (Map.Entry<String, OpenApi3Utils.ObjectField> entry : properties.entrySet()) {
groupName = "p" + i;
regex.append(
RegexBuilder.create().optionalGroup(
RegexBuilder.create()
.escapeCharacter(";").quote(entry.getKey()).append("=")
.namedGroup(groupName,
RegexBuilder.create().notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "=", "+", "$", ",", "/", "?", "#", "[", "]",
(shouldThreatDotAsReserved) ? "." : null
).zeroOrMore()
)
)
);
mappedGroups.put(groupName, entry.getKey());
i++;
}
} else if (isArray && explode) {
regex.append(
RegexBuilder.create().namedGroup(
groupName,
RegexBuilder.create().atomicGroup(
RegexBuilder.create()
.append(";").quote(paramName).append("=")
.notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "=", "+", "$", ",", "/", "?", "#", "[", "]",
(shouldThreatDotAsReserved) ? "." : null
).zeroOrMore()
).oneOrMore()
)
);
mappedGroups.put(groupName, paramName);
} else {
regex.append(
RegexBuilder.create()
.append(";").quote(paramName).append("=")
.namedGroup(
groupName,
RegexBuilder.create().notCharactersClass(
"!", "*", "'", "(", ")", ";", "@", "&", "=", "+", "$", "/", "?", "#", "[", "]",
(shouldThreatDotAsReserved) ? "." : null
).zeroOrMore()
).zeroOrOne()
);
mappedGroups.put(groupName, paramName);
}
}
} else {
throw RouterFactoryException.createSpecInvalidException("Missing parameter description for parameter name: " + paramName);
}
i++;
}
String toAppendQuoted = oasPath.substring(lastMatchEnd, (endSlash) ? oasPath.length() - 1 : oasPath.length());
if (toAppendQuoted.length() != 0)
regex.append(Pattern.quote(toAppendQuoted));
if (endSlash)
regex.append("\\/");
return Optional.of(Pattern.compile(regex.toString()));
} else {
return Optional.empty();
}
}
public Pattern getResolvedPattern() {
return resolvedPattern;
}
public Map<String, String> getMappedGroups() {
return mappedGroups;
}
private String solveParamStyle(Parameter parameter) {
return (parameter.getStyle() != null) ? parameter.getStyle().toString() : "simple";
}
private boolean solveParamExplode(Parameter parameter) {
return safeBoolean.apply(parameter.getExplode());
}
private boolean hasParameterWithStyle(String style) {
return parameters.stream().map(this::solveParamStyle).anyMatch(s -> s.equals(style));
}
}