diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractMultipleOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractMultipleOpenApiResource.java index 3f3d94b03..693190841 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractMultipleOpenApiResource.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractMultipleOpenApiResource.java @@ -126,8 +126,8 @@ protected AbstractMultipleOpenApiResource(List groupedOpenApis, @Override public void afterPropertiesSet() { this.groupedOpenApis.forEach(groupedOpenApi -> { - springDocCustomizers.getGlobalOpenApiCustomizers().ifPresent(groupedOpenApi::addAllOpenApiCustomizer); - springDocCustomizers.getGlobalOperationCustomizers().ifPresent(groupedOpenApi::addAllOperationCustomizer); + groupedOpenApi.addAllOpenApiCustomizer(springDocCustomizers.getGlobalOpenApiCustomizersStream().toList()); + groupedOpenApi.addAllOperationCustomizer(springDocCustomizers.getGlobalOperationCustomizersStream().toList()); springDocCustomizers.getGlobalOpenApiMethodFilters().ifPresent(groupedOpenApi::addAllOpenApiMethodFilter); } ); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java index aa6bbae9e..bb27b02e7 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java @@ -428,8 +428,8 @@ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) { List servers = openAPI.getServers(); List serversCopy = cloneViaJson(servers, new TypeReference>() {}, springDocProviders.jsonMapper()); - openAPIService.getContext().getBeansOfType(OpenApiLocaleCustomizer.class).values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale)); - springDocCustomizers.getOpenApiCustomizers().ifPresent(apiCustomizers -> apiCustomizers.forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI))); + openAPIService.getContext().getBeanProvider(OpenApiLocaleCustomizer.class).orderedStream().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale)); + springDocCustomizers.getOpenApiCustomizersStream().forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI)); if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy)) openAPIService.setServersPresent(true); @@ -863,13 +863,8 @@ else if (ArrayUtils.isEmpty(methodProduces)) { * @return the router operation */ private RouterOperation customizeDataRestRouterOperation(RouterOperation routerOperation) { - Optional> optionalDataRestRouterOperationCustomizers = springDocCustomizers.getDataRestRouterOperationCustomizers(); - if (optionalDataRestRouterOperationCustomizers.isPresent()) { - Set dataRestRouterOperationCustomizerList = optionalDataRestRouterOperationCustomizers.get(); - for (DataRestRouterOperationCustomizer dataRestRouterOperationCustomizer : dataRestRouterOperationCustomizerList) { - routerOperation = dataRestRouterOperationCustomizer.customize(routerOperation); - } - } + for (DataRestRouterOperationCustomizer dataRestRouterOperationCustomizer : springDocCustomizers.getDataRestRouterOperationCustomizersStream().toList()) + routerOperation = dataRestRouterOperationCustomizer.customize(routerOperation); return routerOperation; } @@ -1115,15 +1110,11 @@ protected Set getDefaultAllowedHttpMethods() { * @return the operation */ protected Operation customizeOperation(Operation operation, Components components, HandlerMethod handlerMethod) { - Optional> optionalOperationCustomizers = springDocCustomizers.getOperationCustomizers(); - if (optionalOperationCustomizers.isPresent()) { - Set operationCustomizerList = optionalOperationCustomizers.get(); - for (OperationCustomizer operationCustomizer : operationCustomizerList) { - if (operationCustomizer instanceof GlobalOperationComponentsCustomizer globalOperationComponentsCustomizer) - operation = globalOperationComponentsCustomizer.customize(operation, components, handlerMethod); - else - operation = operationCustomizer.customize(operation, handlerMethod); - } + for (OperationCustomizer operationCustomizer : springDocCustomizers.getOperationCustomizersStream().toList()) { + if (operationCustomizer instanceof GlobalOperationComponentsCustomizer globalOperationComponentsCustomizer) + operation = globalOperationComponentsCustomizer.customize(operation, components, handlerMethod); + else + operation = operationCustomizer.customize(operation, handlerMethod); } return operation; } @@ -1136,13 +1127,8 @@ protected Operation customizeOperation(Operation operation, Components component * @return the router operation */ protected RouterOperation customizeRouterOperation(RouterOperation routerOperation, HandlerMethod handlerMethod) { - Optional> optionalRouterOperationCustomizers = springDocCustomizers.getRouterOperationCustomizers(); - if (optionalRouterOperationCustomizers.isPresent()) { - Set routerOperationCustomizerList = optionalRouterOperationCustomizers.get(); - for (RouterOperationCustomizer routerOperationCustomizer : routerOperationCustomizerList) { - routerOperation = routerOperationCustomizer.customize(routerOperation, handlerMethod); - } - } + for (RouterOperationCustomizer routerOperationCustomizer : springDocCustomizers.getRouterOperationCustomizersStream().toList()) + routerOperation = routerOperationCustomizer.customize(routerOperation, handlerMethod); return routerOperation; } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/SpringDocCustomizers.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/SpringDocCustomizers.java index 832ce0b5c..173454487 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/SpringDocCustomizers.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/SpringDocCustomizers.java @@ -25,11 +25,13 @@ */ package org.springdoc.core.customizers; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.springdoc.core.filters.GlobalOpenApiMethodFilter; import org.springdoc.core.filters.OpenApiMethodFilter; @@ -38,6 +40,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.CollectionUtils; import static org.springdoc.core.utils.Constants.LINKS_SCHEMA_CUSTOMIZER; @@ -169,6 +172,15 @@ public Optional> getOpenApiCustomizers() { return openApiCustomizers; } + /** + * Gets ordered open api customizer stream. + * + * @return the ordered open api customizer stream + */ + public Stream getOpenApiCustomizersStream() { + return orderedStream(openApiCustomizers); + } + /** * Gets operation customizers. * @@ -178,6 +190,15 @@ public Optional> getOperationCustomizers() { return operationCustomizers; } + /** + * Gets ordered operation customizer stream. + * + * @return the ordered operation customizer stream + */ + public Stream getOperationCustomizersStream() { + return orderedStream(operationCustomizers); + } + /** * Gets router operation customizers. * @@ -187,6 +208,15 @@ public Optional> getRouterOperationCustomizers() return routerOperationCustomizers; } + /** + * Gets ordered router operation customizer stream. + * + * @return the ordered router operation customizer stream + */ + public Stream getRouterOperationCustomizersStream() { + return orderedStream(routerOperationCustomizers); + } + /** * Gets data rest router operation customizers. * @@ -196,6 +226,15 @@ public Optional> getDataRestRouterOperati return dataRestRouterOperationCustomizers; } + /** + * Gets ordered data rest router operation customizer stream. + * + * @return the ordered data rest router operation customizer stream + */ + public Stream getDataRestRouterOperationCustomizersStream() { + return orderedStream(dataRestRouterOperationCustomizers); + } + /** * Gets method filters. * @@ -219,6 +258,15 @@ public Optional> getGlobalOpenApiCustomizers() { return globalOpenApiCustomizers; } + /** + * Gets ordered global open api customizer stream. + * + * @return the ordered global open api customizer stream + */ + public Stream getGlobalOpenApiCustomizersStream() { + return orderedStream(globalOpenApiCustomizers); + } + /** * Gets global operation customizers. * @@ -228,6 +276,15 @@ public Optional> getGlobalOperationCustomizers() return globalOperationCustomizers; } + /** + * Gets ordered global operation customizer stream. + * + * @return the ordered global operation customizer stream + */ + public Stream getGlobalOperationCustomizersStream() { + return orderedStream(globalOperationCustomizers); + } + /** * Gets global open api method filters. * @@ -255,6 +312,13 @@ public Optional> getParameterCustomizers() { return parameterCustomizers; } + private static Stream orderedStream(Optional> customizers) { + return customizers.stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .sorted(AnnotationAwareOrderComparator.INSTANCE); + } + @Override public void afterPropertiesSet() { //add the default customizers diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/SpringDocCustomizersTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/SpringDocCustomizersTest.java new file mode 100644 index 000000000..4eae51d7f --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/SpringDocCustomizersTest.java @@ -0,0 +1,113 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2026 the original author or 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 org.springdoc.core.customizers; + +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +import io.swagger.v3.oas.models.OpenAPI; +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; + +class SpringDocCustomizersTest { + + @Test + void openApiCustomizersStreamHonorsOrderAnnotation() { + OpenApiCustomizer lastCustomizer = new LastOpenApiCustomizer(); + OpenApiCustomizer firstCustomizer = new FirstOpenApiCustomizer(); + Set unorderedCustomizers = new LinkedHashSet<>(); + unorderedCustomizers.add(lastCustomizer); + unorderedCustomizers.add(firstCustomizer); + + SpringDocCustomizers springDocCustomizers = new SpringDocCustomizers( + Optional.of(unorderedCustomizers), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + assertThat(springDocCustomizers.getOpenApiCustomizersStream().toList()).containsExactly(firstCustomizer, lastCustomizer); + } + + @Test + void globalOpenApiCustomizersStreamHonorsOrderAnnotation() { + GlobalOpenApiCustomizer lastCustomizer = new LastGlobalOpenApiCustomizer(); + GlobalOpenApiCustomizer firstCustomizer = new FirstGlobalOpenApiCustomizer(); + Set unorderedCustomizers = new LinkedHashSet<>(); + unorderedCustomizers.add(lastCustomizer); + unorderedCustomizers.add(firstCustomizer); + + SpringDocCustomizers springDocCustomizers = new SpringDocCustomizers( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of(unorderedCustomizers), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + assertThat(springDocCustomizers.getGlobalOpenApiCustomizersStream().toList()).containsExactly(firstCustomizer, lastCustomizer); + } + + @Order(1) + private static class FirstOpenApiCustomizer implements OpenApiCustomizer { + + @Override + public void customise(OpenAPI openApi) { + } + } + + @Order(2) + private static class LastOpenApiCustomizer implements OpenApiCustomizer { + + @Override + public void customise(OpenAPI openApi) { + } + } + + @Order(1) + private static class FirstGlobalOpenApiCustomizer implements GlobalOpenApiCustomizer { + + @Override + public void customise(OpenAPI openApi) { + } + } + + @Order(2) + private static class LastGlobalOpenApiCustomizer implements GlobalOpenApiCustomizer { + + @Override + public void customise(OpenAPI openApi) { + } + } +}