diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java index a6fcc7f44..3f7eac09e 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java @@ -15,18 +15,10 @@ */ package org.springframework.modulith.core; -import java.util.List; -import java.util.function.Supplier; +import org.springframework.modulith.core.config.StrategyLookup; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeanUtils; -import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.support.SpringFactoriesLoader; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; +import java.util.Map; +import java.util.function.Supplier; /** * A factory for the {@link ApplicationModuleDetectionStrategy} to be used when scanning code for @@ -37,35 +29,6 @@ class ApplicationModuleDetectionStrategyLookup { private static final String DETECTION_STRATEGY_PROPERTY = "spring.modulith.detection-strategy"; - private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleDetectionStrategyLookup.class); - private static final Supplier FALLBACK_DETECTION_STRATEGY; - - static { - - FALLBACK_DETECTION_STRATEGY = () -> { - - List loadFactories = SpringFactoriesLoader.loadFactories( - ApplicationModuleDetectionStrategy.class, ApplicationModules.class.getClassLoader()); - - var size = loadFactories.size(); - - if (size == 0) { - return ApplicationModuleDetectionStrategy.directSubPackage(); - } - - if (size > 1) { - - throw new IllegalStateException( - "Multiple module detection strategies configured. Only one supported! %s".formatted(loadFactories)); - } - - LOG.warn( - "Configuring the application module detection strategy via spring.factories is deprecated! Please configure {} instead.", - DETECTION_STRATEGY_PROPERTY); - - return loadFactories.get(0); - }; - } /** * Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModule}s. Will use @@ -74,7 +37,7 @@ class ApplicationModuleDetectionStrategyLookup { *
  • Use the prepared strategies if either {@code direct-sub-packages} or {@code explicitly-annotated} is configured * for the {@code spring.modulith.detection-strategy} configuration property.
  • *
  • Interpret the configured value as class if it doesn't match the predefined values just described.
  • - *
  • Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.properties} + *
  • Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.factories} * (deprecated)
  • *
  • A final fallback on the {@code direct-sub-packages}.
  • * @@ -83,33 +46,16 @@ class ApplicationModuleDetectionStrategyLookup { */ static ApplicationModuleDetectionStrategy getStrategy() { - var environment = new StandardEnvironment(); - ConfigDataEnvironmentPostProcessor.applyTo(environment, - new DefaultResourceLoader(ApplicationModuleDetectionStrategyLookup.class.getClassLoader()), null); - - var configuredStrategy = environment.getProperty(DETECTION_STRATEGY_PROPERTY, String.class); - - // Nothing configured? Use fallback. - if (!StringUtils.hasText(configuredStrategy)) { - return FALLBACK_DETECTION_STRATEGY.get(); - } - - // Any of the prepared ones? - switch (configuredStrategy) { - case "direct-sub-packages": - return ApplicationModuleDetectionStrategy.directSubPackage(); - case "explicitly-annotated": - return ApplicationModuleDetectionStrategy.explicitlyAnnotated(); - } - - try { + Map> predefinedStrategies = Map.of( + "direct-sub-packages", ApplicationModuleDetectionStrategy::directSubPackage, + "explicitly-annotated", ApplicationModuleDetectionStrategy::explicitlyAnnotated); - // Lookup configured value as class - var strategyType = ClassUtils.forName(configuredStrategy, ApplicationModules.class.getClassLoader()); - return BeanUtils.instantiateClass(strategyType, ApplicationModuleDetectionStrategy.class); + var lookup = new StrategyLookup<>( + DETECTION_STRATEGY_PROPERTY, + ApplicationModuleDetectionStrategy.class, + predefinedStrategies, + ApplicationModuleDetectionStrategy::directSubPackage); - } catch (ClassNotFoundException | LinkageError o_O) { - throw new IllegalStateException(o_O); - } + return lookup.lookup(); } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/config/StrategyLookup.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/config/StrategyLookup.java new file mode 100644 index 000000000..4a6cff425 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/config/StrategyLookup.java @@ -0,0 +1,134 @@ +/* + * Copyright 2024-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.springframework.modulith.core.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Generic utility for looking up strategy implementations based on configuration. Supports property-based + * configuration, predefined strategies, custom class instantiation, and {@link SpringFactoriesLoader} fallback. + * + * @param the strategy type + * @since 1.4 + */ +public class StrategyLookup { + + private static final Logger LOG = LoggerFactory.getLogger(StrategyLookup.class); + + private final String propertyName; + private final Class strategyType; + private final Map> predefinedStrategies; + private final Supplier fallbackSupplier; + + /** + * Creates a new {@link StrategyLookup} instance. + * + * @param propertyName the configuration property name (e.g., "spring.modulith.detection-strategy") + * @param strategyType the strategy class + * @param predefinedStrategies map of predefined strategy names to their suppliers + * @param fallbackSupplier the fallback strategy supplier + */ + public StrategyLookup(String propertyName, Class strategyType, Map> predefinedStrategies, + Supplier fallbackSupplier) { + + this.propertyName = propertyName; + this.strategyType = strategyType; + this.predefinedStrategies = predefinedStrategies; + this.fallbackSupplier = fallbackSupplier; + } + + /** + * Looks up and returns the strategy implementation using the following algorithm: + *
      + *
    1. Use the predefined strategies if the configured property value matches one of them.
    2. + *
    3. Interpret the configured value as a class name if it doesn't match the predefined values.
    4. + *
    5. Use the {@link SpringFactoriesLoader} if no property is configured (deprecated).
    6. + *
    7. A final fallback on the provided fallback supplier.
    8. + *
    + * + * @return the strategy implementation, never {@literal null} + */ + public T lookup() { + + var environment = new StandardEnvironment(); + ConfigDataEnvironmentPostProcessor.applyTo(environment, + new DefaultResourceLoader(StrategyLookup.class.getClassLoader()), null); + + var configuredStrategy = environment.getProperty(propertyName, String.class); + + // Nothing configured? Use SpringFactoriesLoader or fallback + if (!StringUtils.hasText(configuredStrategy)) { + return lookupViaSpringFactoriesOrFallback(); + } + + // Check predefined strategies + var predefined = predefinedStrategies.get(configuredStrategy); + + if (predefined != null) { + return predefined.get(); + } + + // Try to load configured value as class + try { + + var strategyClass = ClassUtils.forName(configuredStrategy, strategyType.getClassLoader()); + return BeanUtils.instantiateClass(strategyClass, strategyType); + + } catch (ClassNotFoundException | LinkageError o_O) { + throw new IllegalStateException("Unable to load strategy class: " + configuredStrategy, o_O); + } + } + + /** + * Attempts to load strategy via {@link SpringFactoriesLoader} (deprecated), falling back to the fallback supplier + * if none found. + * + * @return the strategy implementation, never {@literal null} + */ + private T lookupViaSpringFactoriesOrFallback() { + + List loadFactories = SpringFactoriesLoader.loadFactories(strategyType, strategyType.getClassLoader()); + + var size = loadFactories.size(); + + if (size == 0) { + return fallbackSupplier.get(); + } + + if (size > 1) { + throw new IllegalStateException( + "Multiple strategies configured via spring.factories. Only one supported! %s".formatted(loadFactories)); + } + + LOG.warn( + "Configuring strategy via spring.factories is deprecated! Please configure {} instead.", + propertyName); + + return loadFactories.get(0); + } +} diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java index 0fc2195a4..43c7309d8 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java @@ -15,36 +15,27 @@ */ package org.springframework.modulith.docs; -import static java.util.stream.Collectors.*; -import static org.springframework.util.ClassUtils.*; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaModifier; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.modulith.core.ApplicationModule; -import org.springframework.modulith.core.ApplicationModuleDependency; -import org.springframework.modulith.core.ApplicationModules; -import org.springframework.modulith.core.ArchitecturallyEvidentType; +import org.springframework.modulith.core.*; import org.springframework.modulith.core.ArchitecturallyEvidentType.ReferenceMethod; -import org.springframework.modulith.core.DependencyType; -import org.springframework.modulith.core.EventType; -import org.springframework.modulith.core.FormattableType; -import org.springframework.modulith.core.Source; -import org.springframework.modulith.core.SpringBean; import org.springframework.modulith.docs.ConfigurationProperties.ModuleProperty; import org.springframework.modulith.docs.Documenter.CanvasOptions; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaModifier; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.joining; +import static org.springframework.util.ClassUtils.convertClassNameToResourcePath; /** * @author Oliver Drotbohm @@ -56,11 +47,9 @@ class Asciidoctor { private static final Pattern LINE_BREAKS = Pattern.compile("\\<\\s*br\\s*\\>"); private static final Logger LOG = LoggerFactory.getLogger(Asciidoctor.class); - private static final Optional DOC_SOURCE = getSpringModulithDocsSource(); - private final ApplicationModules modules; private final String javaDocBase; - private final Optional docSource; + private final DocumentationSource docSource; private Asciidoctor(ApplicationModules modules, String javaDocBase) { @@ -69,13 +58,15 @@ private Asciidoctor(ApplicationModules modules, String javaDocBase) { this.javaDocBase = javaDocBase; this.modules = modules; - this.docSource = DOC_SOURCE.map(it -> new CodeReplacingDocumentationSource(it, this)); + + var rawSource = DocumentationSourceLookup.getDocumentationSource(); + this.docSource = new CodeReplacingDocumentationSource(rawSource, this); } /** * Creates a new {@link Asciidoctor} instance for the given {@link ApplicationModules} and Javadoc base URI. * - * @param modules must not be {@literal null}. + * @param modules must not be {@literal null}. * @param javadocBase can be {@literal null}. * @return will never be {@literal null}. */ @@ -103,7 +94,7 @@ public String toInlineCode(String source) { var parts = source.split("#"); var type = parts[0]; - var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional. empty(); + var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.empty(); if (type.isBlank()) { return methodSignature.map(Asciidoctor::toCode).orElse(source); @@ -138,7 +129,7 @@ public String toInlineCode(SpringBean bean) { private String withDocumentation(String base, JavaClass type) { - return docSource.flatMap(it -> it.getDocumentation(type)) + return docSource.getDocumentation(type) .map(it -> base + " -- " + it) .orElse(base); } @@ -194,7 +185,7 @@ public String renderPublishedEvents(ApplicationModule module) { continue; } - var documentation = docSource.flatMap(it -> it.getDocumentation(eventType.getType())) + var documentation = docSource.getDocumentation(eventType.getType()) .map(" -- "::concat); builder.append("* ") @@ -333,7 +324,7 @@ private String renderReferenceMethod(ReferenceMethod it, int level) { var isAsync = it.isAsync() ? "(async) " : ""; var indent = "*".repeat(level + 1); - return docSource.flatMap(source -> source.getDocumentation(method)) + return docSource.getDocumentation(method) .map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync, doc)) .orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync)); } @@ -411,7 +402,7 @@ public String renderBeanReferences(ApplicationModule module) { } public String renderModuleDescription(ApplicationModule module) { - return docSource.flatMap(it -> it.getDocumentation(module.getBasePackage())).orElse(""); + return docSource.getDocumentation(module.getBasePackage()).orElse(""); } public String renderHeadline(int i, String modules) { @@ -426,16 +417,6 @@ public String renderGeneralInclude(String componentsFilename) { return "include::" + componentsFilename + "[]" + System.lineSeparator(); } - private static Optional getSpringModulithDocsSource() { - - return SpringModulithDocumentationSource.getInstance() - .map(it -> { - LOG.debug("Using Javadoc extracted by Spring Modulith in {}.", - SpringModulithDocumentationSource.getMetadataLocation()); - return it; - }); - } - private static final String wrap(String source, String chars) { return chars + source + chars; } diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSource.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSource.java index 2b1f799c4..ef61c2843 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSource.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSource.java @@ -27,7 +27,7 @@ * * @author Oliver Drotbohm */ -interface DocumentationSource { +public interface DocumentationSource { /** * Returns the documentation to be used for the given {@link JavaMethod}. diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSourceLookup.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSourceLookup.java new file mode 100644 index 000000000..10a85a097 --- /dev/null +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/DocumentationSourceLookup.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024-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.springframework.modulith.docs; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.modulith.core.config.StrategyLookup; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * A factory for the {@link DocumentationSource} to be used when generating documentation. + */ +class DocumentationSourceLookup { + + private static final String DOCUMENTATION_SOURCE_PROPERTY = "spring.modulith.documentation-source"; + private static final Logger LOG = LoggerFactory.getLogger(DocumentationSourceLookup.class); + + /** + * Returns the {@link DocumentationSource} to be used for documentation generation. Will use the following + * algorithm: + *
      + *
    1. Use the predefined strategy if {@code spring-modulith} is configured for the + * {@code spring.modulith.documentation-source} configuration property.
    2. + *
    3. Interpret the configured value as class if it doesn't match the predefined value.
    4. + *
    5. Use the {@link DocumentationSource} declared in {@code META-INF/spring.factories} (deprecated)
    6. + *
    7. A final fallback on {@link SpringModulithDocumentationSource} or {@link NoOpDocumentationSource} if the + * metadata file is not available.
    8. + *
    + * + * @return will never be {@literal null}. + */ + static DocumentationSource getDocumentationSource() { + + Map> predefinedStrategies = Map.of( + "spring-modulith", DocumentationSourceLookup::getSpringModulithDocumentationSource); + + var lookup = new StrategyLookup<>( + DOCUMENTATION_SOURCE_PROPERTY, + DocumentationSource.class, + predefinedStrategies, + DocumentationSourceLookup::getDefaultDocumentationSource); + + return lookup.lookup(); + } + + /** + * Returns the Spring Modulith documentation source, or a no-op source if metadata is not available. + * + * @return will never be {@literal null}. + */ + private static DocumentationSource getSpringModulithDocumentationSource() { + + return SpringModulithDocumentationSource.getInstance() + .map(it -> { + LOG.debug("Using Javadoc extracted by Spring Modulith in {}.", + SpringModulithDocumentationSource.getMetadataLocation()); + return it; + }) + .orElseGet(NoOpDocumentationSource::new); + } + + /** + * Returns the default documentation source (Spring Modulith or no-op). + * + * @return will never be {@literal null}. + */ + private static DocumentationSource getDefaultDocumentationSource() { + return getSpringModulithDocumentationSource(); + } +} diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/NoOpDocumentationSource.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/NoOpDocumentationSource.java new file mode 100644 index 000000000..8a290f4d6 --- /dev/null +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/NoOpDocumentationSource.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024-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.springframework.modulith.docs; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaMethod; +import org.springframework.modulith.core.JavaPackage; + +import java.util.Optional; + +/** + * A no-op {@link DocumentationSource} that returns empty {@link Optional}s for all documentation lookups. Used as a + * fallback when no documentation source is available. + */ +class NoOpDocumentationSource implements DocumentationSource { + + @Override + public Optional getDocumentation(JavaMethod method) { + return Optional.empty(); + } + + @Override + public Optional getDocumentation(JavaClass type) { + return Optional.empty(); + } + + @Override + public Optional getDocumentation(JavaPackage pkg) { + return Optional.empty(); + } +} diff --git a/spring-modulith-docs/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-modulith-docs/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 000000000..afa34126b --- /dev/null +++ b/spring-modulith-docs/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,28 @@ +{ + "properties": [ + { + "name": "spring.modulith.documentation-source", + "type": "java.lang.String", + "description": "The documentation source to use for extracting Javadoc comments." + } + ], + "hints": [ + { + "name": "spring.modulith.documentation-source", + "values": [ + { + "value": "spring-modulith", + "description": "Uses Javadoc metadata extracted by Spring Modulith APT processor." + } + ], + "providers": [ + { + "name": "class-reference", + "parameters": { + "target": "org.springframework.modulith.docs.DocumentationSource" + } + } + ] + } + ] +} diff --git a/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/AsciidoctorUnitTests.java b/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/AsciidoctorUnitTests.java index 26f2272a2..53207fbc2 100644 --- a/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/AsciidoctorUnitTests.java +++ b/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/AsciidoctorUnitTests.java @@ -48,8 +48,8 @@ void rendersLinkToMethodReference() { @Test void doesNotRenderLinkToMethodReferenceForNonPublicType() { - assertThat(asciidoctor.toInlineCode("DocumentationSource#getDocumentation(JavaMethod)")) - .isEqualTo("`o.s.m.d.DocumentationSource#getDocumentation(JavaMethod)`"); + assertThat(asciidoctor.toInlineCode("ConfigurationProperties#getModuleProperties(ApplicationModule)")) + .isEqualTo("`o.s.m.d.ConfigurationProperties#getModuleProperties(ApplicationModule)`"); } @Test diff --git a/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/DocumentationSourceLookupTests.java b/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/DocumentationSourceLookupTests.java new file mode 100644 index 000000000..d3d44c284 --- /dev/null +++ b/spring-modulith-docs/src/test/java/org/springframework/modulith/docs/DocumentationSourceLookupTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024-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.springframework.modulith.docs; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaMethod; +import org.junit.jupiter.api.Test; +import org.springframework.modulith.core.JavaPackage; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DocumentationSourceLookup}. + */ +class DocumentationSourceLookupTests { + + @Test + void usesSpringModulithSourceIfConfigured() { + + System.setProperty("spring.config.additional-location", "classpath:documentation-source/spring-modulith.properties"); + + var source = DocumentationSourceLookup.getDocumentationSource(); + + // Should return either SpringModulithDocumentationSource or NoOpDocumentationSource + // (depending on whether metadata file exists) + assertThat(source).isNotNull(); + } + + @Test + void usesCustomSourceIfConfigured() { + + System.setProperty("spring.config.additional-location", "classpath:documentation-source/custom-type.properties"); + + var source = DocumentationSourceLookup.getDocumentationSource(); + + assertThat(source).isInstanceOf(TestDocumentationSource.class); + } + + @Test + void usesDefaultSourceWhenNoConfigurationProvided() { + + // Clear any existing configuration + System.clearProperty("spring.config.additional-location"); + + var source = DocumentationSourceLookup.getDocumentationSource(); + + // Should return either SpringModulithDocumentationSource or NoOpDocumentationSource as default + assertThat(source).isNotNull(); + } + + /** + * Test implementation of {@link DocumentationSource} for testing custom type configuration. + */ + public static class TestDocumentationSource implements DocumentationSource { + + @Override + public Optional getDocumentation(JavaMethod method) { + return Optional.of("Test method documentation"); + } + + @Override + public Optional getDocumentation(JavaClass type) { + return Optional.of("Test class documentation"); + } + + @Override + public Optional getDocumentation(JavaPackage pkg) { + return Optional.of("Test package documentation"); + } + } +} diff --git a/spring-modulith-docs/src/test/resources/documentation-source/custom-type.properties b/spring-modulith-docs/src/test/resources/documentation-source/custom-type.properties new file mode 100644 index 000000000..86f22089a --- /dev/null +++ b/spring-modulith-docs/src/test/resources/documentation-source/custom-type.properties @@ -0,0 +1 @@ +spring.modulith.documentation-source=org.springframework.modulith.docs.DocumentationSourceLookupTests.TestDocumentationSource diff --git a/spring-modulith-docs/src/test/resources/documentation-source/spring-modulith.properties b/spring-modulith-docs/src/test/resources/documentation-source/spring-modulith.properties new file mode 100644 index 000000000..26ca878d9 --- /dev/null +++ b/spring-modulith-docs/src/test/resources/documentation-source/spring-modulith.properties @@ -0,0 +1 @@ +spring.modulith.documentation-source=spring-modulith