diff --git a/CHANGELOG.md b/CHANGELOG.md index e71124f9c..0fde4af6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2025-11-21 + +### Added +- #2975 - Spring Framework 7 - Initial API versioning support +- #3123 - Support static resources for webflux + +### Changed +- Upgrade to Spring Boot 4.0.0 +- Upgrade to Scalar 0.4.3 + +### Fixed + +- #3131 - Warning messages when docs are explicitly enabled +- #3121 - NPE in KotlinDeprecatedPropertyCustomizer - resolvedSchema is null + ## [3.0.0-RC1] - 2025-11-02 ### Added diff --git a/pom.xml b/pom.xml index 9b614166d..46cbce477 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 pom Spring openapi documentation Spring openapi documentation @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-RC1 + 4.0.0 @@ -35,7 +35,7 @@ scm:git:git@github.com:springdoc/springdoc-openapi.git scm:git:git@github.com:springdoc/springdoc-openapi.git - v3.0.0-RC1 + v3.0.0 @@ -63,7 +63,7 @@ 5.0.0-M1 2.0.0-M2 - 0.3.12 + 0.4.3 false @@ -123,11 +123,6 @@ spring-boot-starter-test test - - org.springframework.boot - spring-boot-jackson - test - @@ -248,4 +243,15 @@ + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + diff --git a/springdoc-openapi-bom/pom.xml b/springdoc-openapi-bom/pom.xml index 0fcd42a83..61c3a1dfd 100644 --- a/springdoc-openapi-bom/pom.xml +++ b/springdoc-openapi-bom/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-bom ${project.artifactId} diff --git a/springdoc-openapi-starter-common/pom.xml b/springdoc-openapi-starter-common/pom.xml index 57e7a24a7..eddb0ff99 100644 --- a/springdoc-openapi-starter-common/pom.xml +++ b/springdoc-openapi-starter-common/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-common ${project.artifactId} @@ -20,6 +20,10 @@ io.swagger.core.v3 swagger-core-jakarta + + org.springframework.boot + spring-boot-jackson + org.springframework.boot spring-boot-configuration-processor 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 c08a1a735..7ded98db0 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 @@ -43,7 +43,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Locale.LanguageRange; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -111,6 +113,7 @@ import org.springdoc.core.utils.PropertyResolverUtils; import org.springdoc.core.utils.SpringDocAnnotationsUtils; import org.springdoc.core.utils.SpringDocUtils; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.ObjectFactory; @@ -369,7 +372,7 @@ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) { .filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(), Hidden.class) == null)) .filter(controller -> !isHiddenRestControllers(controller.getValue().getClass())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a1, a2) -> a1)); + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a1, a2) -> a1)); Map findControllerAdvice = openAPIService.getControllerAdviceMap(); if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion()) { @@ -434,7 +437,7 @@ private Locale selectLocale(Locale inputLocale) { List allowedLocales = springDocConfigProperties.getAllowedLocales(); if (!CollectionUtils.isEmpty(allowedLocales)) { Locale bestMatchingAllowedLocale = Locale.lookup( - Locale.LanguageRange.parse(inputLocale.toLanguageTag()), + LanguageRange.parse(inputLocale.toLanguageTag()), allowedLocales.stream().map(Locale::forLanguageTag).toList() ); @@ -568,7 +571,9 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router String[] methodProduces = routerOperation.getProduces(); String[] headers = routerOperation.getHeaders(); Map queryParams = routerOperation.getQueryParams(); - + SpringDocVersionStrategy springDocVersionStrategy = routerOperation.getSpringDocVersionStrategy(); + if (springDocVersionStrategy != null) + queryParams = springDocVersionStrategy.updateQueryParams(queryParams); Components components = openAPI.getComponents(); Paths paths = openAPI.getPaths(); @@ -590,7 +595,7 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router RequestMapping reqMappingClass = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequestMapping.class); - MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale); + MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, springDocVersionStrategy, locale); methodAttributes.setMethodOverloaded(existingOperation != null); //Use the javadoc return if present if (javadocProvider != null) { @@ -655,7 +660,7 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router } } - Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class); + Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, Callback.class); // callbacks buildCallbacks(openAPI, methodAttributes, operation, apiCallbacks); @@ -757,7 +762,9 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale, Ope String[] methodProduces = routerOperation.getProduces(); String[] headers = routerOperation.getHeaders(); Map queryParams = routerOperation.getQueryParams(); - + SpringDocVersionStrategy springDocVersionStrategy = routerOperation.getSpringDocVersionStrategy(); + if(springDocVersionStrategy != null) + queryParams = springDocVersionStrategy.updateQueryParams(queryParams); Paths paths = openAPI.getPaths(); Map operationMap = null; if (paths.containsKey(operationPath)) { @@ -766,7 +773,7 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale, Ope } for (RequestMethod requestMethod : routerOperation.getMethods()) { Operation existingOperation = getExistingOperation(operationMap, requestMethod); - MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale); + MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, springDocVersionStrategy, locale); methodAttributes.setMethodOverloaded(existingOperation != null); Operation operation = getOperation(routerOperation, existingOperation); if (apiOperation != null) @@ -812,19 +819,20 @@ private RouterOperation customizeDataRestRouterOperation(RouterOperation routerO /** * Calculate path. * - * @param handlerMethod the handler method - * @param operationPath the operation path - * @param requestMethods the request methods - * @param consumes the consumes - * @param produces the produces - * @param headers the headers - * @param params the params - * @param locale the locale - * @param openAPI the open api + * @param handlerMethod the handler method + * @param operationPath the operation path + * @param requestMethods the request methods + * @param consumes the consumes + * @param produces the produces + * @param headers the headers + * @param params the params + * @param versionStrategy the version strategy + * @param locale the locale + * @param openAPI the open api */ protected void calculatePath(HandlerMethod handlerMethod, String operationPath, - Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, Locale locale, OpenAPI openAPI) { - this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params), locale, openAPI); + Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, SpringDocVersionStrategy versionStrategy, Locale locale, OpenAPI openAPI) { + this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params, versionStrategy), locale, openAPI); } /** @@ -1227,10 +1235,16 @@ private void fillParametersList(Operation operation, Map queryPa } }); if (!CollectionUtils.isEmpty(queryParams)) { - for (Map.Entry entry : queryParams.entrySet()) { - io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter(); - parameter.setName(entry.getKey()); - parameter.setSchema(new StringSchema()._default(entry.getValue())); + Map versionDefaultMap = null; + if(methodAttributes.getSpringDocVersionStrategy() != null) + versionDefaultMap= methodAttributes.getSpringDocVersionStrategy().getVersionDefaultMap(); + for (Entry entry : queryParams.entrySet()) { + Parameter parameter = new Parameter(); + String name = entry.getKey(); + String value = entry.getValue(); + String defaultValue = (versionDefaultMap != null) ? versionDefaultMap.get(name) : value; + parameter.setName(name); + parameter.setSchema(new StringSchema()._default(defaultValue)._enum(Collections.singletonList(value))); parameter.setRequired(true); parameter.setIn(ParameterIn.QUERY.toString()); GenericParameterService.mergeParameter(parametersList, parameter); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java index 2b8882808..e01cd6e00 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java @@ -736,6 +736,6 @@ MethodParameterPojoExtractor methodParameterPojoExtractor(SchemaUtils schemaUtil @ConditionalOnMissingBean(name = "springDocAppInitializer") @Lazy(false) SpringDocAppInitializer springDocAppInitializer(SpringDocConfigProperties springDocConfigProperties){ - return new SpringDocAppInitializer(springDocConfigProperties.getApiDocs().getPath(), SPRINGDOC_ENABLED); + return new SpringDocAppInitializer(springDocConfigProperties.getApiDocs().getPath(), SPRINGDOC_ENABLED, springDocConfigProperties.getApiDocs().isEnabled()); } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java index add34935f..12a12fede 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java @@ -54,11 +54,11 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.data.core.TypeInformation; import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; import org.springframework.data.querydsl.binding.QuerydslBindings; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; import org.springframework.data.querydsl.binding.QuerydslPredicate; -import org.springframework.data.util.TypeInformation; import org.springframework.util.CollectionUtils; import org.springframework.web.method.HandlerMethod; diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java index bd37f965a..7e2d1c95b 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java @@ -285,7 +285,7 @@ else if (ControllerType.PROPERTY.equals(controllerType)) MethodResourceMapping methodResourceMapping, HandlerMethod handlerMethod, RequestMethod requestMethod, ResourceMetadata resourceMetadata, String operationPath, ControllerType controllerType) { - RouterOperation routerOperation = new RouterOperation(operationPath, new RequestMethod[] { requestMethod }, null, null, null, null); + RouterOperation routerOperation = new RouterOperation(operationPath, new RequestMethod[] { requestMethod }, null, null, null, null,null); MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), dataRestRepository.getLocale()); methodAttributes.calculateConsumesProduces(handlerMethod.getMethod()); routerOperation.setConsumes(methodAttributes.getMethodConsumes()); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java index f1a02b05b..83fa21557 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java @@ -49,6 +49,11 @@ public class SpringDocAppInitializer { */ private final String property; + /** + * The Springdoc enabled. + */ + private final boolean springdocEnabled; + /** * The constant LOGGER. */ @@ -57,12 +62,14 @@ public class SpringDocAppInitializer { /** * Instantiates a new Spring doc app initializer. * - * @param endpoint the endpoint - * @param property the property + * @param endpoint the endpoint + * @param property the property + * @param springdocEnabled the springdoc enabled */ - public SpringDocAppInitializer(String endpoint, String property) { + public SpringDocAppInitializer(String endpoint, String property, boolean springdocEnabled) { this.endpoint = endpoint; this.property = property; + this.springdocEnabled = springdocEnabled; } /** @@ -70,6 +77,7 @@ public SpringDocAppInitializer(String endpoint, String property) { */ @EventListener(ApplicationReadyEvent.class) public void init() { - LOGGER.warn("SpringDoc {} endpoint is enabled by default. To disable it in production, set the property '{}=false'", endpoint, property); + if(!this.springdocEnabled) + LOGGER.warn("SpringDoc {} endpoint is enabled by default. To disable it in production, set the property '{}=false'", endpoint, property); } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java index 693547609..cdec2edaa 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java @@ -35,6 +35,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.operation.Builder; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springframework.web.bind.annotation.RequestMethod; @@ -107,6 +108,11 @@ public class RouterOperation implements Comparable { */ private io.swagger.v3.oas.models.Operation operationModel; + /** + * The Spring doc version strategy. + */ + private SpringDocVersionStrategy springDocVersionStrategy; + /** * Instantiates a new Router operation. */ @@ -154,20 +160,22 @@ public RouterOperation(org.springdoc.core.annotations.RouterOperation routerOper /** * Instantiates a new Router operation. * - * @param path the path - * @param methods the methods - * @param consumes the consumes - * @param produces the produces - * @param headers the headers - * @param params the params - */ - public RouterOperation(String path, RequestMethod[] methods, String[] consumes, String[] produces, String[] headers, String[] params) { + * @param path the path + * @param methods the methods + * @param consumes the consumes + * @param produces the produces + * @param headers the headers + * @param params the params + * @param springDocVersionStrategy the version strategy + */ + public RouterOperation(String path, RequestMethod[] methods, String[] consumes, String[] produces, String[] headers, String[] params, SpringDocVersionStrategy springDocVersionStrategy) { this.path = path; this.methods = methods; this.consumes = consumes; this.produces = produces; this.headers = headers; this.params = params; + this.springDocVersionStrategy = springDocVersionStrategy; } /** @@ -422,6 +430,24 @@ public void setOperationModel(io.swagger.v3.oas.models.Operation operationModel) this.operationModel = operationModel; } + /** + * Gets version strategy. + * + * @return the version strategy + */ + public SpringDocVersionStrategy getSpringDocVersionStrategy() { + return springDocVersionStrategy; + } + + /** + * Sets version strategy. + * + * @param springDocVersionStrategy the version strategy + */ + public void setVersionStrategy(SpringDocVersionStrategy springDocVersionStrategy) { + this.springDocVersionStrategy = springDocVersionStrategy; + } + @Override public int compareTo(RouterOperation routerOperation) { int result = path.compareTo(routerOperation.getPath()); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java index 8baac19d9..59e6da6df 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java @@ -41,6 +41,9 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; +import org.springdoc.core.versions.HeaderVersionStrategy; +import org.springdoc.core.versions.MediaTypeVersionStrategy; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.web.bind.annotation.DeleteMapping; @@ -136,6 +139,11 @@ public class MethodAttributes { */ private boolean useReturnTypeSchema; + /** + * The Spring doc version strategy. + */ + private SpringDocVersionStrategy springDocVersionStrategy; + /** * Instantiates a new Method attributes. * @@ -174,14 +182,17 @@ public MethodAttributes(String defaultConsumesMediaType, String defaultProducesM * @param methodConsumes the method consumes * @param methodProduces the method produces * @param headers the headers + * @param springDocVersionStrategy the spring doc version strategy * @param locale the locale */ - public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType, String[] methodConsumes, String[] methodProduces, String[] headers, Locale locale) { + public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType, String[] methodConsumes, String[] methodProduces, String[] headers, + SpringDocVersionStrategy springDocVersionStrategy, Locale locale) { this.defaultConsumesMediaType = defaultConsumesMediaType; this.defaultProducesMediaType = defaultProducesMediaType; this.methodProduces = methodProduces; this.methodConsumes = methodConsumes; this.locale = locale; + this.springDocVersionStrategy = springDocVersionStrategy; setHeaders(headers); } @@ -296,6 +307,10 @@ else if (reqMappingClass != null) { private void fillMethods(String[] produces, String[] consumes, String[] headers) { if (ArrayUtils.isNotEmpty(produces)) { methodProduces = mergeArrays(methodProduces, produces); + if (springDocVersionStrategy instanceof MediaTypeVersionStrategy mediaTypeVersionStrategy + && mediaTypeVersionStrategy.getVersion() != null) { + methodProduces = mediaTypeVersionStrategy.buildProduces(); + } } else if (ArrayUtils.isNotEmpty(classProduces)) { methodProduces = mergeArrays(methodProduces, classProduces); @@ -318,13 +333,13 @@ else if (ArrayUtils.isEmpty(methodConsumes)) { } /** - * If there is any method type(s) present, then these will override the class type(s). - * See ... for details + * If there is any method type(s) present, then these will override the class type(s). + * See ... for details * - * @param methodTypes the method types - * @param classTypes the class types - * @return the string [ ] containing the types that can be used for the method - */ + * @param methodTypes the method types + * @param classTypes the class types + * @return the string [ ] containing the types that can be used for the method + */ private String[] calculateMethodMediaTypes(@Nullable String[] methodTypes, String[] classTypes) { if (ArrayUtils.isNotEmpty(methodTypes)) { return methodTypes; @@ -340,9 +355,45 @@ private String[] calculateMethodMediaTypes(@Nullable String[] methodTypes, Strin * @return the string [ ] */ private String[] mergeArrays(@Nullable String[] array1, String[] array2) { - Set uniqueValues = array1 == null ? new LinkedHashSet<>() : Arrays.stream(array1).collect(Collectors.toCollection(LinkedHashSet::new)); - uniqueValues.addAll(Arrays.asList(array2)); - return uniqueValues.toArray(new String[0]); + Set merged = array1 == null + ? new LinkedHashSet<>() + : Arrays.stream(array1).collect(Collectors.toCollection(LinkedHashSet::new)); + + merged.addAll(Arrays.asList(array2)); + + // Apply dynamic version filtering if versionStrategy is active + if (springDocVersionStrategy != null + && springDocVersionStrategy instanceof MediaTypeVersionStrategy mediaTypeVersionStrategy + && mediaTypeVersionStrategy.getVersion() != null) { + + // Remove unversioned media types if versioned variants exist + Set baseTypesWithVersion = merged.stream() + .filter(mt -> mt.contains(";")) + .filter(this::containsVersionParameter) + .map(mt -> mt.split(";", 2)[0]) + .collect(Collectors.toSet()); + + merged.removeIf(mt -> baseTypesWithVersion.contains(mt) && !mt.contains(";")); + } + + return merged.toArray(new String[0]); + } + + /** + * Contains version parameter boolean. + * + * @param mediaType the media type + * @return the boolean + */ + private boolean containsVersionParameter(String mediaType) { + String[] parts = mediaType.split(";"); + for (int i = 1; i < parts.length; i++) { + String param = parts[i].trim().toLowerCase(); + if (param.startsWith("version=") || param.startsWith("v=") || param.contains("version=")) { + return true; + } + } + return false; } /** @@ -419,6 +470,15 @@ public void setJsonViewAnnotationForRequestBody(JsonView jsonViewAnnotationForRe this.jsonViewAnnotationForRequestBody = jsonViewAnnotationForRequestBody; } + /** + * Is with response body schema doc boolean. + * + * @return the boolean + */ + public boolean isWithResponseBodySchemaDoc() { + return withResponseBodySchemaDoc; + } + /** * Gets headers. * @@ -447,6 +507,11 @@ private void setHeaders(String[] headers) { this.headers.put(keyValueHeader[0], StringUtils.EMPTY); } } + + if (springDocVersionStrategy instanceof HeaderVersionStrategy headerVersionStrategy + && headerVersionStrategy.getVersion() != null) { + this.headers.put(headerVersionStrategy.getHeaderName(), headerVersionStrategy.getVersion()); + } } /** @@ -471,15 +536,6 @@ public Map getGenericMapResponse() { return genericMapResponse; } - /** - * Is with response body schema doc boolean. - * - * @return the boolean - */ - public boolean isWithResponseBodySchemaDoc() { - return withResponseBodySchemaDoc; - } - /** * Sets with response body schema doc. * @@ -545,4 +601,13 @@ public boolean isUseReturnTypeSchema() { public void setUseReturnTypeSchema(boolean useReturnTypeSchema) { this.useReturnTypeSchema = useReturnTypeSchema; } + + /** + * Gets spring doc version strategy. + * + * @return the spring doc version strategy + */ + public SpringDocVersionStrategy getSpringDocVersionStrategy() { + return springDocVersionStrategy; + } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java index d27787f51..1ea92f1ff 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java @@ -1434,7 +1434,7 @@ public static class ApiDocs { /** * Whether to generate and serve an OpenAPI document. */ - private boolean enabled = true; + private boolean enabled; /** * The Resolve schema properties. diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java index dd3544909..c3e95e6f3 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java @@ -96,7 +96,7 @@ public class SwaggerUiConfigProperties extends AbstractSwaggerUiConfigProperties /** * Whether to generate and serve an OpenAPI document. */ - private boolean enabled = true; + private boolean enabled; /** * The Use root path. diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java index aebda82b2..71e9e2417 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java @@ -25,14 +25,21 @@ */ package org.springdoc.core.providers; +import java.util.HashMap; import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.versions.SpringDocApiVersionType; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.util.CollectionUtils; /** * The type Spring web provider. @@ -41,6 +48,11 @@ */ public abstract class SpringWebProvider implements ApplicationContextAware { + /** + * The constant LOGGER. + */ + protected static final Logger LOGGER = LoggerFactory.getLogger(SpringWebProvider.class); + /** * The Application context. */ @@ -51,6 +63,11 @@ public abstract class SpringWebProvider implements ApplicationContextAware { */ protected Map handlerMethods; + /** + * The Spring doc version strategy map. + */ + protected final Map springDocVersionStrategyMap = new HashMap<>(); + /** * Gets handler methods. * @@ -74,6 +91,46 @@ public abstract class SpringWebProvider implements ApplicationContextAware { */ public abstract Set getActivePatterns(Object requestMappingInfo); + /** + * Gets spring doc version strategy. + * + * @param version the version + * @param params the params + * @return the spring doc version strategy + */ + public SpringDocVersionStrategy getSpringDocVersionStrategy(String version, String[] params) { + SpringDocVersionStrategy springDocVersionStrategy = null; + if (!CollectionUtils.isEmpty(springDocVersionStrategyMap)) { + if (springDocVersionStrategyMap.size() == 1) + springDocVersionStrategy = springDocVersionStrategyMap.values().iterator().next(); + else + springDocVersionStrategy = resolveApiVersionStrategy(version, params); + springDocVersionStrategy.updateVersion(version, params); + } + return springDocVersionStrategy; + } + + /** + * Resolve api version strategy spring doc version strategy. + * + * @param version the version + * @param params the params + * @return the spring doc version strategy + */ + private SpringDocVersionStrategy resolveApiVersionStrategy(String version, String[] params) { + if (version != null) { + if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.PATH)) + return springDocVersionStrategyMap.get(SpringDocApiVersionType.PATH); + else if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.HEADER)) + return springDocVersionStrategyMap.get(SpringDocApiVersionType.HEADER); + else if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.MEDIA_TYPE)) + return springDocVersionStrategyMap.get(SpringDocApiVersionType.MEDIA_TYPE); + } + if (ArrayUtils.isNotEmpty(params) && springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.QUERY_PARAM)) + return springDocVersionStrategyMap.get(SpringDocApiVersionType.QUERY_PARAM); + return springDocVersionStrategyMap.values().iterator().next(); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java index 0e6dcfe00..0dce0206f 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java @@ -72,6 +72,7 @@ import org.slf4j.LoggerFactory; import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer; import org.springdoc.core.customizers.ParameterCustomizer; +import org.springdoc.core.customizers.PropertyCustomizer; import org.springdoc.core.customizers.SpringDocCustomizers; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; import org.springdoc.core.extractor.DelegatingMethodParameter; @@ -124,7 +125,7 @@ public abstract class AbstractRequestService { * The constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRequestService.class); - + /** * The constant ACTUATOR_PKGS. */ @@ -132,7 +133,7 @@ public abstract class AbstractRequestService { "org.springframework.boot.webmvc.actuate", "org.springframework.boot.webflux.actuate" ); - + /** * The constant PARAM_TYPES_TO_IGNORE. */ @@ -264,19 +265,31 @@ public static boolean isRequestTypeToIgnore(Class rawClass) { */ @SuppressWarnings("unchecked") public static Collection getHeaders(MethodAttributes methodAttributes, Map map) { + Map versionDefaultMap = null; + if (methodAttributes.getSpringDocVersionStrategy() != null) + versionDefaultMap = methodAttributes.getSpringDocVersionStrategy().getVersionDefaultMap(); for (Entry entry : methodAttributes.getHeaders().entrySet()) { StringSchema schema = new StringSchema(); - if (StringUtils.isNotEmpty(entry.getValue())) - schema.addEnumItem(entry.getValue()); - Parameter parameter = new Parameter().in(ParameterIn.HEADER.toString()).name(entry.getKey()).schema(schema); + String headerName = entry.getKey(); + String headerValue = entry.getValue(); + if (StringUtils.isNotEmpty(headerValue)) + schema.addEnumItem(headerValue); + String defaultValue = null; + if(versionDefaultMap != null) { + defaultValue = versionDefaultMap.get(headerName); + schema._default(defaultValue); + } + Parameter parameter = new Parameter().in(ParameterIn.HEADER.toString()).name(headerName).schema(schema); ParameterId parameterId = new ParameterId(parameter); if (map.containsKey(parameterId)) { parameter = map.get(parameterId); List existingEnum = null; if (parameter.getSchema() != null && !CollectionUtils.isEmpty(parameter.getSchema().getEnum())) existingEnum = parameter.getSchema().getEnum(); - if (StringUtils.isNotEmpty(entry.getValue()) && (existingEnum == null || !existingEnum.contains(entry.getValue()))) - parameter.getSchema().addEnumItemObject(entry.getValue()); + if (StringUtils.isNotEmpty(headerValue) && (existingEnum == null || !existingEnum.contains(headerValue))) + parameter.getSchema().addEnumItemObject(headerValue); + if (defaultValue != null && (existingEnum == null || !existingEnum.contains(defaultValue))) + parameter.getSchema().addEnumItemObject(defaultValue); parameter.setSchema(parameter.getSchema()); } map.put(parameterId, parameter); @@ -293,9 +306,9 @@ public static Collection getHeaders(MethodAttributes methodAttributes * @param methodAttributes the method attributes * @param openAPI the open api * @return the operation - * @see org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) + * @see DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) * @see ParameterCustomizer#customize(Parameter, MethodParameter) ParameterCustomizer#customize(Parameter, MethodParameter) - * @see org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType) org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType) + * @see PropertyCustomizer#customize(Schema, AnnotatedType) org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType) */ public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod, Operation operation, MethodAttributes methodAttributes, OpenAPI openAPI) { @@ -530,7 +543,7 @@ private boolean isRequestBodyWithMapType(MethodParameter parameter) { if (ACTUATOR_PKGS.stream().anyMatch(pkg::startsWith)) { return false; } - + // Check for @RequestBody annotation org.springframework.web.bind.annotation.RequestBody requestBody = parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestBody.class); if (requestBody == null) { @@ -549,7 +562,7 @@ private boolean isRequestBodyWithMapType(MethodParameter parameter) { */ private boolean isRequestPartWithMapType(MethodParameter parameter) { // Check for @RequestPart annotation - org.springframework.web.bind.annotation.RequestPart requestPart = parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestPart.class); + RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); if (requestPart == null) { return false; } @@ -693,7 +706,7 @@ public void applyBeanValidatorAnnotations(final MethodParameter methodParameter, java.lang.reflect.AnnotatedType[] typeArgs = paramType.getAnnotatedActualTypeArguments(); for (java.lang.reflect.AnnotatedType typeArg : typeArgs) { List genericAnnotations = Arrays.stream(typeArg.getAnnotations()).toList(); - Schema schemaItemsClone = cloneViaJson(schema.getItems(), Schema.class, ObjectMapperProvider.createJson(parameterBuilder.getPropertyResolverUtils().getSpringDocConfigProperties())); + Schema schemaItemsClone = cloneViaJson(schema.getItems(), Schema.class, ObjectMapperProvider.createJson(parameterBuilder.getPropertyResolverUtils().getSpringDocConfigProperties())); schema.items(schemaItemsClone); SchemaUtils.applyValidationsToSchema(schema.getItems(), genericAnnotations, openapiVersion); } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java index ebb63f801..4d04070ce 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java @@ -57,6 +57,7 @@ import io.swagger.v3.oas.models.media.FileSchema; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; @@ -237,7 +238,7 @@ public static void mergeParameter(Parameter paramCalcul, Parameter paramDoc) { paramDoc.setAllowReserved(paramCalcul.getAllowReserved()); if (StringUtils.isBlank(paramDoc.get$ref())) - paramDoc.set$ref(paramDoc.get$ref()); + paramDoc.set$ref(paramCalcul.get$ref()); if (paramDoc.getSchema() == null && paramDoc.getContent() == null) paramDoc.setSchema(paramCalcul.getSchema()); @@ -253,6 +254,29 @@ public static void mergeParameter(Parameter paramCalcul, Parameter paramDoc) { if (paramDoc.getExplode() == null) paramDoc.setExplode(paramCalcul.getExplode()); + + if (paramDoc.getSchema() instanceof StringSchema existingSchema && + paramCalcul.getSchema() instanceof StringSchema newSchema) { + + List existingEnums = existingSchema.getEnum() != null + ? new ArrayList<>(existingSchema.getEnum()) + : new ArrayList<>(); + + List newEnums = newSchema.getEnum(); + + if (newEnums != null && !newEnums.isEmpty()) { + for (String val : newEnums) { + if (!existingEnums.contains(val)) { + existingEnums.add(val); + } + } + existingSchema.setEnum(existingEnums); + } + + if (newSchema.getDefault() != null) { + existingSchema.setDefault(newSchema.getDefault()); + } + } } /** diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java index 35ec79e9b..1b8bdd309 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java @@ -299,7 +299,9 @@ private RequestBody buildRequestBody(io.swagger.v3.oas.annotations.parameters.Re Schema schema = parameterBuilder.calculateSchema(components, parameterInfo, requestBodyInfo, methodAttributes.getJsonViewAnnotationForRequestBody()); Map parameterEncoding = getParameterEncoding(parameterInfo); - buildContent(requestBody, methodAttributes, schema, parameterEncoding); + // If a content type is explicitly stated with a @RequestBody annotation, yield to that content type + if (!methodAttributes.isWithResponseBodySchemaDoc()) + buildContent(requestBody, methodAttributes, schema, parameterEncoding); // Add requestBody javadoc if (StringUtils.isBlank(requestBody.getDescription()) && parameterBuilder.getJavadocProvider() != null diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java index 6ffef2802..43f3c9870 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java @@ -52,6 +52,7 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; +import org.springframework.data.core.TypeInformation; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimpleAssociationHandler; @@ -61,7 +62,6 @@ import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.RestMediaTypes; -import org.springframework.data.util.TypeInformation; import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.util.CollectionUtils; diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java new file mode 100644 index 000000000..28895bc93 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java @@ -0,0 +1,61 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +/** + * The type Header version strategy. + * + * @author bnasslahsen + */ +public class HeaderVersionStrategy extends SpringDocVersionStrategy { + + /** + * The Header name. + */ + private final String headerName; + + + /** + * Instantiates a new Header version strategy. + * + * @param headerName the header name + */ + public HeaderVersionStrategy(String headerName, String defaultVersion) { + super(defaultVersion); + this.headerName = headerName; + if(defaultVersion != null) + versionDefaultMap.put(headerName, defaultVersion); + } + + /** + * Gets header name. + * + * @return the header name + */ + public String getHeaderName() { + return headerName; + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java new file mode 100644 index 000000000..f43e5406a --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java @@ -0,0 +1,91 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +import org.springframework.http.MediaType; + +/** + * The type Media type version strategy. + * + * @author bnasslahsen + */ +public class MediaTypeVersionStrategy extends SpringDocVersionStrategy { + + /** + * The Media type. + */ + private final MediaType mediaType; + + /** + * The Parameter name. + */ + private final String parameterName; + + /** + * Instantiates a new Media type version strategy. + * + * @param mediaType the media type + * @param parameterName the parameter name + */ + public MediaTypeVersionStrategy(MediaType mediaType, String parameterName, String defaultVersion) { + super(defaultVersion); + this.mediaType = mediaType; + this.parameterName = parameterName; + if(defaultVersion != null) + versionDefaultMap.put(parameterName, defaultVersion); + } + + /** + * Gets media type. + * + * @return the media type + */ + public MediaType getMediaType() { + return mediaType; + } + + /** + * Gets parameter name. + * + * @return the parameter name + */ + public String getParameterName() { + return parameterName; + } + + /** + * Build produces string [ ]. + * + * @return the string [ ] + */ + public String[] buildProduces() { + String type = mediaType.getType(); + String subtype = mediaType.getSubtype(); + String produces = String.format("%s/%s;%s=%s", type, subtype, parameterName, version); + return new String[] { produces }; + } + +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java new file mode 100644 index 000000000..495d96ea2 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java @@ -0,0 +1,97 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +import java.util.ArrayList; +import java.util.List; + +/** + * The type Path version strategy. + * + * @author bnasslahsen + */ +public class PathVersionStrategy extends SpringDocVersionStrategy { + + /** + * The Path segment index. + */ + private final int pathSegmentIndex; + + /** + * Instantiates a new Path version strategy. + * + * @param pathSegmentIndex the path segment index + */ + public PathVersionStrategy(int pathSegmentIndex, String defaultVersion) { + super(defaultVersion); + this.pathSegmentIndex = pathSegmentIndex; + } + + /** + * Gets path segment index. + * + * @return the path segment index + */ + public int getPathSegmentIndex() { + return pathSegmentIndex; + } + + + @Override + public String updateOperationPath(String operationPath, String version) { + if (operationPath == null || version == null) { + return operationPath; + } + String[] segments = operationPath.split("/"); + List updatedSegments = new ArrayList<>(); + int segmentCount = 0; + + for (String segment : segments) { + if (segment.isEmpty()) { + continue; + } + + if (segmentCount == pathSegmentIndex) { + String newSegment = version; + if (segment.contains("{")) { + // Extract static prefix before placeholder + int placeholderStart = segment.indexOf('{'); + String prefix = segment.substring(0, placeholderStart); + newSegment = prefix + version; + } + updatedSegments.add(newSegment); + } else { + updatedSegments.add(segment); + } + segmentCount++; + } + return "/" + String.join("/", updatedSegments); + } + +} + + + diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java new file mode 100644 index 000000000..03eef82d2 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java @@ -0,0 +1,88 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The type Query param version strategy. + * + * @author bnasslahsen + */ +public class QueryParamVersionStrategy extends SpringDocVersionStrategy { + + /** + * The Parameter name. + */ + private final String parameterName; + + /** + * Instantiates a new Query param version strategy. + * + * @param parameterName the parameter name + */ + public QueryParamVersionStrategy(String parameterName, String defaultVersion) { + super(defaultVersion); + this.parameterName = parameterName; + if(defaultVersion != null) + versionDefaultMap.put(parameterName, defaultVersion); + } + + /** + * Gets parameter name. + * + * @return the parameter name + */ + public String getParameterName() { + return parameterName; + } + + + @Override + public void updateVersion(String version, String[] params) { + for (String param : params) { + if (param.contains("=")) { + String[] paramValues = param.split("=", 2); + String paramName = paramValues[0]; + String paramValue = paramValues[1]; + if (parameterName.equals(paramName)) { + setVersion(paramValue); + break; + } + } + } + } + + @Override + public Map updateQueryParams(Map queryParams) { + if (queryParams == null) + queryParams = new LinkedHashMap<>(); + if(version !=null) + queryParams.put(parameterName, version); + return queryParams; + } +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java new file mode 100644 index 000000000..5afbb1204 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java @@ -0,0 +1,33 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +/** + * @author bnasslahsen + */ +public enum SpringDocApiVersionType { + PATH, HEADER, QUERY_PARAM, MEDIA_TYPE +} \ No newline at end of file diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java new file mode 100644 index 000000000..91d1482dc --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java @@ -0,0 +1,114 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.versions; + +import java.util.HashMap; +import java.util.Map; + +/** + * The interface Spring doc version strategy. + * + * @author bnasslahsen + */ +public abstract class SpringDocVersionStrategy { + + /** + * The Version. + */ + protected String version; + + /** + * The Default version. + */ + protected final String defaultVersion; + + /** + * The Default values. + */ + protected Map versionDefaultMap = new HashMap<>(); + + /** + * Instantiates a new Spring doc version strategy. + * + * @param defaultVersion the default version + */ + protected SpringDocVersionStrategy(String defaultVersion) { + this.defaultVersion = defaultVersion; + } + + /** + * Gets version. + * + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * Sets version. + * + * @param version the version + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Update version. + * + * @param version the version + * @param params the params + */ + public void updateVersion(String version, String[] params) { + setVersion(version); + } + + /** + * Update query params map. + * + * @param queryParams the query params + * @return the map + */ + public Map updateQueryParams(Map queryParams) { + return queryParams; + } + + /** + * Gets operation path. + * + * @param operationPath the operation path + * @param version the version + * @return the operation path + */ + public String updateOperationPath(String operationPath, String version) { + return operationPath; + } + + public Map getVersionDefaultMap() { + return versionDefaultMap; + } +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java index 68120fd52..af83251b5 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java @@ -28,17 +28,18 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.Collectors; import com.scalar.maven.webjar.ScalarProperties; -import com.scalar.maven.webjar.ScalarProperties.ScalarSource; +import com.scalar.maven.webjar.internal.ScalarConfiguration; +import com.scalar.maven.webjar.internal.ScalarConfigurationMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import static org.springdoc.scalar.ScalarConstants.HTML_TEMPLATE_PATH; import static org.springdoc.scalar.ScalarConstants.SCALAR_DEFAULT_URL; import static org.springdoc.scalar.ScalarConstants.SCALAR_JS_FILENAME; import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR; @@ -60,14 +61,21 @@ public abstract class AbstractScalarController { */ protected final String originalScalarUrl; + /** + * The Object mapper. + */ + private final ObjectMapper objectMapper; + /** * Instantiates a new Abstract scalar controller. * * @param scalarProperties the scalar properties + * @param objectMapper the object mapper */ - protected AbstractScalarController(ScalarProperties scalarProperties) { + protected AbstractScalarController(ScalarProperties scalarProperties, ObjectMapper objectMapper) { this.scalarProperties = scalarProperties; this.originalScalarUrl = scalarProperties.getUrl(); + this.objectMapper = objectMapper; } /** @@ -82,22 +90,18 @@ protected AbstractScalarController(ScalarProperties scalarProperties) { */ protected ResponseEntity getDocs(String requestUrl) throws IOException { // Load the template HTML - InputStream inputStream = getClass().getResourceAsStream("/META-INF/resources/webjars/scalar/index.html"); + InputStream inputStream = getClass().getResourceAsStream(HTML_TEMPLATE_PATH); if (inputStream == null) { - return ResponseEntity.notFound().build(); + throw new IOException("HTML template not found at: " + HTML_TEMPLATE_PATH); } String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - requestUrl = decode(requestUrl); + // Replace the placeholders with actual values - String cdnUrl = buildJsBundleUrl(requestUrl); + String bundleUrl = buildJsBundleUrl(requestUrl); String injectedHtml = html - .replace("__JS_BUNDLE_URL__", cdnUrl) - .replace("__CONFIGURATION__", """ - { - url: "%s" - } - """.formatted(buildApiDocsUrl(requestUrl))); + .replace("__JS_BUNDLE_URL__", bundleUrl) + .replace("__CONFIGURATION__", buildConfigurationJson(buildApiDocsUrl(requestUrl))); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) @@ -126,16 +130,6 @@ protected ResponseEntity getScalarJs() throws IOException { .body(jsContent); } - /** - * Decode string. - * - * @param requestURI the request uri - * @return the string - */ - protected String decode(String requestURI) { - return URLDecoder.decode(requestURI, StandardCharsets.UTF_8); - } - /** * Gets api docs url. * @@ -186,159 +180,20 @@ protected String buildJsBundleUrl(String requestUrl, String scalarPath) { */ protected abstract String buildJsBundleUrl(String requestUrl); - /** - * Builds the configuration JSON for the Scalar API Reference. - * - * @return the configuration JSON as a string - */ - private String buildConfigurationJson() { - StringBuilder config = new StringBuilder(); - config.append("{"); - - // Add URL - config.append("\n url: \"").append(escapeJson(scalarProperties.getUrl())).append("\""); - - // Add sources - if (scalarProperties.getSources() != null && !scalarProperties.getSources().isEmpty()) { - config.append(",\n sources: ").append(buildSourcesJsonArray(scalarProperties.getSources())); - } - - // Add showSidebar - if (!scalarProperties.isShowSidebar()) { - config.append(",\n showSidebar: false"); - } - - // Add hideModels - if (scalarProperties.isHideModels()) { - config.append(",\n hideModels: true"); - } - - // Add hideTestRequestButton - if (scalarProperties.isHideTestRequestButton()) { - config.append(",\n hideTestRequestButton: true"); - } - - // Add darkMode - if (scalarProperties.isDarkMode()) { - config.append(",\n darkMode: true"); - } - - // Add hideDarkModeToggle - if (scalarProperties.isHideDarkModeToggle()) { - config.append(",\n hideDarkModeToggle: true"); - } - - // Add customCss - if (scalarProperties.getCustomCss() != null && !scalarProperties.getCustomCss().trim().isEmpty()) { - config.append(",\n customCss: \"").append(escapeJson(scalarProperties.getCustomCss())).append("\""); - } - - // Add theme - if (scalarProperties.getTheme() != null && !"default".equals(scalarProperties.getTheme())) { - config.append(",\n theme: \"").append(escapeJson(scalarProperties.getTheme())).append("\""); - } - - // Add layout - if (scalarProperties.getLayout() != null && !"modern".equals(scalarProperties.getLayout())) { - config.append(",\n layout: \"").append(escapeJson(scalarProperties.getLayout())).append("\""); - } - - // Add hideSearch - if (scalarProperties.isHideSearch()) { - config.append(",\n hideSearch: true"); - } - - // Add documentDownloadType - if (scalarProperties.getDocumentDownloadType() != null && !"both".equals(scalarProperties.getDocumentDownloadType())) { - config.append(",\n documentDownloadType: \"").append(escapeJson(scalarProperties.getDocumentDownloadType())).append("\""); - } - - config.append("\n}"); - return config.toString(); - } - - /** - * Escapes a string for JSON output. + /** + * Build configuration json string. * - * @param input the input string - * @return the escaped string - */ - private String escapeJson(String input) { - if (input == null) { - return ""; - } - return input.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); - } - - /** - * Builds the JSON for the OpenAPI reference sources - * - * @param sources list of OpenAPI reference sources - * @return the sources as a JSON string - */ - private String buildSourcesJsonArray(List sources) { - final StringBuilder builder = new StringBuilder("["); - - // Filter out sources with invalid urls - final List filteredSources = sources.stream() - .filter(source -> isNotNullOrBlank(source.getUrl())) - .collect(Collectors.toList()); - - // Append each source to json array - for (int i = 0; i < filteredSources.size(); i++) { - final ScalarSource source = filteredSources.get(i); - - final String sourceJson = buildSourceJson(source); - builder.append("\n").append(sourceJson); - - if (i != filteredSources.size() - 1) { - builder.append(","); - } - } - - builder.append("\n]"); - return builder.toString(); - } - - /** - * Builds the JSON for an OpenAPI reference source - * - * @param source the OpenAPI reference source - * @return the source as a JSON string + * @param requestUrl the request url + * @return the string */ - private String buildSourceJson(ScalarSource source) { - final StringBuilder builder = new StringBuilder("{"); - - builder.append("\n url: \"").append(escapeJson(source.getUrl())).append("\""); - - - if (isNotNullOrBlank(source.getTitle())) { - builder.append(",\n title: \"").append(escapeJson(source.getTitle())).append("\""); + private String buildConfigurationJson(String requestUrl) { + try { + this.scalarProperties.setUrl(requestUrl); + ScalarConfiguration config = ScalarConfigurationMapper.map(scalarProperties); + return objectMapper.writeValueAsString(config); } - - if (isNotNullOrBlank(source.getSlug())) { - builder.append(",\n slug: \"").append(escapeJson(source.getSlug())).append("\""); - } - - if (source.isDefault() != null) { - builder.append(",\n default: ").append(source.isDefault()); + catch (JacksonException e) { + throw new RuntimeException("Failed to serialize Scalar configuration", e); } - - builder.append("\n}"); - return builder.toString(); - } - - /** - * Returns whether a String is not null or blank - * - * @param input the string - * @return whether the string is not null or blank - */ - private boolean isNotNullOrBlank(String input) { - return input != null && !input.isBlank(); } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java index c694f2132..729e32db7 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java @@ -46,10 +46,16 @@ public class ScalarConstants { /** * The constant SCALAR_DEFAULT_URL. */ - public static final String SCALAR_DEFAULT_URL = "https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json"; + public static final String SCALAR_DEFAULT_URL = "https://registry.scalar.com/@scalar/apis/galaxy?format=json"; /** * The constant DEFAULT_SCALAR_ACTUATOR_PATH. */ public static final String DEFAULT_SCALAR_ACTUATOR_PATH = "scalar"; + + /** + * The constant HTML_TEMPLATE_PATH. + */ + public static final String HTML_TEMPLATE_PATH = "/META-INF/resources/webjars/scalar/index.html"; + } diff --git a/springdoc-openapi-starter-webflux-api/pom.xml b/springdoc-openapi-starter-webflux-api/pom.xml index 899182c4e..df34c8484 100644 --- a/springdoc-openapi-starter-webflux-api/pom.xml +++ b/springdoc-openapi-starter-webflux-api/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webflux-api ${project.artifactId} diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java index 4a04368bc..641bc4b87 100644 --- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java +++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java @@ -49,6 +49,7 @@ import org.springdoc.core.service.GenericResponseService; import org.springdoc.core.service.OpenAPIService; import org.springdoc.core.service.OperationService; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springdoc.webflux.core.visitor.RouterFunctionVisitor; import reactor.core.publisher.Mono; @@ -187,13 +188,17 @@ protected void calculatePath(Map restControllers, Map requestMethods = requestMappingInfo.getMethodsCondition().getMethods(); // default allowed requestmethods if (requestMethods.isEmpty()) requestMethods = this.getDefaultAllowedHttpMethods(); - calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params, locale, openAPI); + SpringDocVersionStrategy springDocVersionStrategy = springWebProvider.getSpringDocVersionStrategy(version, params); + if(springDocVersionStrategy != null) + operationPath = springDocVersionStrategy.updateOperationPath(operationPath, version); + calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params,springDocVersionStrategy, locale, openAPI); } } } diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java index 51cf71e4b..832b1281c 100644 --- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java +++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java @@ -65,6 +65,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.web.reactive.accept.ApiVersionStrategy; import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED; @@ -143,13 +144,14 @@ GenericResponseService responseBuilder(OperationService operationService, Spring /** * Spring web provider spring web provider. * + * @param apiVersionStrategyOptional the api version strategy optional * @return the spring web provider */ @Bean @ConditionalOnMissingBean @Lazy(false) - SpringWebProvider springWebProvider() { - return new SpringWebFluxProvider(); + SpringWebProvider springWebProvider(Optional apiVersionStrategyOptional) { + return new SpringWebFluxProvider(apiVersionStrategyOptional); } /** diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java index 1acca1186..5f8e62875 100644 --- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java +++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java @@ -25,20 +25,37 @@ */ package org.springdoc.webflux.core.providers; +import java.lang.reflect.Field; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.providers.SpringWebProvider; +import org.springdoc.core.versions.HeaderVersionStrategy; +import org.springdoc.core.versions.MediaTypeVersionStrategy; +import org.springdoc.core.versions.PathVersionStrategy; +import org.springdoc.core.versions.QueryParamVersionStrategy; +import org.springdoc.core.versions.SpringDocApiVersionType; +import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.accept.ApiVersionResolver; +import org.springframework.web.reactive.accept.ApiVersionStrategy; +import org.springframework.web.reactive.accept.DefaultApiVersionStrategy; +import org.springframework.web.reactive.accept.HeaderApiVersionResolver; +import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver; +import org.springframework.web.reactive.accept.PathApiVersionResolver; +import org.springframework.web.reactive.accept.QueryApiVersionResolver; import org.springframework.web.reactive.result.condition.PatternsRequestCondition; import org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping; import org.springframework.web.reactive.result.method.RequestMappingInfo; @@ -53,6 +70,55 @@ */ public class SpringWebFluxProvider extends SpringWebProvider { + /** + * Instantiates a new Spring web flux provider. + * + * @param apiVersionStrategyOptional the api version strategy optional + */ + public SpringWebFluxProvider(Optional apiVersionStrategyOptional) { + apiVersionStrategyOptional.ifPresent(apiVersionStrategy -> { + try { + DefaultApiVersionStrategy defaultApiVersionStrategy = (DefaultApiVersionStrategy) apiVersionStrategy; + String defaultVersion = null; + if(defaultApiVersionStrategy.getDefaultVersion() !=null) + defaultVersion = defaultApiVersionStrategy.getDefaultVersion().toString(); + Field field = FieldUtils.getDeclaredField(DefaultApiVersionStrategy.class, "versionResolvers", true); + final List versionResolvers = (List) field.get(defaultApiVersionStrategy); + for (ApiVersionResolver apiVersionResolver : versionResolvers) { + if (apiVersionResolver instanceof MediaTypeParamApiVersionResolver mediaTypeParamApiVersionResolver) { + field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "compatibleMediaType", true); + MediaType mediaType = (MediaType) field.get(mediaTypeParamApiVersionResolver); + field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "parameterName", true); + String parameterName = (String) field.get(mediaTypeParamApiVersionResolver); + MediaTypeVersionStrategy mediaTypeStrategy = new MediaTypeVersionStrategy(mediaType, parameterName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.MEDIA_TYPE, mediaTypeStrategy); + } + else if (apiVersionResolver instanceof PathApiVersionResolver pathApiVersionResolver) { + field = FieldUtils.getDeclaredField(PathApiVersionResolver.class, "pathSegmentIndex", true); + Integer pathSegmentIndex = (Integer) field.get(pathApiVersionResolver); + PathVersionStrategy pathVersionStrategy = new PathVersionStrategy(pathSegmentIndex, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.PATH, pathVersionStrategy); + } + else if (apiVersionResolver instanceof HeaderApiVersionResolver headerApiVersionResolver) { + field = FieldUtils.getDeclaredField(HeaderApiVersionResolver.class, "headerName", true); + String headerName = (String) field.get(headerApiVersionResolver); + HeaderVersionStrategy headerVersionStrategy = new HeaderVersionStrategy(headerName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.HEADER, headerVersionStrategy); + } + else if (apiVersionResolver instanceof QueryApiVersionResolver queryApiVersionResolver) { + field = FieldUtils.getDeclaredField(QueryApiVersionResolver.class, "queryParamName", true); + String queryParamName = (String) field.get(queryApiVersionResolver); + QueryParamVersionStrategy queryParamVersionStrategy = new QueryParamVersionStrategy(queryParamName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.QUERY_PARAM, queryParamVersionStrategy); + } + } + } + catch (IllegalAccessException e) { + LOGGER.warn(e.getMessage()); + } + }); + } + /** * Finds path prefix. * @@ -88,7 +154,6 @@ public Set getActivePatterns(Object requestMapping) { .map(PathPattern::getPatternString).collect(Collectors.toCollection(LinkedHashSet::new)); } - /** * Gets handler methods. * diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java index d3cfb4e5f..756358115 100644 --- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java +++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java @@ -26,19 +26,24 @@ package org.springdoc.webflux.core.visitor; +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Set; import java.util.function.Function; import org.springdoc.core.fn.AbstractRouterFunctionVisitor; import reactor.core.publisher.Mono; import org.springframework.core.io.Resource; +import org.springframework.http.HttpMethod; +import org.springframework.util.ReflectionUtils; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.util.pattern.PathPattern; /** * The type Router function visitor. @@ -67,7 +72,19 @@ public void endNested(RequestPredicate predicate) { @Override public void resources(Function> lookupFunction) { - // Not yet needed + if ("PathResourceLookupFunction".equals(lookupFunction.getClass().getSimpleName())) { + Field patternField = ReflectionUtils.findField(lookupFunction.getClass(), "pattern"); + if (patternField != null) { + ReflectionUtils.makeAccessible(patternField); + Object patternFieldValue = ReflectionUtils.getField(patternField, lookupFunction); + if (patternFieldValue instanceof PathPattern patternCastedValue) { + this.currentRouterFunctionDatas = new ArrayList<>(); + this.path(patternCastedValue.getPatternString()); + this.commonRoute(); + this.method(Set.of(HttpMethod.GET)); + } + } + } } @Override diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java index 9593f2b80..db8238969 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.reactive.server.WebTestClient; diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java index de0d8b638..26b8ab2d2 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java @@ -30,7 +30,7 @@ import org.springdoc.core.utils.Constants; import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.ServerResponse; diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java index 3ca0d9afd..3f29f5d89 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java index 8367ecae5..66cd00188 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java @@ -30,7 +30,7 @@ import org.springdoc.core.utils.Constants; import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.ServerResponse; diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java new file mode 100644 index 000000000..edcafb302 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java @@ -0,0 +1,39 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v31.app193; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +public class SpringDocApp193Test extends AbstractSpringDocTest { + + @SpringBootApplication + @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app193" }) + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java new file mode 100644 index 000000000..166b61383 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app193.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java new file mode 100644 index 000000000..b0f9dea59 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java @@ -0,0 +1,23 @@ +package test.org.springdoc.api.v31.app193.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.config.ApiVersionConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +/** + * @author bnasslahsen + */ +@Configuration +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .setVersionRequired(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .useMediaTypeParameter(MediaType.APPLICATION_JSON, "version") + .setVersionParser(new ApiVersionParser()); + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java new file mode 100644 index 000000000..e28a5b320 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app193.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java new file mode 100644 index 000000000..21a7b046f --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java @@ -0,0 +1,47 @@ +package test.org.springdoc.api.v31.app193.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING MEDIA TYPE (Content Negotiation) ======================================= + + @GetMapping(value = "/users/media", version = "1.0", produces = "application/json") + public List getUsersMediaV1() { + log.info("Find All Users using media type versioning: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users/media", version = "2.0", produces = MediaType.APPLICATION_JSON_VALUE) + public List getUsersMediaV2() { + log.info("Find All Users using media type versioning: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java new file mode 100644 index 000000000..9ba026b48 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app193.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java new file mode 100644 index 000000000..ab84172f6 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app193.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java new file mode 100644 index 000000000..e1818822f --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app193.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java new file mode 100644 index 000000000..61eba9c29 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app193.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java new file mode 100644 index 000000000..9788a7cd2 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java @@ -0,0 +1,39 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v31.app194; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +public class SpringDocApp194Test extends AbstractSpringDocTest { + + @SpringBootApplication + @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app194" }) + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java new file mode 100644 index 000000000..78c38fd37 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app194.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java new file mode 100644 index 000000000..102e18416 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java @@ -0,0 +1,21 @@ +package test.org.springdoc.api.v31.app194.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.ApiVersionConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .usePathSegment(1) + .detectSupportedVersions(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .setVersionRequired(false) + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java new file mode 100644 index 000000000..1768bf7a3 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app194.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java new file mode 100644 index 000000000..21a1ab3b3 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java @@ -0,0 +1,39 @@ +package test.org.springdoc.api.v31.app194.user; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING PATH SEGMENT ====================================================== + + @GetMapping(value = "/{version}/users", version = "1.0") + public List findAllv1() { + log.info("Finding all users v1"); + return userRepository.findAll(); + } + + @GetMapping(value = "/{version}/users", version = "2.0") + public List findAllv2() { + log.info("Finding all users v2"); + return userRepository.findAll(); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java new file mode 100644 index 000000000..2a6ea9d64 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app194.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java new file mode 100644 index 000000000..e3b381f9d --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app194.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java new file mode 100644 index 000000000..a0552e4b4 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app194.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java new file mode 100644 index 000000000..45d41de23 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app194.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java new file mode 100644 index 000000000..05bbe3f44 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java @@ -0,0 +1,39 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v31.app195; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +public class SpringDocApp195Test extends AbstractSpringDocTest { + + @SpringBootApplication + @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app195" }) + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java new file mode 100644 index 000000000..179d04557 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app195.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java new file mode 100644 index 000000000..1b6154082 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java @@ -0,0 +1,21 @@ +package test.org.springdoc.api.v31.app195.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.ApiVersionConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .detectSupportedVersions(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .setVersionRequired(false) + .useRequestHeader("X-API-Version") + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java new file mode 100644 index 000000000..f10aa2e34 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app195.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java new file mode 100644 index 000000000..7fe63304a --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java @@ -0,0 +1,46 @@ +package test.org.springdoc.api.v31.app195.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING REQUEST HEADER ====================================================== + + @GetMapping(value = "/users", version = "1.0") + public List getUsersV1() { + log.info("Find All Users using request header: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users", version = "2.0") + public List getUsersV2() { + log.info("Find All Users using request header: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java new file mode 100644 index 000000000..da39f3017 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app195.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java new file mode 100644 index 000000000..e381e4c0e --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app195.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java new file mode 100644 index 000000000..bdc4a736a --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app195.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java new file mode 100644 index 000000000..690a1fcdc --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app195.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java new file mode 100644 index 000000000..9ec2ba77e --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java @@ -0,0 +1,39 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v31.app196; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +public class SpringDocApp196Test extends AbstractSpringDocTest { + + @SpringBootApplication + @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app196" }) + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java new file mode 100644 index 000000000..6a4fddce9 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app196.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java new file mode 100644 index 000000000..fd5d199d3 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java @@ -0,0 +1,20 @@ +package test.org.springdoc.api.v31.app196.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.ApiVersionConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .setVersionRequired(false) + .addSupportedVersions("1.0","v2") + .setDefaultVersion("1.0") + .useQueryParam("version") + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java new file mode 100644 index 000000000..4eff6d48d --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app196.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java new file mode 100644 index 000000000..59a5dea94 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java @@ -0,0 +1,46 @@ +package test.org.springdoc.api.v31.app196.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING REQUEST PARAMETER (Query Parameter) =================================== + + @GetMapping(value = "/users/list", params = "version=1.0") + public List listUsersV1() { + log.info("Find All Users using request header: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users/list", params = "version=v2") + public List listUsersV2() { + log.info("Find All Users using request header: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java new file mode 100644 index 000000000..0159d09da --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app196.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java new file mode 100644 index 000000000..1413f0f35 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app196.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java new file mode 100644 index 000000000..ba64a768d --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app196.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java new file mode 100644 index 000000000..ded9db774 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app196.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java new file mode 100644 index 000000000..cb80d75dd --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java @@ -0,0 +1,113 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v31.app197; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.junit.jupiter.api.Test; +import org.springdoc.core.annotations.RouterOperation; +import reactor.core.publisher.Mono; +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.function.server.EntityResponse; +import org.springframework.web.reactive.function.server.HandlerFilterFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class SpringDocApp197Test extends AbstractSpringDocTest { + + @Test + void getIconResource() { + webTestClient.get().uri("/icons/icon.svg").exchange() + .expectStatus().isOk() + .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofHours(24))) + .expectBody().consumeWith(response -> { + byte[] body = response.getResponseBody(); + assertNotNull(body); + String bodyValue = new String(body, StandardCharsets.UTF_8); + assertThat(bodyValue).contains(""); + }); + } + + @Test + void getUnknownResource() { + webTestClient.get().uri("/icons/unknown.svg").exchange() + .expectStatus().isNotFound(); + } + + @SpringBootApplication + @ComponentScan(basePackages = { "org.springdoc" }) + static class SpringDocTestApp { + + @Bean + @RouterOperation( + method = RequestMethod.GET, + operation = @Operation( + operationId = "getIcon", + summary = "Get icons resource.", + responses = @ApiResponse( + responseCode = "200", content = @Content(mediaType = "image/svg+xml"), description = "icon resource", + headers = @Header(name = HttpHeaders.CACHE_CONTROL, required = true, description = "Cache-Control for icons", schema = @Schema(type = "string")) + ) + ) + ) + public RouterFunction getIconsResourceWithCacheControl() { + return RouterFunctions + .resources("/icons/**", new ClassPathResource("icons/")) + .filter(HandlerFilterFunction.ofResponseProcessor(this::injectCacheControlHeader)); + } + + private Mono injectCacheControlHeader(ServerResponse serverResponse) { + if (serverResponse instanceof EntityResponse entityResponse) { + return EntityResponse.fromObject(entityResponse.entity()) + .header(HttpHeaders.CACHE_CONTROL, CacheControl.maxAge(24, TimeUnit.HOURS).getHeaderValue()) + .build().cast(ServerResponse.class); + } + return Mono.just(serverResponse); + } + + } + +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java new file mode 100644 index 000000000..37ace0881 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java @@ -0,0 +1,17 @@ +package test.org.springdoc.api.v31.app64; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.ApiVersionConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +/** + * @author bnasslahsen + */ +@Configuration +public class WebConfiguration implements WebFluxConfigurer { + + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.setVersionRequired(false); + configurer.useQueryParam("API-Version"); + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg b/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg new file mode 100644 index 000000000..6d210d9f2 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json index c8280b78f..acc0a55dc 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json @@ -22,6 +22,9 @@ "required": true, "schema": { "type": "string", + "enum": [ + "value" + ], "default": "value" } }, diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json index 485bf016d..d84ef9dff 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json @@ -22,6 +22,9 @@ "required": true, "schema": { "type": "string", + "enum": [ + "value" + ], "default": "value" } }, diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json new file mode 100644 index 000000000..6138f1ccf --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json @@ -0,0 +1,83 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "", + "description": "Generated server url" + } + ], + "paths": { + "/api/users/media": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "getUsersMediaV2", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;version=2.0": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + }, + "application/json;version=1.0": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json new file mode 100644 index 000000000..523f08b83 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json @@ -0,0 +1,80 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "", + "description": "Generated server url" + } + ], + "paths": { + "/api/2.0/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "findAllv2", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + }, + "/api/1.0/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "findAllv1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json new file mode 100644 index 000000000..7170ac8b6 --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json @@ -0,0 +1,99 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "", + "description": "Generated server url" + } + ], + "paths": { + "/api/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "getUsersV2", + "parameters": [ + { + "name": "X-API-Version", + "in": "header", + "schema": { + "type": "string", + "default": "1.0", + "enum": [ + "2.0", + "1.0" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json new file mode 100644 index 000000000..4f75b3c7e --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json @@ -0,0 +1,100 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "", + "description": "Generated server url" + } + ], + "paths": { + "/api/users/list": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "listUsersV2", + "parameters": [ + { + "name": "version", + "in": "query", + "required": true, + "schema": { + "type": "string", + "default": "1.0", + "enum": [ + "v2", + "1.0" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json new file mode 100644 index 000000000..c738d9efd --- /dev/null +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json @@ -0,0 +1,40 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "", + "description": "Generated server url" + } + ], + "paths": { + "/icons/**": { + "get": { + "operationId": "getIcon", + "summary": "Get icons resource.", + "responses": { + "200": { + "description": "icon resource", + "content": { + "image/svg+xml": {} + }, + "headers": { + "Cache-Control": { + "description": "Cache-Control for icons", + "required": true, + "style":"simple", + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": {} +} diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json index 48a13cd8f..9944778a3 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json @@ -22,7 +22,10 @@ "required": true, "schema": { "type": "string", - "default": "value" + "default": "value", + "enum": [ + "value" + ] } }, { diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json index 02a8ac4be..99ef2381f 100644 --- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json +++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json @@ -22,7 +22,10 @@ "required": true, "schema": { "type": "string", - "default": "value" + "default": "value", + "enum": [ + "value" + ] } }, { diff --git a/springdoc-openapi-starter-webflux-scalar/pom.xml b/springdoc-openapi-starter-webflux-scalar/pom.xml index 6760da933..c71b3ed09 100644 --- a/springdoc-openapi-starter-webflux-scalar/pom.xml +++ b/springdoc-openapi-starter-webflux-scalar/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webflux-scalar ${project.artifactId} diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java index 4f254dafe..c9e222b04 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java +++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java @@ -31,6 +31,7 @@ import com.scalar.maven.webjar.ScalarProperties; import io.swagger.v3.oas.annotations.Operation; import org.springdoc.scalar.AbstractScalarController; +import tools.jackson.databind.ObjectMapper; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; @@ -59,9 +60,10 @@ public class ScalarActuatorController extends AbstractScalarController { * * @param scalarProperties the scalar properties * @param webEndpointProperties the web endpoint properties + * @param objectMapper the object mapper */ - protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties) { - super(scalarProperties); + protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) { + super(scalarProperties, objectMapper); this.webEndpointProperties = webEndpointProperties; } diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java index acc15b13f..1dd9e3677 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java +++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java @@ -30,6 +30,7 @@ import org.springdoc.core.configuration.SpringDocConfiguration; import org.springdoc.core.events.SpringDocAppInitializer; import org.springdoc.core.properties.SpringDocConfigProperties; +import tools.jackson.databind.ObjectMapper; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; @@ -69,14 +70,15 @@ public class ScalarConfiguration { * * @param scalarProperties the scalar properties * @param springDocConfigProperties the spring doc config properties + * @param objectMapper the object mapper * @return the scalar web mvc controller */ @Bean @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @ConditionalOnMissingBean @Lazy(false) - ScalarWebFluxController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) { - return new ScalarWebFluxController(scalarProperties,springDocConfigProperties); + ScalarWebFluxController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) { + return new ScalarWebFluxController(scalarProperties,springDocConfigProperties, objectMapper); } /** @@ -102,7 +104,7 @@ ForwardedHeaderTransformer forwardedHeaderTransformer() { @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) SpringDocAppInitializer springDocScalarInitializer(ScalarProperties scalarProperties){ - return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED); + return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED, scalarProperties.isEnabled()); } /** @@ -118,13 +120,14 @@ static class SwaggerActuatorWelcomeConfiguration { * * @param properties the properties * @param webEndpointProperties the web endpoint properties + * @param objectMapper the object mapper * @return the scalar actuator controller */ @Bean @ConditionalOnMissingBean @Lazy(false) - ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties) { - return new ScalarActuatorController(properties,webEndpointProperties); + ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) { + return new ScalarActuatorController(properties,webEndpointProperties, objectMapper); } /** @@ -135,8 +138,8 @@ ScalarActuatorController scalarActuatorController(ScalarProperties properties, @Bean @ConditionalOnMissingBean(name = "springDocScalarInitializer") @Lazy(false) - SpringDocAppInitializer springDocScalarInitializer(){ - return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED); + SpringDocAppInitializer springDocScalarInitializer(ScalarProperties scalarProperties){ + return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED, scalarProperties.isEnabled()); } } diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java index 85597fb82..64656179b 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java +++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java @@ -31,6 +31,7 @@ import com.scalar.maven.webjar.ScalarProperties; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.scalar.AbstractScalarController; +import tools.jackson.databind.ObjectMapper; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -61,9 +62,10 @@ public class ScalarWebFluxController extends AbstractScalarController { * * @param scalarProperties the scalar properties * @param springDocConfigProperties the spring doc config properties + * @param objectMapper the object mapper */ - protected ScalarWebFluxController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) { - super(scalarProperties); + protected ScalarWebFluxController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) { + super(scalarProperties, objectMapper); this.springDocConfigProperties = springDocConfigProperties; } diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java index 78f050427..8b7c6a14b 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java +++ b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1 index 19e300d37..e1509f923 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app10 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app10 deleted file mode 100644 index dfbe8d55c..000000000 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app10 +++ /dev/null @@ -1,25 +0,0 @@ - - - - Scalar API Reference - - - - - -
- - - - - - - - diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11 index 6b9b9273d..e1509f923 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-1 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-1 index 9501a81ca..3c295c4ca 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-1 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-2 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-2 index b3de119ee..8eb5ffd17 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-2 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app11-2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app12 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app12 index 008c75abc..844a7e492 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app12 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app12 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app13 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app13 index 8a5017e08..f33e2d27e 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app13 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app13 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app2 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app2 index 57691e413..d20d37394 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app2 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app3 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app3 index bba87d100..8f6c4ae27 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app3 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app3 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app4 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app4 index dc17a8ec8..3b7276151 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app4 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app4 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app5 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app5 index 98d5fe303..706664496 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app5 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app5 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app6 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app6 index d2c196024..013e443c2 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app6 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app6 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app7 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app7 index fb40ef696..5cf457e6f 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app7 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app7 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app8 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app8 index 10573c433..346052d28 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app8 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app8 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app9 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app9 index 73c9ba06a..a2289eb03 100644 --- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app9 +++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app9 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webflux-ui/pom.xml b/springdoc-openapi-starter-webflux-ui/pom.xml index 4332a70cb..d5abf58aa 100644 --- a/springdoc-openapi-starter-webflux-ui/pom.xml +++ b/springdoc-openapi-starter-webflux-ui/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webflux-ui ${project.artifactId} diff --git a/springdoc-openapi-starter-webflux-ui/src/main/java/org/springdoc/webflux/ui/SwaggerConfig.java b/springdoc-openapi-starter-webflux-ui/src/main/java/org/springdoc/webflux/ui/SwaggerConfig.java index 3ef7c6169..f9f1eddb2 100644 --- a/springdoc-openapi-starter-webflux-ui/src/main/java/org/springdoc/webflux/ui/SwaggerConfig.java +++ b/springdoc-openapi-starter-webflux-ui/src/main/java/org/springdoc/webflux/ui/SwaggerConfig.java @@ -52,6 +52,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.web.reactive.accept.ApiVersionStrategy; import org.springframework.web.reactive.config.WebFluxConfigurer; import static org.springdoc.core.utils.Constants.DEFAULT_SWAGGER_UI_ACTUATOR_PATH; @@ -84,7 +85,7 @@ public class SwaggerConfig implements WebFluxConfigurer { @ConditionalOnMissingBean @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) - SwaggerWelcomeWebFlux swaggerWelcome(SwaggerUiConfigProperties swaggerUiConfig, SpringDocConfigProperties springDocConfigProperties, SpringWebProvider springWebProvider) { + SwaggerWelcomeWebFlux swaggerWelcome(SwaggerUiConfigProperties swaggerUiConfig, SpringDocConfigProperties springDocConfigProperties, @Lazy SpringWebProvider springWebProvider) { return new SwaggerWelcomeWebFlux(swaggerUiConfig, springDocConfigProperties, springWebProvider); } @@ -160,8 +161,8 @@ SwaggerIndexTransformer indexPageTransformer(SwaggerUiConfigProperties swaggerUi @Bean @ConditionalOnMissingBean @Lazy(false) - SpringWebProvider springWebProvider() { - return new SpringWebFluxProvider(); + SpringWebProvider springWebProvider(Optional apiVersionStrategyOptional) { + return new SpringWebFluxProvider(apiVersionStrategyOptional); } /** @@ -195,7 +196,7 @@ SwaggerResourceResolver swaggerResourceResolver(SwaggerUiConfigProperties swagge @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) SpringDocAppInitializer springDocSwaggerInitializer(SwaggerUiConfigProperties swaggerUiConfigProperties) { - return new SpringDocAppInitializer(swaggerUiConfigProperties.getPath(), SPRINGDOC_SWAGGER_UI_ENABLED); + return new SpringDocAppInitializer(swaggerUiConfigProperties.getPath(), SPRINGDOC_SWAGGER_UI_ENABLED, swaggerUiConfigProperties.isEnabled()); } /** @@ -232,8 +233,8 @@ SwaggerWelcomeActuator swaggerActuatorWelcome(SwaggerUiConfigProperties swaggerU @Bean @ConditionalOnMissingBean(name = "springDocSwaggerInitializer") @Lazy(false) - SpringDocAppInitializer springDocSwaggerInitializer() { - return new SpringDocAppInitializer(DEFAULT_SWAGGER_UI_ACTUATOR_PATH, SPRINGDOC_SWAGGER_UI_ENABLED); + SpringDocAppInitializer springDocSwaggerInitializer(SwaggerUiConfigProperties swaggerUiConfigProperties) { + return new SpringDocAppInitializer(DEFAULT_SWAGGER_UI_ACTUATOR_PATH, SPRINGDOC_SWAGGER_UI_ENABLED, swaggerUiConfigProperties.isEnabled()); } } } diff --git a/springdoc-openapi-starter-webflux-ui/src/test/java/test/org/springdoc/ui/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-ui/src/test/java/test/org/springdoc/ui/AbstractCommonTest.java index 9840fe38f..0c090188d 100644 --- a/springdoc-openapi-starter-webflux-ui/src/test/java/test/org/springdoc/ui/AbstractCommonTest.java +++ b/springdoc-openapi-starter-webflux-ui/src/test/java/test/org/springdoc/ui/AbstractCommonTest.java @@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.reactive.server.WebTestClient; diff --git a/springdoc-openapi-starter-webmvc-api/pom.xml b/springdoc-openapi-starter-webmvc-api/pom.xml index 3001ef55a..c3643b2e7 100644 --- a/springdoc-openapi-starter-webmvc-api/pom.xml +++ b/springdoc-openapi-starter-webmvc-api/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webmvc-api ${project.artifactId} diff --git a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/api/OpenApiResource.java b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/api/OpenApiResource.java index 6d2465ba8..20f2a5e7b 100644 --- a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/api/OpenApiResource.java +++ b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/api/OpenApiResource.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -54,6 +55,7 @@ import org.springdoc.core.service.GenericResponseService; import org.springdoc.core.service.OpenAPIService; import org.springdoc.core.service.OperationService; +import org.springdoc.core.versions.SpringDocVersionStrategy; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.ObjectFactory; @@ -73,7 +75,7 @@ * @author bnasslahsen, Azige */ public abstract class OpenApiResource extends AbstractOpenApiResource { - + /** * Instantiates a new Open api resource. * @@ -163,7 +165,7 @@ protected void getPaths(Map restControllers, Locale locale, Open AbstractOpenApiResource.addRestControllers(additionalRestClasses); map.putAll(mapDataRest); }); - + Optional actuatorProviderOptional = springDocProviders.getActuatorProvider(); if (actuatorProviderOptional.isPresent() && springDocConfigProperties.isShowActuator()) { Map actuatorMap = actuatorProviderOptional.get().getMethods(); @@ -205,8 +207,8 @@ protected void calculatePath(Map restControllers, Map methodTreeMap = new TreeMap<>(byReversedRequestMappingInfos()); methodTreeMap.putAll(map); Optional springWebProviderOptional = springDocProviders.getSpringWebProvider(); - springWebProviderOptional.ifPresent(springWebProvider -> { - for (Map.Entry entry : methodTreeMap.entrySet()) { + springWebProviderOptional.ifPresent( springWebProvider -> { + for (Entry entry : methodTreeMap.entrySet()) { RequestMappingInfo requestMappingInfo = entry.getKey(); HandlerMethod handlerMethod = entry.getValue(); Set patterns = springWebProvider.getActivePatterns(requestMappingInfo); @@ -218,13 +220,18 @@ protected void calculatePath(Map restControllers, Map requestMethods = requestMappingInfo.getMethodsCondition().getMethods(); // default allowed requestmethods if (requestMethods.isEmpty()) requestMethods = this.getDefaultAllowedHttpMethods(); - calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params, locale, openAPI); + SpringDocVersionStrategy springDocVersionStrategy = springWebProvider.getSpringDocVersionStrategy(version, params); + if(springDocVersionStrategy != null) + operationPath = springDocVersionStrategy.updateOperationPath(operationPath, version); + + calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params, springDocVersionStrategy, locale, openAPI); } } } diff --git a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.java b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.java index b05038ad6..19df09647 100644 --- a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.java +++ b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.java @@ -66,6 +66,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -138,13 +139,14 @@ RequestService requestBuilder(GenericParameterService parameterBuilder, RequestB /** * Spring web provider spring web provider. * + * @param apiVersionStrategyOptional the api version strategy optional * @return the spring web provider */ @Bean @ConditionalOnMissingBean @Lazy(false) - SpringWebProvider springWebProvider() { - return new SpringWebMvcProvider(); + SpringWebProvider springWebProvider(Optional apiVersionStrategyOptional) { + return new SpringWebMvcProvider(apiVersionStrategyOptional); } /** diff --git a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/providers/SpringWebMvcProvider.java b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/providers/SpringWebMvcProvider.java index b532a73c3..7a7fe9354 100644 --- a/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/providers/SpringWebMvcProvider.java +++ b/springdoc-openapi-starter-webmvc-api/src/main/java/org/springdoc/webmvc/core/providers/SpringWebMvcProvider.java @@ -25,18 +25,35 @@ */ package org.springdoc.webmvc.core.providers; +import java.lang.reflect.Field; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.providers.SpringWebProvider; +import org.springdoc.core.versions.HeaderVersionStrategy; +import org.springdoc.core.versions.MediaTypeVersionStrategy; +import org.springdoc.core.versions.PathVersionStrategy; +import org.springdoc.core.versions.QueryParamVersionStrategy; +import org.springdoc.core.versions.SpringDocApiVersionType; +import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; +import org.springframework.web.accept.ApiVersionResolver; +import org.springframework.web.accept.ApiVersionStrategy; +import org.springframework.web.accept.DefaultApiVersionStrategy; +import org.springframework.web.accept.HeaderApiVersionResolver; +import org.springframework.web.accept.MediaTypeParamApiVersionResolver; +import org.springframework.web.accept.PathApiVersionResolver; +import org.springframework.web.accept.QueryApiVersionResolver; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition; @@ -51,6 +68,55 @@ */ public class SpringWebMvcProvider extends SpringWebProvider { + /** + * Instantiates a new Spring web mvc provider. + * + * @param apiVersionStrategyOptional the api version strategy optional + */ + public SpringWebMvcProvider(Optional apiVersionStrategyOptional) { + apiVersionStrategyOptional.ifPresent(apiVersionStrategy -> { + try { + DefaultApiVersionStrategy defaultApiVersionStrategy = (DefaultApiVersionStrategy) apiVersionStrategy; + String defaultVersion = null; + if(defaultApiVersionStrategy.getDefaultVersion() !=null) + defaultVersion = defaultApiVersionStrategy.getDefaultVersion().toString(); + Field field = FieldUtils.getDeclaredField(DefaultApiVersionStrategy.class, "versionResolvers", true); + final List versionResolvers = (List) field.get(defaultApiVersionStrategy); + for (ApiVersionResolver apiVersionResolver : versionResolvers) { + if (apiVersionResolver instanceof MediaTypeParamApiVersionResolver mediaTypeParamApiVersionResolver) { + field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "compatibleMediaType", true); + MediaType mediaType = (MediaType) field.get(mediaTypeParamApiVersionResolver); + field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "parameterName", true); + String parameterName = (String) field.get(mediaTypeParamApiVersionResolver); + MediaTypeVersionStrategy mediaTypeStrategy = new MediaTypeVersionStrategy(mediaType, parameterName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.MEDIA_TYPE, mediaTypeStrategy); + } + else if (apiVersionResolver instanceof PathApiVersionResolver pathApiVersionResolver) { + field = FieldUtils.getDeclaredField(PathApiVersionResolver.class, "pathSegmentIndex", true); + Integer pathSegmentIndex = (Integer) field.get(pathApiVersionResolver); + PathVersionStrategy pathVersionStrategy = new PathVersionStrategy(pathSegmentIndex, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.PATH, pathVersionStrategy); + } + else if (apiVersionResolver instanceof HeaderApiVersionResolver headerApiVersionResolver) { + field = FieldUtils.getDeclaredField(HeaderApiVersionResolver.class, "headerName", true); + String headerName = (String) field.get(headerApiVersionResolver); + HeaderVersionStrategy headerVersionStrategy = new HeaderVersionStrategy(headerName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.HEADER, headerVersionStrategy); + } + else if (apiVersionResolver instanceof QueryApiVersionResolver queryApiVersionResolver) { + field = FieldUtils.getDeclaredField(QueryApiVersionResolver.class, "queryParamName", true); + String queryParamName = (String) field.get(queryApiVersionResolver); + QueryParamVersionStrategy queryParamVersionStrategy = new QueryParamVersionStrategy(queryParamName, defaultVersion); + springDocVersionStrategyMap.put(SpringDocApiVersionType.QUERY_PARAM, queryParamVersionStrategy); + } + } + } + catch (IllegalAccessException e) { + LOGGER.warn(e.getMessage()); + } + }); + } + /** * Finds path prefix. * @@ -93,7 +159,6 @@ public Set getActivePatterns(Object requestMapping) { return patterns; } - /** * Gets handler methods. * @@ -107,8 +172,9 @@ public Map getHandlerMethods() { .map(AbstractHandlerMethodMapping::getHandlerMethods) .map(Map::entrySet) .flatMap(Collection::stream) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a1, a2) -> a1, LinkedHashMap::new)); + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a1, a2) -> a1, LinkedHashMap::new)); } return this.handlerMethods; } + } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java new file mode 100644 index 000000000..2d16db3c8 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java @@ -0,0 +1,86 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 test.org.springdoc.api.v30.app244; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import org.springdoc.core.annotations.ParameterObject; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + public record Greeting(String hi, String bye) { + } + + @PostMapping(value = "v1/greet", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public void endpoint(@RequestBody @ModelAttribute Greeting greeting) { + + } + + @PostMapping(value = "v2/greet") + public void endpoint2(@ParameterObject @ModelAttribute Greeting greeting) { + + } + + @PostMapping(value = "v3/greet") + @RequestBody( + content = @Content( + mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + schema = @Schema(implementation = Greeting.class) + )) + public void endpoint3(Greeting greeting) { + + } + + @PostMapping(value = "v4/greet") + public void endpoint4( + @RequestBody(content = @Content( + mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + schema = @Schema(implementation = Greeting.class))) + @ModelAttribute Greeting greeting) { + + } + + @PostMapping(value = "v5/greet") + @Operation( + requestBody = @RequestBody(content = @Content( + mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + schema = @Schema(implementation = Greeting.class)) + )) + public void endpoint5(@ModelAttribute Greeting greeting) { + + } + +} + diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java new file mode 100644 index 000000000..2261ea5bb --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java @@ -0,0 +1,35 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2025 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 test.org.springdoc.api.v30.app244; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp244Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java new file mode 100644 index 000000000..45461c3ba --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java @@ -0,0 +1,11 @@ +package test.org.springdoc.api.v31.app248; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp248Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/ApiVersionParser.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/ApiVersionParser.java new file mode 100644 index 000000000..106f47f5b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app248.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/WebConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/WebConfig.java new file mode 100644 index 000000000..51c5f0fcc --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/config/WebConfig.java @@ -0,0 +1,23 @@ +package test.org.springdoc.api.v31.app248.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author bnasslahsen + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .setVersionRequired(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .useMediaTypeParameter(MediaType.APPLICATION_JSON, "version") + .setVersionParser(new ApiVersionParser()); + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/User.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/User.java new file mode 100644 index 000000000..f501389ed --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app248.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserController.java new file mode 100644 index 000000000..5dd2216c4 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserController.java @@ -0,0 +1,47 @@ +package test.org.springdoc.api.v31.app248.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING MEDIA TYPE (Content Negotiation) ======================================= + + @GetMapping(value = "/users/media", version = "1.0", produces = "application/json") + public List getUsersMediaV1() { + log.info("Find All Users using media type versioning: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users/media", version = "2.0", produces = MediaType.APPLICATION_JSON_VALUE) + public List getUsersMediaV2() { + log.info("Find All Users using media type versioning: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv1.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv1.java new file mode 100644 index 000000000..4095422a5 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app248.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv2.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv2.java new file mode 100644 index 000000000..3e9892a40 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app248.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserMapper.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserMapper.java new file mode 100644 index 000000000..31207a589 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app248.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserRepository.java new file mode 100644 index 000000000..b98ec5f76 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app248.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/SpringDocApp249Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/SpringDocApp249Test.java new file mode 100644 index 000000000..4bf8235c0 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/SpringDocApp249Test.java @@ -0,0 +1,11 @@ +package test.org.springdoc.api.v31.app249; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp249Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/ApiVersionParser.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/ApiVersionParser.java new file mode 100644 index 000000000..e842f664b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app249.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/WebConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/WebConfig.java new file mode 100644 index 000000000..6fa733c41 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/config/WebConfig.java @@ -0,0 +1,21 @@ +package test.org.springdoc.api.v31.app249.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .usePathSegment(1) + .detectSupportedVersions(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .setVersionRequired(false) + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/User.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/User.java new file mode 100644 index 000000000..78092199f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app249.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserController.java new file mode 100644 index 000000000..1464665e0 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserController.java @@ -0,0 +1,39 @@ +package test.org.springdoc.api.v31.app249.user; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING PATH SEGMENT ====================================================== + + @GetMapping(value = "/v{api}/users", version = "1.0") + public List findAllv1() { + log.info("Finding all users v1"); + return userRepository.findAll(); + } + + @GetMapping(value = "/{version}/users", version = "2.0") + public List findAllv2() { + log.info("Finding all users v2"); + return userRepository.findAll(); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv1.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv1.java new file mode 100644 index 000000000..75fc7eb89 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app249.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv2.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv2.java new file mode 100644 index 000000000..a1883833e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app249.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserMapper.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserMapper.java new file mode 100644 index 000000000..57258f6c5 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app249.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserRepository.java new file mode 100644 index 000000000..42f7e1a2b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app249/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app249.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/SpringDocApp250Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/SpringDocApp250Test.java new file mode 100644 index 000000000..2dc545c19 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/SpringDocApp250Test.java @@ -0,0 +1,11 @@ +package test.org.springdoc.api.v31.app250; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp250Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/ApiVersionParser.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/ApiVersionParser.java new file mode 100644 index 000000000..652cfc591 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/ApiVersionParser.java @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app250.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + // allows us to use /api/v2/users instead of /api/2.0/users + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/WebConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/WebConfig.java new file mode 100644 index 000000000..2220e2d1f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/config/WebConfig.java @@ -0,0 +1,21 @@ +package test.org.springdoc.api.v31.app250.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .detectSupportedVersions(false) + .addSupportedVersions("1.0","2.0") + .setDefaultVersion("1.0") + .setVersionRequired(false) + .useRequestHeader("X-API-Version") + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/User.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/User.java new file mode 100644 index 000000000..dd1fdec1e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app250.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserController.java new file mode 100644 index 000000000..a1c1fc72e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserController.java @@ -0,0 +1,46 @@ +package test.org.springdoc.api.v31.app250.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING REQUEST HEADER ====================================================== + + @GetMapping(value = "/users", version = "1.0") + public List getUsersV1() { + log.info("Find All Users using request header: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users", version = "2.0") + public List getUsersV2() { + log.info("Find All Users using request header: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv1.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv1.java new file mode 100644 index 000000000..32b17a53c --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app250.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv2.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv2.java new file mode 100644 index 000000000..ff4bd7b2c --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app250.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserMapper.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserMapper.java new file mode 100644 index 000000000..9ab5218ec --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app250.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserRepository.java new file mode 100644 index 000000000..64163c587 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app250/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app250.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/SpringDocApp251Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/SpringDocApp251Test.java new file mode 100644 index 000000000..bc5991f41 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/SpringDocApp251Test.java @@ -0,0 +1,11 @@ +package test.org.springdoc.api.v31.app251; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp251Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/ApiVersionParser.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/ApiVersionParser.java new file mode 100644 index 000000000..1817e4eda --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/ApiVersionParser.java @@ -0,0 +1,23 @@ +package test.org.springdoc.api.v31.app251.config; + +public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser { + + @Override + public Comparable parseVersion(String version) { + // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2) + if (version.startsWith("v") || version.startsWith("V")) { + version = version.substring(1); + } + + if("api-docs".equals(version) || "index.html".equals(version) + || "swagger-ui-bundle.js".equals(version) + || "swagger-ui.css".equals(version) + || "index.css".equals(version) + || "swagger-ui-standalone-preset.js".equals(version) + || "favicon-32x32.png".equals(version) + || "favicon-16x16.png".equals(version) + || "swagger-initializer.js".equals(version)) + return null; + return version; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/WebConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/WebConfig.java new file mode 100644 index 000000000..6e41d7902 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/config/WebConfig.java @@ -0,0 +1,20 @@ +package test.org.springdoc.api.v31.app251.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .setVersionRequired(false) + .addSupportedVersions("1.0","v2") + .setDefaultVersion("1.0") + .useQueryParam("version") + .setVersionParser(new ApiVersionParser()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/User.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/User.java new file mode 100644 index 000000000..b3d3e4495 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/User.java @@ -0,0 +1,10 @@ +package test.org.springdoc.api.v31.app251.user; + +public record User( + Integer id, + String name, + String email + + // a lot more fields here +) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserController.java new file mode 100644 index 000000000..950ef4eac --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserController.java @@ -0,0 +1,46 @@ +package test.org.springdoc.api.v31.app251.user; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserController(UserRepository userRepository, UserMapper userMapper) { + this.userRepository = userRepository; + this.userMapper = userMapper; + } + + // USING REQUEST PARAMETER (Query Parameter) =================================== + + @GetMapping(value = "/users/list", params = "version=1.0") + public List listUsersV1() { + log.info("Find All Users using request header: {}", "v1"); + return userRepository.findAll() + .stream() + .map(userMapper::toV1) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/users/list", params = "version=v2") + public List listUsersV2() { + log.info("Find All Users using request header: {}", "v2"); + return userRepository.findAll() + .stream() + .map(userMapper::toV2) + .collect(Collectors.toList()); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv1.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv1.java new file mode 100644 index 000000000..4f5f4f77b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv1.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app251.user; + +public record UserDTOv1(Integer id, String name, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv2.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv2.java new file mode 100644 index 000000000..617d96fad --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserDTOv2.java @@ -0,0 +1,4 @@ +package test.org.springdoc.api.v31.app251.user; + +public record UserDTOv2(Integer id, String firstName,String lastName, String email) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserMapper.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserMapper.java new file mode 100644 index 000000000..2e1764e75 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserMapper.java @@ -0,0 +1,83 @@ +package test.org.springdoc.api.v31.app251.user; + +import org.springframework.stereotype.Component; + +/* + * you could also use a library like MapStruct + */ +@Component +public class UserMapper { + + // User to DTOs + public UserDTOv1 toV1(User user) { + return new UserDTOv1( + user.id(), + user.name(), + user.email() + ); + } + + public UserDTOv2 toV2(User user) { + String[] nameParts = splitName(user.name()); + return new UserDTOv2( + user.id(), + nameParts[0], // firstName + nameParts[1], // lastName + user.email() + ); + } + + // DTOs to User + public User fromV1(UserDTOv1 dto) { + return new User( + dto.id(), + dto.name(), + dto.email() + ); + } + + public User fromV2(UserDTOv2 dto) { + String combinedName = combineName(dto.firstName(), dto.lastName()); + return new User( + dto.id(), + combinedName, + dto.email() + ); + } + + // Helper methods + private String[] splitName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + + String trimmed = fullName.trim(); + int lastSpaceIndex = trimmed.lastIndexOf(' '); + + if (lastSpaceIndex == -1) { + // Single word name - put it as firstName + return new String[]{trimmed, ""}; + } + + // Split at last space (handles middle names better) + // "John Smith Jr" -> "John Smith" and "Jr" + // "Mary Ann Smith" -> "Mary Ann" and "Smith" + return new String[]{ + trimmed.substring(0, lastSpaceIndex), + trimmed.substring(lastSpaceIndex + 1) + }; + } + + private String combineName(String firstName, String lastName) { + firstName = firstName != null ? firstName.trim() : ""; + lastName = lastName != null ? lastName.trim() : ""; + + if (firstName.isEmpty()) { + return lastName; + } + if (lastName.isEmpty()) { + return firstName; + } + return firstName + " " + lastName; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserRepository.java new file mode 100644 index 000000000..ba5b1675c --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app251/user/UserRepository.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app251.user; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final List users = new ArrayList<>(); + + public List findAll() { + return users; + } + + public User findById(Integer id) { + return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null); + } + + @PostConstruct + private void init() { + users.add(new User(1,"Dan Vega","danvega@gmail.com")); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/SpringDocApp252Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/SpringDocApp252Test.java new file mode 100644 index 000000000..bb4ebfce3 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/SpringDocApp252Test.java @@ -0,0 +1,11 @@ +package test.org.springdoc.api.v31.app252; + +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp252Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/AppConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/AppConfig.java new file mode 100644 index 000000000..2fcc989c9 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/AppConfig.java @@ -0,0 +1,35 @@ +package test.org.springdoc.api.v31.app252.config; + +import java.math.BigDecimal; +import java.util.Map; + +import test.org.springdoc.api.v31.app252.repository.Account; +import test.org.springdoc.api.v31.app252.repository.AccountRepository; +import test.org.springdoc.api.v31.app252.repository.Statement; +import test.org.springdoc.api.v31.app252.repository.Statement.Type; +import test.org.springdoc.api.v31.app252.repository.StatementRepository; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * @author bnasslahsen + */ +@Configuration +public class AppConfig { + + @Bean + AccountRepository accountRepository() { + return new AccountRepository(Map.of("1", new Account("1", "Checking"))); + } + + @Bean + StatementRepository statementRepository() { + MultiValueMap statements = new LinkedMultiValueMap<>(); + statements.add("1", new Statement(250, Type.CREDIT)); + statements.add("1", new Statement(110, Type.DEBIT)); + return new StatementRepository(statements); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/WebConfig.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/WebConfig.java new file mode 100644 index 000000000..ab2a797ca --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/config/WebConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .detectSupportedVersions(false) + .addSupportedVersions("1.1.0","1.2.0") + .setDefaultVersion("1.1.0") + .setVersionRequired(false) + .useRequestHeader("X-Api-Version"); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Account.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Account.java new file mode 100644 index 000000000..2d19d4ce7 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Account.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.repository; + +public record Account(String id, String name) { +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/AccountRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/AccountRepository.java new file mode 100644 index 000000000..4fd32f555 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/AccountRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.repository; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public class AccountRepository { + + private final Map accounts; + + public AccountRepository(Map accounts) { + this.accounts = new HashMap<>(accounts); + } + + public Account getAccount(String id) { + Account account = accounts.get(id); + if (account == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + return account; + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Statement.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Statement.java new file mode 100644 index 000000000..85add0c55 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/Statement.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.repository; + +public record Statement(int amount, Type type) { + + public enum Type { + DEBIT, CREDIT + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/StatementRepository.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/StatementRepository.java new file mode 100644 index 000000000..a2bf0d31b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/repository/StatementRepository.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.repository; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ResponseStatusException; + +public class StatementRepository { + + private final MultiValueMap statementsByAccount; + + public StatementRepository(MultiValueMap statements) { + this.statementsByAccount = new LinkedMultiValueMap<>(statements); + } + + public List getStatementsForAccount(String id) { + List list = statementsByAccount.get(id); + if (list == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + return list; + } + + public List getStatementsForAccountAndType(String id, Statement.Type type) { + List statements = statementsByAccount.get(id); + if (statements == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + List filteredStatements = statements.stream().filter(statement -> + statement.type() == type + ).collect(Collectors.toUnmodifiableList()); + + return filteredStatements; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/AccountController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/AccountController.java new file mode 100644 index 000000000..3fa7ea719 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/AccountController.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.web; + + +import test.org.springdoc.api.v31.app252.repository.Account; +import test.org.springdoc.api.v31.app252.repository.AccountRepository; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/accounts") +public class AccountController { + + private final AccountRepository repository; + + public AccountController(AccountRepository repository) { + this.repository = repository; + } + + @GetMapping(value = "/{id}", version = "1.0.0") + Account getAccount(@PathVariable String id) { + return repository.getAccount(id); + } + + @GetMapping(path = "/{id}", version = "1.1.0") + Account getAccount1_1(@PathVariable String id) { + return repository.getAccount(id); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/StatementController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/StatementController.java new file mode 100644 index 000000000..b00f0bb16 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app252/web/StatementController.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2025 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 test.org.springdoc.api.v31.app252.web; + +import java.util.List; + + +import test.org.springdoc.api.v31.app252.repository.Statement; +import test.org.springdoc.api.v31.app252.repository.StatementRepository; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/accounts/{id}/statements") +public class StatementController { + + private final StatementRepository repository; + + public StatementController(StatementRepository repository) { + this.repository = repository; + } + + @GetMapping + List getStatements(@PathVariable String id) { + return repository.getStatementsForAccount(id); + } + + @GetMapping(version = "1.2.0") + List getStatements1_2(@PathVariable String id, @RequestParam(required = false) Statement.Type type) { + + return repository.getStatementsForAccountAndType(id, type); + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json new file mode 100644 index 000000000..bbc136c6e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json @@ -0,0 +1,134 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "OpenAPI definition", + "version" : "v0" + }, + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/v5/greet" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "endpoint5", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + } + }, + "/v4/greet" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "endpoint4", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + } + }, + "/v3/greet" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "endpoint3", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + } + }, + "/v2/greet" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "endpoint2", + "parameters" : [ { + "name" : "hi", + "in" : "query", + "required" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "bye", + "in" : "query", + "required" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "OK" + } + } + } + }, + "/v1/greet" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "endpoint", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + } + } + }, + "components" : { + "schemas" : { + "Greeting" : { + "type" : "object", + "properties" : { + "hi" : { + "type" : "string" + }, + "bye" : { + "type" : "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json new file mode 100644 index 000000000..40b3da993 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json @@ -0,0 +1,83 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api/users/media": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "getUsersMediaV2", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;version=2.0": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + }, + "application/json;version=1.0": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app249.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app249.json new file mode 100644 index 000000000..d4178bcb7 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app249.json @@ -0,0 +1,80 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api/2.0/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "findAllv2", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + }, + "/api/v1.0/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "findAllv1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app250.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app250.json new file mode 100644 index 000000000..653ae94ea --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app250.json @@ -0,0 +1,99 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api/users": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "getUsersV2", + "parameters": [ + { + "name": "X-API-Version", + "in": "header", + "schema": { + "type": "string", + "default": "1.0", + "enum": [ + "2.0", + "1.0" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app251.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app251.json new file mode 100644 index 000000000..b60372b42 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app251.json @@ -0,0 +1,100 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api/users/list": { + "get": { + "tags": [ + "user-controller" + ], + "operationId": "listUsersV2", + "parameters": [ + { + "name": "version", + "in": "query", + "required": true, + "schema": { + "type": "string", + "default": "1.0", + "enum": [ + "v2", + "1.0" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv1" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTOv2" + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserDTOv2": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserDTOv1": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app252.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app252.json new file mode 100644 index 000000000..78e6c7300 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app252.json @@ -0,0 +1,145 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/accounts/{id}": { + "get": { + "tags": [ + "account-controller" + ], + "operationId": "getAccount1_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Api-Version", + "in": "header", + "schema": { + "type": "string", + "default": "1.1.0", + "enum": [ + "1.1.0", + "1.0.0" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + } + } + } + }, + "/accounts/{id}/statements": { + "get": { + "tags": [ + "statement-controller" + ], + "operationId": "getStatements", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Api-Version", + "in": "header", + "schema": { + "type": "string", + "default": "1.1.0", + "enum": [ + "1.2.0", + "1.1.0" + ] + } + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "DEBIT", + "CREDIT" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Statement" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "Statement": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "enum": [ + "DEBIT", + "CREDIT" + ] + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-scalar/pom.xml b/springdoc-openapi-starter-webmvc-scalar/pom.xml index 511607f8e..c70ffcf50 100644 --- a/springdoc-openapi-starter-webmvc-scalar/pom.xml +++ b/springdoc-openapi-starter-webmvc-scalar/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webmvc-scalar ${project.artifactId} diff --git a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarActuatorController.java b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarActuatorController.java index 23ae45d36..3540b515d 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarActuatorController.java +++ b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarActuatorController.java @@ -28,6 +28,8 @@ import java.io.IOException; + +import tools.jackson.databind.ObjectMapper; import com.scalar.maven.webjar.ScalarProperties; import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; @@ -59,9 +61,10 @@ public class ScalarActuatorController extends AbstractScalarController { * * @param scalarProperties the scalar properties * @param webEndpointProperties the web endpoint properties + * @param objectMapper the object mapper */ - protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties) { - super(scalarProperties); + protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) { + super(scalarProperties, objectMapper); this.webEndpointProperties = webEndpointProperties; } diff --git a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarConfiguration.java b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarConfiguration.java index dec8fcdc1..fdcb0da5d 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarConfiguration.java +++ b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarConfiguration.java @@ -30,6 +30,7 @@ import org.springdoc.core.configuration.SpringDocConfiguration; import org.springdoc.core.events.SpringDocAppInitializer; import org.springdoc.core.properties.SpringDocConfigProperties; +import tools.jackson.databind.ObjectMapper; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; @@ -70,14 +71,15 @@ public class ScalarConfiguration { * * @param scalarProperties the scalar properties * @param springDocConfigProperties the spring doc config properties + * @param objectMapper the object mapper * @return the scalar web mvc controller */ @Bean @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @ConditionalOnMissingBean @Lazy(false) - ScalarWebMvcController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) { - return new ScalarWebMvcController(scalarProperties, springDocConfigProperties); + ScalarWebMvcController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) { + return new ScalarWebMvcController(scalarProperties, springDocConfigProperties, objectMapper); } /** @@ -103,7 +105,7 @@ public FilterRegistrationBean forwardedHeaderFilter() { @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) SpringDocAppInitializer springDocScalarInitializer(ScalarProperties scalarProperties) { - return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED); + return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED, scalarProperties.isEnabled()); } /** @@ -119,13 +121,14 @@ static class SwaggerActuatorWelcomeConfiguration { * * @param properties the properties * @param webEndpointProperties the web endpoint properties + * @param objectMapper the object mapper * @return the scalar actuator controller */ @Bean @ConditionalOnMissingBean @Lazy(false) - ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties) { - return new ScalarActuatorController(properties, webEndpointProperties); + ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) { + return new ScalarActuatorController(properties, webEndpointProperties, objectMapper); } @@ -137,8 +140,8 @@ ScalarActuatorController scalarActuatorController(ScalarProperties properties, W @Bean @ConditionalOnMissingBean(name = "springDocScalarInitializer") @Lazy(false) - SpringDocAppInitializer springDocScalarInitializer() { - return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED); + SpringDocAppInitializer springDocScalarInitializer(ScalarProperties properties) { + return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED, properties.isEnabled()); } } diff --git a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarWebMvcController.java b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarWebMvcController.java index c41d6112c..9845230ff 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarWebMvcController.java +++ b/springdoc-openapi-starter-webmvc-scalar/src/main/java/org/springdoc/webmvc/scalar/ScalarWebMvcController.java @@ -28,6 +28,7 @@ import java.io.IOException; +import tools.jackson.databind.ObjectMapper; import com.scalar.maven.webjar.ScalarProperties; import jakarta.servlet.http.HttpServletRequest; import org.springdoc.core.properties.SpringDocConfigProperties; @@ -61,9 +62,10 @@ public class ScalarWebMvcController extends AbstractScalarController { * * @param scalarProperties the scalar properties * @param springDocConfigProperties the spring doc config properties + * @param objectMapper the object mapper */ - protected ScalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) { - super(scalarProperties); + protected ScalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) { + super(scalarProperties, objectMapper); this.springDocConfigProperties = springDocConfigProperties; } diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app1 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app1 index 19e300d37..e1509f923 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app1 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app10 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app10 index f5c061728..cb070c3a7 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app10 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app10 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app11 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app11 index 8b67f74a0..87c9507a2 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app11 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app11 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app12 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app12 index 143bdbe0d..336fe7d7e 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app12 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app12 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app13 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app13 index 35f039756..2c3d611c4 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app13 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app13 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app14 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app14 index 4c16125c7..8c22dbac3 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app14 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app14 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app15 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app15 index 19e300d37..e1509f923 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app15 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app15 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16 index 19e300d37..e1509f923 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-1 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-1 index 9501a81ca..3c295c4ca 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-1 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-2 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-2 index b3de119ee..8eb5ffd17 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-2 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app16-2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17 index 283140e4d..570095a1a 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-1 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-1 index 54c4dbde7..f33ad7ed9 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-1 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-2 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-2 index f57db2773..b36e67947 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-2 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app17-2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18 index 5a8152f2d..c0773c02f 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18-1 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18-1 index 58a9afb5c..f33e2d27e 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18-1 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app18-1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app19 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app19 index 5d3ff1550..d07d20501 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app19 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app19 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app2 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app2 index 0b10a2dde..d2baa0b57 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app2 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app3 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app3 index fdc75a528..31f0abec7 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app3 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app3 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app4 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app4 index f62a9805b..c6b455298 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app4 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app4 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ - + \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app5 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app5 index ae093c742..564b88889 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app5 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app5 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app6 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app6 index de88d5e88..620aee4b2 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app6 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app6 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-1 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-1 index 4966e1084..cd12f1096 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-1 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-1 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-2 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-2 index 13dfec43b..e7b136282 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-2 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app7-2 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app8 b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app8 index b84405624..31f0abec7 100644 --- a/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app8 +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/resources/results/app8 @@ -4,8 +4,8 @@ Scalar API Reference + content="width=device-width, initial-scale=1" + name="viewport" /> @@ -16,10 +16,7 @@ \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-ui/pom.xml b/springdoc-openapi-starter-webmvc-ui/pom.xml index c2a360bfc..bdca28e01 100644 --- a/springdoc-openapi-starter-webmvc-ui/pom.xml +++ b/springdoc-openapi-starter-webmvc-ui/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi - 3.0.0-RC1 + 3.0.0 springdoc-openapi-starter-webmvc-ui ${project.artifactId} diff --git a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java index 307c102c4..3a1aac8bd 100644 --- a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java +++ b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java @@ -51,6 +51,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.web.accept.ApiVersionStrategy; import static org.springdoc.core.utils.Constants.DEFAULT_SWAGGER_UI_ACTUATOR_PATH; import static org.springdoc.core.utils.Constants.SPRINGDOC_SWAGGER_UI_ENABLED; @@ -82,20 +83,21 @@ public class SwaggerConfig { @ConditionalOnMissingBean @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) - SwaggerWelcomeWebMvc swaggerWelcome(SwaggerUiConfigProperties swaggerUiConfig, SpringDocConfigProperties springDocConfigProperties, SpringWebProvider springWebProvider) { + SwaggerWelcomeWebMvc swaggerWelcome(SwaggerUiConfigProperties swaggerUiConfig, SpringDocConfigProperties springDocConfigProperties, @Lazy SpringWebProvider springWebProvider) { return new SwaggerWelcomeWebMvc(swaggerUiConfig, springDocConfigProperties, springWebProvider); } /** * Spring web provider spring web provider. * + * @param apiVersionStrategyOptional the api version strategy optional * @return the spring web provider */ @Bean @ConditionalOnMissingBean @Lazy(false) - SpringWebProvider springWebProvider() { - return new SpringWebMvcProvider(); + SpringWebProvider springWebProvider(Optional apiVersionStrategyOptional) { + return new SpringWebMvcProvider(apiVersionStrategyOptional); } /** @@ -183,7 +185,7 @@ SwaggerResourceResolver swaggerResourceResolver(SwaggerUiConfigProperties swagge @ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true) @Lazy(false) SpringDocAppInitializer springDocSwaggerInitializer(SwaggerUiConfigProperties swaggerUiConfigProperties) { - return new SpringDocAppInitializer(swaggerUiConfigProperties.getPath(), SPRINGDOC_SWAGGER_UI_ENABLED); + return new SpringDocAppInitializer(swaggerUiConfigProperties.getPath(), SPRINGDOC_SWAGGER_UI_ENABLED, swaggerUiConfigProperties.isEnabled()); } /** @@ -217,8 +219,8 @@ SwaggerWelcomeActuator swaggerActuatorWelcome(SwaggerUiConfigProperties swaggerU @Bean @ConditionalOnMissingBean(name = "springDocSwaggerInitializer") @Lazy(false) - SpringDocAppInitializer springDocSwaggerInitializer() { - return new SpringDocAppInitializer(DEFAULT_SWAGGER_UI_ACTUATOR_PATH, SPRINGDOC_SWAGGER_UI_ENABLED); + SpringDocAppInitializer springDocSwaggerInitializer(SwaggerUiConfigProperties swaggerUiConfigProperties) { + return new SpringDocAppInitializer(DEFAULT_SWAGGER_UI_ACTUATOR_PATH, SPRINGDOC_SWAGGER_UI_ENABLED, swaggerUiConfigProperties.isEnabled()); } } } diff --git a/springdoc-openapi-tests/pom.xml b/springdoc-openapi-tests/pom.xml index b932c8905..9c2bda6cd 100644 --- a/springdoc-openapi-tests/pom.xml +++ b/springdoc-openapi-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi org.springdoc - 3.0.0-RC1 + 3.0.0 pom 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml index fc71a9ca9..2029f8b63 100644 --- a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java index 6cf91d3bb..5af760463 100644 --- a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java @@ -36,7 +36,7 @@ import org.springdoc.core.utils.Constants; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.reactive.server.EntityExchangeResult; diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java index e2a6bf29e..191509402 100644 --- a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java @@ -36,7 +36,7 @@ import org.springdoc.core.utils.Constants; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.reactive.server.EntityExchangeResult; diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml index a1fbb2541..c273606c3 100644 --- a/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml index ca52ec2f9..5890a5383 100644 --- a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 springdoc-openapi-data-rest-tests diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml index 3f8fadb6f..f4fe304fb 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java index 9593f2b80..db8238969 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.reactive.server.WebTestClient; diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocFunctionTest.java b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocFunctionTest.java index 37c9e6ea4..9538ea60d 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocFunctionTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocFunctionTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; import org.springdoc.core.utils.Constants; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.cloud.function.context.test.FunctionalSpringBootTest; import org.springframework.test.web.reactive.server.EntityExchangeResult; diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java index 3ca0d9afd..3f29f5d89 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocFunctionTest.java b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocFunctionTest.java index afacd5f56..bb23c9668 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocFunctionTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocFunctionTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; import org.springdoc.core.utils.Constants; -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.cloud.function.context.test.FunctionalSpringBootTest; import org.springframework.test.web.reactive.server.EntityExchangeResult; diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml index 5f06b4673..e44d785f4 100644 --- a/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml index d5e19b4f2..38a9bbe5a 100644 --- a/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi-tests - 3.0.0-RC1 + 3.0.0 springdoc-openapi-groovy-tests ${project.artifactId} diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml index 441958465..2be8ef7ec 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 springdoc-openapi-hateoas-tests diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml index 06348194f..16cbdef54 100644 --- a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml @@ -2,7 +2,7 @@ org.springdoc springdoc-openapi-tests - 3.0.0-RC1 + 3.0.0 4.0.0 diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml index 0d2bf466c..669265037 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 springdoc-openapi-kotlin-webflux-tests diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v30/AbstractKotlinSpringDocTest.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v30/AbstractKotlinSpringDocTest.kt index ad749605a..9e0f53c0c 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v30/AbstractKotlinSpringDocTest.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v30/AbstractKotlinSpringDocTest.kt @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory import org.springdoc.core.utils.Constants import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.TestPropertySource import org.springframework.test.web.reactive.server.WebTestClient diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinSpringDocTest.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinSpringDocTest.kt index 9e596a4e5..6607d33cb 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinSpringDocTest.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinSpringDocTest.kt @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory import org.springdoc.core.utils.Constants import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest -import org.springframework.boot.webtestclient.AutoConfigureWebTestClient +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import java.nio.charset.StandardCharsets diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml index c7601ccd5..bdb813e53 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml @@ -2,7 +2,7 @@ springdoc-openapi-tests org.springdoc - 3.0.0-RC1 + 3.0.0 4.0.0 springdoc-openapi-kotlin-webmvc-tests diff --git a/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml index bdd4d4598..674c053ba 100644 --- a/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml +++ b/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml @@ -3,7 +3,7 @@ org.springdoc springdoc-openapi-tests - 3.0.0-RC1 + 3.0.0 springdoc-openapi-security-tests ${project.artifactId}