Skip to content

Commit 90a3eea

Browse files
fix(console): correct find overlapping paths algorithm
fix(console): remove deprecated class
1 parent bfe7fea commit 90a3eea

File tree

18 files changed

+248
-866
lines changed

18 files changed

+248
-866
lines changed

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java

Lines changed: 108 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
import io.gravitee.definition.model.v4.flow.Flow;
2727
import io.gravitee.definition.model.v4.flow.selector.ChannelSelector;
2828
import io.gravitee.definition.model.v4.flow.selector.HttpSelector;
29-
import io.gravitee.definition.model.v4.flow.selector.McpSelector;
3029
import io.gravitee.definition.model.v4.flow.selector.Selector;
3130
import io.gravitee.definition.model.v4.flow.selector.SelectorType;
3231
import io.gravitee.definition.model.v4.flow.step.Step;
3332
import io.gravitee.definition.model.v4.nativeapi.NativeFlow;
3433
import java.util.ArrayList;
34+
import java.util.Arrays;
3535
import java.util.Collection;
3636
import java.util.HashMap;
3737
import java.util.HashSet;
@@ -40,7 +40,6 @@
4040
import java.util.Objects;
4141
import java.util.Optional;
4242
import java.util.Set;
43-
import java.util.concurrent.atomic.AtomicBoolean;
4443
import java.util.function.Function;
4544
import java.util.regex.Pattern;
4645
import java.util.stream.Collectors;
@@ -227,7 +226,7 @@ public void validatePathParameters(ApiType apiType, Stream<Flow> apiFlows, Strea
227226
planFlows = planFlows == null ? Stream.empty() : planFlows;
228227
// group all flows in one stream
229228
final Stream<Flow> flowsWithPathParam = filterFlowsWithPathParam(apiType, apiFlows, planFlows);
230-
validatePathParamOverlapping(apiType, flowsWithPathParam);
229+
checkOverlappingPaths(apiType, flowsWithPathParam);
231230
}
232231

