Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ApplicationModuleDetectionStrategyLookup {
* <li>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.</li>
* <li>Interpret the configured value as class if it doesn't match the predefined values just described.</li>
* <li>Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.properties}
* <li>Use the {@link ApplicationModuleDetectionStrategy} declared in {@code META-INF/spring.factories}
* (deprecated)</li>
* <li>A final fallback on the {@code direct-sub-packages}.</li>
* </ol>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DocumentationSource> DOC_SOURCE = getSpringModulithDocsSource();

private final ApplicationModules modules;
private final String javaDocBase;
private final Optional<DocumentationSource> docSource;
private final DocumentationSource docSource;

private Asciidoctor(ApplicationModules modules, String javaDocBase) {

Expand All @@ -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}.
*/
Expand Down Expand Up @@ -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.<String> empty();
var methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.<String>empty();

if (type.isBlank()) {
return methodSignature.map(Asciidoctor::toCode).orElse(source);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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("* ")
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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) {
Expand All @@ -426,16 +417,6 @@ public String renderGeneralInclude(String componentsFilename) {
return "include::" + componentsFilename + "[]" + System.lineSeparator();
}

private static Optional<DocumentationSource> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*
* @author Oliver Drotbohm
*/
interface DocumentationSource {
public interface DocumentationSource {

/**
* Returns the documentation to be used for the given {@link JavaMethod}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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 java.util.List;

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;

/**
* 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:
* <ol>
* <li>Use the predefined strategy if {@code spring-modulith} is configured for the
* {@code spring.modulith.documentation-source} configuration property.</li>
* <li>Interpret the configured value as class if it doesn't match the predefined value.</li>
* <li>Use the {@link DocumentationSource} declared in {@code META-INF/spring.factories} (deprecated)</li>
* <li>A final fallback on {@link SpringModulithDocumentationSource} or {@link NoOpDocumentationSource} if the
* metadata file is not available.</li>
* </ol>
*
* @return will never be {@literal null}.
*/
static DocumentationSource getDocumentationSource() {

var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment,
new DefaultResourceLoader(DocumentationSourceLookup.class.getClassLoader()), null);

var configuredSource = environment.getProperty(DOCUMENTATION_SOURCE_PROPERTY, String.class);

// Nothing configured? Use SpringFactoriesLoader or fallback
if (!StringUtils.hasText(configuredSource)) {
return lookupViaSpringFactoriesOrFallback();
}

// Check predefined strategy
if ("spring-modulith".equals(configuredSource)) {
return getSpringModulithDocumentationSource();
}

// Try to load configured value as class
try {

var sourceClass = ClassUtils.forName(configuredSource, DocumentationSource.class.getClassLoader());
return BeanUtils.instantiateClass(sourceClass, DocumentationSource.class);

} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException("Unable to load documentation source class: " + configuredSource, o_O);
}
}

/**
* Attempts to load documentation source via {@link SpringFactoriesLoader} (deprecated), falling back to the default
* source if none found.
*
* @return will never be {@literal null}.
*/
private static DocumentationSource lookupViaSpringFactoriesOrFallback() {

List<DocumentationSource> loadFactories = SpringFactoriesLoader.loadFactories(DocumentationSource.class,
DocumentationSource.class.getClassLoader());

var size = loadFactories.size();

if (size == 0) {
return getDefaultDocumentationSource();
}

if (size > 1) {
throw new IllegalStateException(
"Multiple documentation sources configured via spring.factories. Only one supported! %s"
.formatted(loadFactories));
}

LOG.warn(
"Configuring documentation source via spring.factories is deprecated! Please configure {} instead.",
DOCUMENTATION_SOURCE_PROPERTY);

return loadFactories.get(0);
}

/**
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> getDocumentation(JavaMethod method) {
return Optional.empty();
}

@Override
public Optional<String> getDocumentation(JavaClass type) {
return Optional.empty();
}

@Override
public Optional<String> getDocumentation(JavaPackage pkg) {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading