org.openapi4j.operation.validator.util.PathResolver Maven / Gradle / Ivy
package org.openapi4j.operation.validator.util;
import org.openapi4j.core.model.OAIContext;
import org.openapi4j.core.util.StringUtil;
import org.openapi4j.parser.model.v3.Server;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.openapi4j.operation.validator.util.PathResolver.Options.*;
public class PathResolver {
public enum Options {
NONE,
START_STRING,
END_STRING,
RETURN_FIXED_PATTERN
}
private static final Pattern OAS_PATH_PARAMETERS_PATTERN = Pattern.compile("\\{[.;?*+]*([^{}.;?*+]+)[^}]*}");
private static final Pattern ABSOLUTE_URL_PATTERN = Pattern.compile("\\A[a-z0-9.+-]+://.*", Pattern.CASE_INSENSITIVE);
private static final Pattern PATH_URL_PATTERN = Pattern.compile("(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\\?([^#]*))?(?:#(.*))?");
private static final String START_STRING_ANCHOR = "^";
private static final String END_STRING_ANCHOR = "$";
private static final String START_PARAM_NAMED_GROUP = "(?<";
private static final String END_PARAM_NAMED_GROUP = ">[^\\/]+)";
private static final PathResolver INSTANCE = new PathResolver();
private PathResolver() {
}
public static PathResolver instance() {
return INSTANCE;
}
/**
* This method returns a pattern only if a pattern is needed, otherwise it returns {@code null}.
* This will not add any anchor to the regular expression.
*
* @param templatePath The template path to build the regular expression from.
* @return Pattern only if a pattern is needed.
*/
public Pattern solve(String templatePath) {
return solve(templatePath, EnumSet.of(Options.NONE));
}
/**
* This method returns a pattern for the given path.
* You need to give {@code RETURN_FIXED_PATTERN} in case of no parameter has been found on path.
*
* @param templatePath The template path to build the regular expression from.
* @param options Options for the regular expression build.
* @return Pattern.
*/
public Pattern solve(String templatePath, Set options) {
final StringBuilder regex = new StringBuilder();
int lastMatchEnd = 0;
boolean foundParameter = false;
Matcher parametersMatcher = OAS_PATH_PARAMETERS_PATTERN.matcher(templatePath);
while (parametersMatcher.find()) {
addConstantFragment(regex, templatePath, lastMatchEnd, parametersMatcher.start());
lastMatchEnd = parametersMatcher.end();
final String paramName = parametersMatcher.group(1);
addVariableFragment(regex, paramName);
foundParameter = true;
}
if (foundParameter) {
addConstantFragment(regex, templatePath, lastMatchEnd, templatePath.length());
setupAnchors(regex, options);
return Pattern.compile(regex.toString());
} else if (options.contains(Options.RETURN_FIXED_PATTERN)) {
regex.append(Pattern.quote(templatePath));
setupAnchors(regex, options);
return Pattern.compile(regex.toString());
}
return null;
}
/**
* Resolves the given URL path with the context of the Document if URL is relative.
*
* @param context The context of the Document
* @param url The URL to resolve.
* @return The resolved URL.
*/
public String getResolvedPath(OAIContext context, String url) {
// server URL may be relative to the location where the OpenAPI document is being served.
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#server-object
try {
if (isAbsoluteUrl(url)) {
// We do not compute directly with URL#getPath()
// to avoid MalformedURLException
Matcher m = PATH_URL_PATTERN.matcher(url);
return m.find() ? m.group(3) : "/";
} else {
// Check if there's a defined file name in URL
URL resource = context.getBaseUrl();
// trim query & anchor
String basePath = StringUtil.tokenize(resource.toString(), "(?:\\?.*|#.*)", false, true).get(0);
// handle scheme://api.com
String host = resource.getHost();
if (host.length() > 0 && basePath.endsWith(host)) {
return "/";
}
// Get last path fragment (maybe file name)
String lastFragment = basePath.substring(basePath.lastIndexOf('/') + 1);
// remove filename from URL
if (lastFragment.contains(".")) {
basePath = basePath.substring(0, basePath.indexOf(lastFragment));
}
return new URL(new URL(basePath), url).getPath();
}
} catch (MalformedURLException e) {
return "/";
}
}
public List buildPathPatterns(OAIContext context, List servers, String templatePath) {
List patterns = new ArrayList<>();
if (servers == null) {
patterns.add(buildPathPattern("", templatePath));
} else {
for (Server server : servers) {
Pattern pattern = buildPathPattern(getResolvedPath(context, server.getUrl()), templatePath);
patterns.add(pattern);
}
}
return patterns;
}
public Pattern findPathPattern(Collection pathPatterns, String requestPath) {
if (requestPath == null || requestPath.isEmpty()) {
requestPath = "/";
}
Pattern bestMatch = null;
int bestMatchGroupCount = Integer.MAX_VALUE;
// Match path pattern
for (Pattern pathPattern : pathPatterns) {
Matcher matcher = pathPattern.matcher(requestPath);
int matchGroupCount = matcher.groupCount();
if (matcher.matches()) {
if (bestMatch == null || matchGroupCount < bestMatchGroupCount) {
bestMatch = pathPattern;
bestMatchGroupCount = matchGroupCount;
}
}
}
return bestMatch;
}
/**
* Ugly method to workaround named group limitations
*
* @param paramName The parameter name
* @return The computed group name
*/
public String getParamGroupName(String paramName) {
// Append hash code to avoid conflicting parameter names
return paramName.replaceAll("[^a-zA-Z]", "") +
Math.abs(paramName.hashCode());
}
private Pattern buildPathPattern(String basePath, String templatePath) {
return solve(
basePath + templatePath,
EnumSet.of(START_STRING, END_STRING, RETURN_FIXED_PATTERN));
}
/**
* Append anchors, if any, to the regular expression.
*
* @param regex The given regular expression.
* @param options The anchors to append.
*/
private void setupAnchors(StringBuilder regex, Set options) {
if (options.contains(Options.START_STRING)) {
regex.insert(0, START_STRING_ANCHOR);
}
if (options.contains(Options.END_STRING)) {
regex.append(END_STRING_ANCHOR);
}
}
/**
* Decides if a URL is absolute based on whether it contains a valid scheme name, as
* defined in RFC 1738.
*/
private boolean isAbsoluteUrl(String url) {
return ABSOLUTE_URL_PATTERN.matcher(url).matches();
}
/**
* Append named group to the regular expression.
*
* @param regex The given regular expression.
* @param paramName The parameter named.
*/
private void addVariableFragment(StringBuilder regex, String paramName) {
regex
.append(START_PARAM_NAMED_GROUP)
.append(getParamGroupName(paramName))
.append(END_PARAM_NAMED_GROUP);
}
/**
* Append constant section to the regular expression if found in {@code oasPath}.
*
* @param regex The given regular expression.
* @param oasPath The OAS path to apply.
* @param beginIndex Begin index.
* @param endIndex End index.
*/
private void addConstantFragment(StringBuilder regex,
String oasPath,
int beginIndex,
int endIndex) {
String toQuote = oasPath.substring(beginIndex, endIndex);
if (toQuote.length() != 0) {
regex.append(Pattern.quote(toQuote));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy