Skip to content

Commit

Permalink
Merge pull request #1670 from altro3/optional-vars-in-urls
Browse files Browse the repository at this point in the history
Fix read url template optional path variables
  • Loading branch information
altro3 authored Aug 5, 2024
2 parents 62cdfd1 + 69e4bc5 commit 072fc00
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.http.uri.UriMatchVariable;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.visitor.VisitorContext;
Expand All @@ -41,15 +39,11 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.replacePlaceholders;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_SERVER_CONTEXT_PATH;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_NAME;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_SCOPES;
import static io.micronaut.openapi.visitor.StringUtil.CLOSE_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.DOLLAR;
import static io.micronaut.openapi.visitor.StringUtil.OPEN_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.SLASH;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.toValue;
import static io.micronaut.openapi.visitor.UrlUtils.buildUrls;
import static io.micronaut.openapi.visitor.UrlUtils.parsePathSegments;

/**
* Abstract base class for OpenAPI visitors.
Expand Down Expand Up @@ -130,102 +124,10 @@ Map<String, List<PathItem>> resolvePathItems(VisitorContext context, List<UriMat
var resultPathItemsMap = new HashMap<String, List<PathItem>>();

for (UriMatchTemplate matchTemplate : matchTemplates) {
var segms = parsePathSegments(matchTemplate.toPathString());
var finalPaths = buildUrls(segms);

var result = new StringBuilder();

boolean varProcess = false;
boolean valueProcess = false;
boolean isFirstVarChar = true;
boolean needToSkip = false;
final String pathString = matchTemplate.toPathString();
for (char c : pathString.toCharArray()) {
if (varProcess) {
if (isFirstVarChar) {
isFirstVarChar = false;
if (c == '?' || c == '.') {
needToSkip = true;
result.deleteCharAt(result.length() - 1);
continue;
} else if (c == '+' || c == '0') {
continue;
} else if (c == '/') {
needToSkip = true;
result.deleteCharAt(result.length() - 1);
continue;
}
}
if (c == ':') {
valueProcess = true;
continue;
}
if (c == '}') {
varProcess = false;
valueProcess = false;
if (!needToSkip) {
result.append('}');
}
needToSkip = false;
continue;
}
if (valueProcess || needToSkip) {
continue;
}
}
if (c == '{') {
varProcess = true;
isFirstVarChar = true;
}
result.append(c);
}

String resultPath = replacePlaceholders(result.toString(), context);

if (!resultPath.startsWith(StringUtil.SLASH) && !resultPath.startsWith(DOLLAR)) {
resultPath = StringUtil.SLASH + resultPath;
}
String contextPath = ConfigUtils.getConfigProperty(MICRONAUT_SERVER_CONTEXT_PATH, context);
if (StringUtils.isNotEmpty(contextPath)) {
if (!contextPath.startsWith(StringUtil.SLASH) && !contextPath.startsWith(DOLLAR)) {
contextPath = StringUtil.SLASH + contextPath;
}
if (contextPath.endsWith(StringUtil.SLASH)) {
contextPath = contextPath.substring(0, contextPath.length() - 1);
}
resultPath = contextPath + resultPath;
}

var finalPaths = new HashMap<Integer, String>();
finalPaths.put(-1, resultPath);
if (CollectionUtils.isNotEmpty(matchTemplate.getVariables())) {
var optionalVars = new ArrayList<String>();
// need check not required path variables
for (UriMatchVariable var : matchTemplate.getVariables()) {
if (var.isQuery() || !var.isOptional() || var.isExploded()) {
continue;
}
optionalVars.add(var.getName());
}
if (CollectionUtils.isNotEmpty(optionalVars)) {

int i = 0;
for (String var : optionalVars) {
if (finalPaths.isEmpty()) {
finalPaths.put(i, resultPath + SLASH + OPEN_BRACE + var + CLOSE_BRACE);
i++;
continue;
}
for (Map.Entry<Integer, String> entry : finalPaths.entrySet()) {
if (entry.getKey() + 1 < i) {
continue;
}
finalPaths.put(i, entry.getValue() + SLASH + OPEN_BRACE + var + CLOSE_BRACE);
}
i++;
}
}
}

for (String finalPath : finalPaths.values()) {
for (String finalPath : finalPaths) {
List<PathItem> resultPathItems = resultPathItemsMap.computeIfAbsent(finalPath, k -> new ArrayList<>());
resultPathItems.add(paths.computeIfAbsent(finalPath, key -> new PathItem()));
}
Expand Down
192 changes: 192 additions & 0 deletions openapi/src/main/java/io/micronaut/openapi/visitor/UrlUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.openapi.visitor;

import io.micronaut.core.annotation.Internal;

import java.util.ArrayList;
import java.util.List;

import static io.micronaut.openapi.visitor.StringUtil.CLOSE_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.OPEN_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.SLASH;

/**
* URL and URL paths util methods.
*
* @since 6.12.0
*/
@Internal
public final class UrlUtils {

private UrlUtils() {
}

public static List<String> buildUrls(List<Segment> segments) {

var results = new ArrayList<StringBuilder>();

Segment prevSegment = null;
for (var segment : segments) {
appendSegment(segment, prevSegment, results);
prevSegment = segment;
}

var resultStrings = new ArrayList<String>();
for (var res : results) {
var url = res.toString();
if (!resultStrings.contains(url)) {
if (url.endsWith(SLASH)) {
url = url.substring(0, url.length() - SLASH.length());
}
resultStrings.add(url);
}
}

return resultStrings;
}

private static void appendSegment(Segment segment, Segment prevSegment, List<StringBuilder> results) {
var type = segment.type;
var value = segment.value;
if (results.isEmpty()) {
if (type == SegmentType.PLACEHOLDER) {
results.add(new StringBuilder(value));
return;
}
var builder = new StringBuilder(SLASH).append(value);
if (!value.endsWith(SLASH)) {
builder.append(SLASH);
}
results.add(builder);
if (type == SegmentType.OPT_VAR) {
results.add(new StringBuilder(SLASH));
}
return;
}
if (type == SegmentType.CONST || type == SegmentType.REQ_VAR || type == SegmentType.PLACEHOLDER) {
for (var result : results) {
result.append(value);
if (type != SegmentType.PLACEHOLDER) {
result.append(SLASH);
}
}
return;
}

var newResults = new ArrayList<StringBuilder>();
for (var result : results) {
newResults.add(new StringBuilder(result));
}
for (var result : results) {
if (prevSegment.type == SegmentType.OPT_VAR && result.indexOf(prevSegment.value + SLASH) < 0) {
continue;
}
result.append(value).append(SLASH);
}
results.addAll(newResults);
}

public static List<Segment> parsePathSegments(String pathString) {

var segments = new ArrayList<Segment>();

var startPos = 0;

for (; ; ) {

var varStartPos = pathString.indexOf('{', startPos);
if (varStartPos < 0) {
addConstValue(pathString.substring(startPos), segments);
break;
}

var varEndPos = pathString.indexOf('}', varStartPos);

var constSegment = pathString.substring(startPos, varStartPos);
var nextChar = pathString.charAt(varStartPos + 1);

// skip non path vars
if (nextChar == '?' || nextChar == '.' || nextChar == '+' || nextChar == '0') {
addConstValue(constSegment, segments);
startPos = varEndPos + 1;
continue;
}

// process placeholders
if (varStartPos >= 1 && pathString.charAt(varStartPos - 1) == '$') {
segments.add(new Segment(SegmentType.PLACEHOLDER, pathString.substring(varStartPos - 1, varEndPos + 1)));
startPos = varEndPos + 1;
continue;
}

SegmentType type = nextChar == '/' ? SegmentType.OPT_VAR : SegmentType.REQ_VAR;

if (!constSegment.isEmpty()) {
addConstValue(constSegment, segments);
}

var startBlockPos = varStartPos;
if (pathString.charAt(startBlockPos + 1) == '/') {
startBlockPos++;
}
for (; ; ) {
var dotPos = pathString.indexOf(',', startBlockPos + 1);
var dotPos2 = pathString.indexOf(':', startBlockPos + 1);
var minEndPos = dotPos > 0 && dotPos < varEndPos ? dotPos : varEndPos;
minEndPos = dotPos2 > 0 && dotPos2 < minEndPos ? dotPos2 : minEndPos;
var varName = pathString.substring(startBlockPos + 1, minEndPos);
segments.add(new Segment(type, OPEN_BRACE + varName + CLOSE_BRACE));
if (minEndPos != dotPos) {
break;
}
startBlockPos = minEndPos;
}
startPos = varEndPos + 1;
}

if (segments.isEmpty()) {
segments.add(new Segment(SegmentType.CONST, SLASH));
}

return segments;
}

private static void addConstValue(String constValue, List<Segment> segments) {
if (constValue.startsWith(SLASH)) {
constValue = constValue.substring(1);
}
if (constValue.endsWith(SLASH)) {
constValue = constValue.substring(0, constValue.length() - SLASH.length());
}
if (!constValue.isEmpty()) {
segments.add(new Segment(SegmentType.CONST, constValue));
}
}

private record Segment(
SegmentType type,
String value
) {
}

private enum SegmentType {
REQ_VAR,
OPT_VAR,
CONST,
PLACEHOLDER,
}
}
Loading

0 comments on commit 072fc00

Please sign in to comment.