233232
private Stream<Flow> filterFlowsWithPathParam(ApiType apiType, Stream<Flow> apiFlows, Stream<Flow> planFlows) {
@@ -236,57 +235,120 @@ private Stream<Flow> filterFlowsWithPathParam(ApiType apiType, Stream<Flow> apiF
236235
.filter(flow -> containsPathParam(apiType, flow));
237236
}
238237

239-
private void validatePathParamOverlapping(ApiType apiType, Stream<Flow> flows) {
240-
Map<String, Integer> paramWithPosition = new HashMap<>();
241-
Map<String, List<String>> pathsByParam = new HashMap<>();
242-
final AtomicBoolean hasOverlap = new AtomicBoolean(false);
243-
244-
flows.forEach(flow -> {
245-
final String path = extractPath(apiType, flow);
246-
String[] branches = SEPARATOR_SPLITTER.split(path);
247-
for (int i = 0; i < branches.length; i++) {
248-
final String currentBranch = branches[i];
249-
if (currentBranch.startsWith(PATH_PARAM_PREFIX)) {
250-
// Store every path for a path param in a map
251-
prepareOverlapsMap(pathsByParam, path, currentBranch);
252-
if (isOverlapping(paramWithPosition, currentBranch, i)) {
253-
// Exception is thrown later to be able to provide every overlapping case to the end user
254-
hasOverlap.set(true);
255-
} else {
256-
paramWithPosition.put(currentBranch, i);
257-
}
238+
private void checkOverlappingPaths(ApiType apiType, Stream<Flow> flows) {
239+
// Extract unique, non-empty paths from enabled flows
240+
List<String> uniquePaths = flows
241+
.map(flow -> extractPath(apiType, flow))
242+
.map(this::normalizePath) // normalize to avoid ambiguity due to slashes/case
243+
.filter(path -> !path.isEmpty())
244+
.distinct()
245+
.toList();
246+
247+
Map<String, Set<String>> overlappingPaths = new HashMap<>();
248+
int pathCount = uniquePaths.size();
249+
250+
for (int i = 0; i < pathCount; i++) {
251+
String path1 = uniquePaths.get(i);
252+
String[] segments1 = splitPathSegments(path1);
253+
254+
for (int j = i + 1; j < pathCount; j++) {
255+
String path2 = uniquePaths.get(j);
256+
String[] segments2 = splitPathSegments(path2);
257+
258+
if (segments1.length != segments2.length) continue;
259+
260+
if (arePathsAmbiguous(segments1, segments2)) {
261+
// Use a deterministic grouping key to avoid merging unrelated conflicts
262+
String key = buildAmbiguitySignature(segments1);
263+
Set<String> paths = overlappingPaths.computeIfAbsent(key, k -> new HashSet<>());
264+
paths.add(path1);
265+
paths.add(path2);
258266
}
259267
}
260-
});
268+
}
261269

262-
if (hasOverlap.get()) {
263-
throw new ValidationDomainException(
264-
"Some path parameters are used at different position across different flows.",
265-
pathsByParam
266-
.entrySet()
267-
.stream()
268-
// Only keep params with overlap
269-
.filter(entry -> entry.getValue().size() > 1)
270-
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().toString()))
271-
);
270+
if (!overlappingPaths.isEmpty()) {
271+
// Sort lists for stable output
272+
Map<String, String> payload = overlappingPaths
273+
.entrySet()
274+
.stream()
275+
.collect(
276+
Collectors.toMap(Map.Entry::getKey, entry -> {
277+
List<String> sortedPaths = new ArrayList<>(entry.getValue());
278+
sortedPaths.sort(String::compareTo);
279+
return sortedPaths.toString();
280+
})
281+
);
282+
283+
throw new ValidationDomainException("Invalid path parameters", payload);
272284
}
273285
}
274286

275-
private static void prepareOverlapsMap(Map<String, List<String>> pathsByParam, String path, String branches) {
276-
pathsByParam.compute(branches, (key, value) -> {
277-
if (value == null) {
278-
value = new ArrayList<>();
279-
}
280-
// Add the path only once to the error message
281-
if (!value.contains(path)) {
282-
value.add(path);
283-
}
284-
return value;
285-
});
287+
/**
288+
* Returns true if the two paths (split into segments) are ambiguous per OpenAPI 3.0:
289+
* - Same number of segments
290+
* - For each segment: both are parameters, or both are static and equal
291+
*/
292+
private boolean arePathsAmbiguous(String[] segments1, String[] segments2) {
293+
for (int i = 0; i < segments1.length; i++) {
294+
boolean isParam1 = segments1[i].startsWith(PATH_PARAM_PREFIX);
295+
boolean isParam2 = segments2[i].startsWith(PATH_PARAM_PREFIX);
296+
297+
if (isParam1 && isParam2) continue;
298+
299+
if (!isParam1 && !isParam2 && segments1[i].equals(segments2[i])) continue;
300+
301+
return false;
302+
}
303+
304+
return true;
305+
}
306+
307+
/**
308+
* Normalize path:
309+
* - Collapse multiple slashes
310+
* - Remove trailing slash (except root "/")
311+
* - Lowercase literals if routing is case-insensitive; keeping case as-is here
312+
*/
313+
private String normalizePath(String raw) {
314+
if (raw == null) return "";
315+
String p = raw.trim();
316+
317+
if (p.isEmpty()) return "";
318+
// Collapse multiple slashes
319+
p = p.replaceAll("/{2,}", PATH_SEPARATOR);
320+
// Remove trailing slash except root
321+
if (p.length() > 1 && p.endsWith(PATH_SEPARATOR)) {
322+
p = p.substring(0, p.length() - 1);
323+
}
324+
// Ensure leading slash for consistency
325+
if (!p.startsWith(PATH_SEPARATOR)) {
326+
p = PATH_SEPARATOR + p;
327+
}
328+
329+
return p;
330+
}
331+
332+
/**
333+
* Split path into non-empty segments after normalization.
334+
*/
335+
private String[] splitPathSegments(String path) {
336+
return Arrays.stream(SEPARATOR_SPLITTER.split(path))
337+
.filter(s -> !s.isEmpty())
338+
.toArray(String[]::new);
286339
}
287340

288-
private static boolean isOverlapping(Map<String, Integer> paramWithPosition, String param, Integer i) {
289-
return paramWithPosition.containsKey(param) && !paramWithPosition.get(param).equals(i);
341+
/**
342+
* Build a deterministic ambiguity signature by replacing any parameter segment with ":" and keeping literals.
343+
* Example: /users/:id/orders -> /users/:/orders
344+
*/
345+
private String buildAmbiguitySignature(String[] segments) {
346+
return (
347+
PATH_SEPARATOR +
348+
Arrays.stream(segments)
349+
.map(s -> s.startsWith(PATH_PARAM_PREFIX) ? PATH_PARAM_PREFIX : s)
350+
.collect(Collectors.joining(PATH_SEPARATOR))
351+
);
290352
}
291353

292354
private static Boolean containsPathParam(ApiType apiType, Flow flow) {

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/exception/PathParameterOverlapValidationException.java

Lines changed: 0 additions & 59 deletions
This file was deleted.

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/PlanServiceImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.fasterxml.jackson.databind.ObjectMapper;
2727
import io.gravitee.apim.core.audit.model.AuditInfo;
2828
import io.gravitee.apim.core.flow.crud_service.FlowCrudService;
29+
import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService;
2930
import io.gravitee.apim.core.subscription.domain_service.CloseSubscriptionDomainService;
3031
import io.gravitee.definition.model.v4.ApiType;
3132
import io.gravitee.definition.model.v4.flow.Flow;
@@ -83,7 +84,6 @@
8384
import io.gravitee.rest.api.service.v4.mapper.GenericPlanMapper;
8485
import io.gravitee.rest.api.service.v4.mapper.PlanMapper;
8586
import io.gravitee.rest.api.service.v4.validation.FlowValidationService;
86-
import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService;
8787
import io.gravitee.rest.api.service.v4.validation.TagsValidationService;
8888
import java.util.Arrays;
8989
import java.util.Collection;
@@ -176,7 +176,7 @@ public class PlanServiceImpl extends AbstractService implements PlanService {
176176
private FlowValidationService flowValidationService;
177177

178178
@Autowired
179-
private PathParametersValidationService pathParametersValidationService;
179+
private FlowValidationDomainService flowValidationDomainService;
180180

181181
@Autowired
182182
private GroupService groupService;
@@ -265,7 +265,7 @@ private void validatePathParameters(Api api, List<Flow> newPlanFlows) throws Tec
265265
.flatMap(Collection::stream);
266266
planFlows = Stream.concat(planFlows, newPlanFlows.stream());
267267

268-
pathParametersValidationService.validate(api.getType(), apiFlows, planFlows);
268+
flowValidationDomainService.validatePathParameters(api.getType(), apiFlows, planFlows);
269269
}
270270

271271
private void validateTags(Set<String> tags, Api api) {
@@ -435,7 +435,7 @@ private void validatePathParameters(Api api, UpdatePlanEntity updatePlan) throws
435435
})
436436
.flatMap(Collection::stream);
437437

438-
pathParametersValidationService.validate(api.getType(), apiFlows, planFlows);
438+
flowValidationDomainService.validatePathParameters(api.getType(), apiFlows, planFlows);
439439
}
440440

441441
private void checkStatusOfGeneralConditions(Plan plan) {

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static io.gravitee.rest.api.model.api.ApiLifecycleState.UNPUBLISHED;
2222
import static org.apache.commons.lang3.StringUtils.isBlank;
2323

24+
import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService;
2425
import io.gravitee.definition.model.DefinitionVersion;
2526
import io.gravitee.definition.model.v4.ApiType;
2627
import io.gravitee.definition.model.v4.flow.Flow;
@@ -51,7 +52,6 @@
5152
import io.gravitee.rest.api.service.v4.validation.FlowValidationService;
5253
import io.gravitee.rest.api.service.v4.validation.GroupValidationService;
5354
import io.gravitee.rest.api.service.v4.validation.ListenerValidationService;
54-
import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService;
5555
import io.gravitee.rest.api.service.v4.validation.PlanValidationService;
5656
import io.gravitee.rest.api.service.v4.validation.ResourcesValidationService;
5757
import io.gravitee.rest.api.service.v4.validation.TagsValidationService;
@@ -78,8 +78,8 @@ public class ApiValidationServiceImpl extends TransactionalService implements Ap
7878
private final AnalyticsValidationService analyticsValidationService;
7979
private final PlanSearchService planSearchService;
8080
private final PlanValidationService planValidationService;
81-
private final PathParametersValidationService pathParametersValidationService;
8281
private final ApiServicePluginService apiServicePluginService;
82+
private final FlowValidationDomainService flowValidationDomainService;
8383

8484
public ApiValidationServiceImpl(
8585
final TagsValidationService tagsValidationService,
@@ -91,8 +91,8 @@ public ApiValidationServiceImpl(
9191
final AnalyticsValidationService loggingValidationService,
9292
final PlanSearchService planSearchService,
9393
final PlanValidationService planValidationService,
94-
final PathParametersValidationService pathParametersValidationService,
95-
ApiServicePluginService apiServicePluginService
94+
ApiServicePluginService apiServicePluginService,
95+
FlowValidationDomainService flowValidationDomainService
9696
) {
9797
this.tagsValidationService = tagsValidationService;
9898
this.groupValidationService = groupValidationService;
@@ -103,8 +103,8 @@ public ApiValidationServiceImpl(
103103
this.analyticsValidationService = loggingValidationService;
104104
this.planSearchService = planSearchService;
105105
this.planValidationService = planValidationService;
106-
this.pathParametersValidationService = pathParametersValidationService;
107106
this.apiServicePluginService = apiServicePluginService;
107+
this.flowValidationDomainService = flowValidationDomainService;
108108
}
109109

110110
@Override
@@ -143,7 +143,7 @@ public void validateAndSanitizeNewApi(
143143
// Validate and clean flow
144144
newApiEntity.setFlows(flowValidationService.validateAndSanitize(newApiEntity.getType(), newApiEntity.getFlows()));
145145

146-
pathParametersValidationService.validate(
146+
flowValidationDomainService.validatePathParameters(
147147
newApiEntity.getType(),
148148
(newApiEntity.getFlows() != null ? newApiEntity.getFlows().stream() : Stream.empty()),
149149
Stream.empty()
@@ -204,7 +204,7 @@ public void validateAndSanitizeUpdateApi(
204204
updateApiEntity.setPlans(planValidationService.validateAndSanitize(updateApiEntity.getType(), updateApiEntity.getPlans()));
205205

206206
// Validate path parameters
207-
pathParametersValidationService.validate(
207+
flowValidationDomainService.validatePathParameters(
208208
updateApiEntity.getType(),
209209
(updateApiEntity.getFlows() != null ? updateApiEntity.getFlows().stream() : Stream.empty()),
210210
getPlansFlows(updateApiEntity.getPlans())
@@ -258,7 +258,7 @@ public void validateAndSanitizeImportApiForCreation(
258258
apiEntity.setPlans(planValidationService.validateAndSanitize(apiEntity.getType(), apiEntity.getPlans()));
259259

260260
// Validate path parameters
261-
pathParametersValidationService.validate(
261+
flowValidationDomainService.validatePathParameters(
262262
apiEntity.getType(),
263263
(apiEntity.getFlows() != null ? apiEntity.getFlows().stream() : Stream.empty()),
264264
getPlansFlows(apiEntity.getPlans())

0 commit comments

Comments
 (0)