diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarDisableAutoConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarDisableAutoConfiguration.java index a8203969b1..456d0ed089 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarDisableAutoConfiguration.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarDisableAutoConfiguration.java @@ -37,6 +37,19 @@ /** * Disable Scalar WebJar AutoConfiguration. + * + *

The class stays in this module, because it is shared by both Springdoc Scalar starters, but it + * is no longer registered here. This module is a dependency of every starter, so a registration + * here made the filter apply to applications using the Swagger UI or the API-only starter as well, + * leaving them with neither Scalar controller (#3387). Each Springdoc Scalar starter now declares + * the filter in its own {@code META-INF/spring.factories}, which keeps the duplicate registration + * protection of #3084 while letting the standalone Scalar integration work everywhere it is not + * replaced. + * + *

Both Scalar auto-configurations are always excluded together, by design: Scalar's own + * {@code ScalarWebFluxAutoConfiguration} carries no {@code @ConditionalOnWebApplication} + * constraint, so excluding only the matching stack would let a reactive Scalar controller be + * registered in a servlet application, and the other way round. */ public class ScalarDisableAutoConfiguration implements AutoConfigurationImportFilter { diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/scalar/ScalarDisableAutoConfigurationTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/scalar/ScalarDisableAutoConfigurationTest.java new file mode 100644 index 0000000000..fcede766c5 --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/scalar/ScalarDisableAutoConfigurationTest.java @@ -0,0 +1,80 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2026 the original author or authors. + * * * * * * + * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * * * you may not use this file except in compliance with the License. + * * * * * * You may obtain a copy of the License at + * * * * * * + * * * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * * * + * * * * * * Unless required by applicable law or agreed to in writing, software + * * * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * * * See the License for the specific language governing permissions and + * * * * * * limitations under the License. + * * * * * + * * * * + * * * + * * + * + */ + +package org.springdoc.scalar; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * springdoc-openapi-starter-common is pulled in by every starter: nothing that disables Scalar's + * standalone autoconfiguration may be registered from here, otherwise applications using the + * Swagger UI or the API-only starter lose the Scalar integration with nothing replacing it (#3387). + * The filter itself must keep excluding both Scalar auto-configurations, since it is the guard + * against the duplicate registration of #3084 once a Springdoc Scalar starter declares it. + * + * @author awinishere + */ +class ScalarDisableAutoConfigurationTest { + + private static final String FILTER_CLASS = "org.springdoc.scalar.ScalarDisableAutoConfiguration"; + + private static final String[] SCALAR_AUTO_CONFIGURATIONS = { + "com.scalar.maven.webmvc.ScalarWebMvcAutoConfiguration", + "com.scalar.maven.webflux.ScalarWebFluxAutoConfiguration" }; + + private List registeredFilterNames() { + SpringFactoriesLoader loader = SpringFactoriesLoader + .forDefaultResourceLocation(getClass().getClassLoader()); + return loader.load(AutoConfigurationImportFilter.class) + .stream() + .map(filter -> filter.getClass().getName()) + .toList(); + } + + @Test + void noScalarAutoConfigurationImportFilterIsRegisteredFromThisModule() { + + assertThat(registeredFilterNames()).doesNotContain(FILTER_CLASS); + } + + @Test + void filterStillDisablesBothScalarAutoConfigurations() { + + boolean[] matches = new ScalarDisableAutoConfiguration() + .match(SCALAR_AUTO_CONFIGURATIONS, (AutoConfigurationMetadata) null); + + assertThat(matches).containsExactly(false, false); + } + +} diff --git a/springdoc-openapi-starter-common/src/main/resources/META-INF/spring.factories b/springdoc-openapi-starter-webflux-scalar/src/main/resources/META-INF/spring.factories similarity index 100% rename from springdoc-openapi-starter-common/src/main/resources/META-INF/spring.factories rename to springdoc-openapi-starter-webflux-scalar/src/main/resources/META-INF/spring.factories diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/ScalarStarterRegistersTheDisableFilterTest.java b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/ScalarStarterRegistersTheDisableFilterTest.java new file mode 100644 index 0000000000..b84c78120a --- /dev/null +++ b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/ScalarStarterRegistersTheDisableFilterTest.java @@ -0,0 +1,62 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2026 the original author or authors. + * * * * * * + * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * * * you may not use this file except in compliance with the License. + * * * * * * You may obtain a copy of the License at + * * * * * * + * * * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * * * + * * * * * * Unless required by applicable law or agreed to in writing, software + * * * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * * * See the License for the specific language governing permissions and + * * * * * * limitations under the License. + * * * * * + * * * * + * * * + * * + * + */ + +package test.org.springdoc.webflux.scalar; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The exclusion of Scalar's standalone auto-configuration belongs to the Scalar starters: it is + * only legitimate where a Springdoc Scalar controller replaces it (#3387). It is declared in this + * module {@code META-INF/spring.factories}, and not in springdoc-openapi-starter-common, so that + * it is loaded exactly when this starter is installed. + * + * @author awinishere + */ +class ScalarStarterRegistersTheDisableFilterTest { + + @Test + void scalarStarterDisablesTheStandaloneAutoConfiguration() { + + SpringFactoriesLoader loader = SpringFactoriesLoader + .forDefaultResourceLocation(getClass().getClassLoader()); + + List registeredFilters = loader.load(AutoConfigurationImportFilter.class) + .stream() + .map(filter -> filter.getClass().getName()) + .toList(); + + assertThat(registeredFilters).contains("org.springdoc.scalar.ScalarDisableAutoConfiguration"); + } + +} diff --git a/springdoc-openapi-starter-webmvc-scalar/src/main/resources/META-INF/spring.factories b/springdoc-openapi-starter-webmvc-scalar/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..0b1ebd1193 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-scalar/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ +org.springdoc.scalar.ScalarDisableAutoConfiguration \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/ScalarStarterRegistersTheDisableFilterTest.java b/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/ScalarStarterRegistersTheDisableFilterTest.java new file mode 100644 index 0000000000..55d848dffc --- /dev/null +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/ScalarStarterRegistersTheDisableFilterTest.java @@ -0,0 +1,62 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2026 the original author or authors. + * * * * * * + * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * * * you may not use this file except in compliance with the License. + * * * * * * You may obtain a copy of the License at + * * * * * * + * * * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * * * + * * * * * * Unless required by applicable law or agreed to in writing, software + * * * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * * * See the License for the specific language governing permissions and + * * * * * * limitations under the License. + * * * * * + * * * * + * * * + * * + * + */ + +package test.org.springdoc.webmvc.scalar; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The exclusion of Scalar's standalone autoconfiguration belongs to the Scalar starters: it is + * only legitimate where a Springdoc Scalar controller replaces it (#3387). It is declared in this + * module {@code META-INF/spring.factories}, and not in springdoc-openapi-starter-common, so that + * it is loaded exactly when this starter is installed. + * + * @author awinishere + */ +class ScalarStarterRegistersTheDisableFilterTest { + + @Test + void scalarStarterDisablesTheStandaloneAutoConfiguration() { + + SpringFactoriesLoader loader = SpringFactoriesLoader + .forDefaultResourceLocation(getClass().getClassLoader()); + + List registeredFilters = loader.load(AutoConfigurationImportFilter.class) + .stream() + .map(filter -> filter.getClass().getName()) + .toList(); + + assertThat(registeredFilters).contains("org.springdoc.scalar.ScalarDisableAutoConfiguration"); + } + +} diff --git a/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/app20/SpringDocApp20Test.java b/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/app20/SpringDocApp20Test.java new file mode 100644 index 0000000000..ba4b38336a --- /dev/null +++ b/springdoc-openapi-starter-webmvc-scalar/src/test/java/test/org/springdoc/webmvc/scalar/app20/SpringDocApp20Test.java @@ -0,0 +1,70 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2026 the original author or authors. + * * * * * * + * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * * * you may not use this file except in compliance with the License. + * * * * * * You may obtain a copy of the License at + * * * * * * + * * * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * * * + * * * * * * Unless required by applicable law or agreed to in writing, software + * * * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * * * See the License for the specific language governing permissions and + * * * * * * limitations under the License. + * * * * * + * * * * + * * * + * * + * + */ + +package test.org.springdoc.webmvc.scalar.app20; + +import com.scalar.maven.webmvc.ScalarWebMvcAutoConfiguration; +import com.scalar.maven.webmvc.ScalarWebMvcController; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The Springdoc Scalar starter and Scalar's own integration both declare a bean named + * {@code scalarWebMvcController} mapped to {@code ${scalar.path}}: the standalone one must not + * survive next to the Springdoc controller, otherwise the application fails on a duplicated bean + * definition (#3084). This is the counterpart of the standalone Scalar support restored by #3387. + * + * @author awinishere + */ +@SpringBootTest +class SpringDocApp20Test { + + @Autowired + private ApplicationContext context; + + @Test + void scalarStandaloneAutoConfigurationIsNotApplied() { + + assertThat(context.getBeanNamesForType(ScalarWebMvcAutoConfiguration.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ScalarWebMvcController.class)).isEmpty(); + } + + @Test + void springdocScalarControllerReplacesIt() { + + assertThat(context.getBean("scalarWebMvcController")) + .isInstanceOf(org.springdoc.webmvc.scalar.ScalarWebMvcController.class); + } + + @SpringBootApplication + static class SpringDocTestApp {} +}