diff --git a/spring-modulith-docs/pom.xml b/spring-modulith-docs/pom.xml
index c9abd605..6d846d98 100644
--- a/spring-modulith-docs/pom.xml
+++ b/spring-modulith-docs/pom.xml
@@ -53,6 +53,32 @@
true
+
+
+
+ org.jmolecules
+ jmolecules-cqrs-architecture
+ true
+
+
+
+ org.jmolecules
+ jmolecules-hexagonal-architecture
+ true
+
+
+
+ org.jmolecules
+ jmolecules-layered-architecture
+ true
+
+
+
+ org.jmolecules
+ jmolecules-onion-architecture
+ true
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java
index f00d7c51..e8e4e10e 100644
--- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java
+++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java
@@ -22,6 +22,7 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
+import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -42,6 +43,8 @@ import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.DependencyDepth;
import org.springframework.modulith.core.DependencyType;
import org.springframework.modulith.core.SpringBean;
+import org.springframework.modulith.docs.Groupings.JMoleculesGroupings;
+import org.springframework.modulith.docs.Groupings.SpringGroupings;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -833,21 +836,39 @@ public class Documenter {
this.hideEmptyLines = hideEmptyLines;
}
+ /**
+ * Creates a default {@link CanvasOptions} instance configuring component {@link Groupings} for jMolecules (if on
+ * the classpath) and Spring Framework. Use {@link #withoutDefaultGroupings()} if you prefer to register component
+ * {@link Grouping}s yourself.
+ *
+ * @return will never be {@literal null}.
+ * @see #withoutDefaultGroupings()
+ * @see Groupings
+ */
public static CanvasOptions defaults() {
return withoutDefaultGroupings()
- .groupingBy("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()) //
- .groupingBy("Services", bean -> bean.toArchitecturallyEvidentType().isService()) //
- .groupingBy("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()) //
- .groupingBy("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()) //
- .groupingBy("Configuration properties",
- bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties());
+ .groupingBy(JMoleculesGroupings.getGroupings())
+ .groupingBy(SpringGroupings.getGroupings());
}
+ /**
+ * Creates a {@link CanvasOptions} instance that does not register any default component {@link Grouping}s.
+ *
+ * @return will never be {@literal null}.
+ * @see #defaults()
+ * @see Groupings
+ */
public static CanvasOptions withoutDefaultGroupings() {
return new CanvasOptions(new ArrayList<>(), null, null, true, true);
}
+ /**
+ * Creates a new {@link CanvasOptions} with the given {@link Grouping}s added.
+ *
+ * @param groupings must not be {@literal null}.
+ * @return will never be {@literal null}.
+ */
public CanvasOptions groupingBy(Grouping... groupings) {
var result = new ArrayList<>(groupers);
@@ -1075,6 +1096,10 @@ public class Documenter {
.and(bean -> !bean.getType().isEquivalentTo(type));
}
+ public static Predicate isAnnotatedWith(Class extends Annotation> type) {
+ return bean -> bean.getType().isAnnotatedWith(type);
+ }
+
/**
* Returns the name of the {@link Grouping}.
*
diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Groupings.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Groupings.java
new file mode 100644
index 00000000..74af4a90
--- /dev/null
+++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Groupings.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2023 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 static org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping.*;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+import org.jmolecules.architecture.cqrs.Command;
+import org.jmolecules.architecture.cqrs.CommandDispatcher;
+import org.jmolecules.architecture.cqrs.CommandHandler;
+import org.jmolecules.architecture.cqrs.QueryModel;
+import org.jmolecules.architecture.hexagonal.Adapter;
+import org.jmolecules.architecture.hexagonal.Application;
+import org.jmolecules.architecture.hexagonal.Port;
+import org.jmolecules.architecture.hexagonal.PrimaryAdapter;
+import org.jmolecules.architecture.hexagonal.PrimaryPort;
+import org.jmolecules.architecture.hexagonal.SecondaryAdapter;
+import org.jmolecules.architecture.hexagonal.SecondaryPort;
+import org.jmolecules.architecture.layered.ApplicationLayer;
+import org.jmolecules.architecture.layered.DomainLayer;
+import org.jmolecules.architecture.layered.InfrastructureLayer;
+import org.jmolecules.architecture.layered.InterfaceLayer;
+import org.jmolecules.architecture.onion.classical.ApplicationServiceRing;
+import org.jmolecules.architecture.onion.classical.DomainModelRing;
+import org.jmolecules.architecture.onion.classical.DomainServiceRing;
+import org.jmolecules.architecture.onion.simplified.ApplicationRing;
+import org.jmolecules.architecture.onion.simplified.DomainRing;
+import org.jmolecules.architecture.onion.simplified.InfrastructureRing;
+import org.springframework.modulith.core.SpringBean;
+import org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping;
+import org.springframework.util.ClassUtils;
+
+/**
+ * A collection of {@link Grouping}s.
+ *
+ * @author Oliver Drotbohm
+ */
+public class Groupings {
+
+ /**
+ * Spring Framework-related {@link Grouping}s.
+ *
+ * @author Oliver Drotbohm
+ */
+ public static class SpringGroupings {
+
+ /**
+ * Returns Spring Framework-related {@link Grouping}s.
+ *
+ * @return will never be {@literal null}.
+ */
+ public static Grouping[] getGroupings() {
+
+ return new Grouping[] {
+ of("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()),
+ of("Services", bean -> bean.toArchitecturallyEvidentType().isService()),
+ of("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()),
+ of("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()),
+ of("Configuration properties",
+ bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties())
+ };
+ }
+ }
+
+ /**
+ * jMolecules-related {@link Grouping}s.
+ *
+ * @author Oliver Drotbohm
+ */
+ public static class JMoleculesGroupings {
+
+ private static final boolean JMOLECULES_CQRS_PRESENT = ClassUtils
+ .isPresent("org.jmolecules.architecture.cqrs.Command", JMoleculesGroupings.class.getClassLoader());
+
+ private static final boolean JMOLECULES_HEXAGONAL_PRESENT = ClassUtils
+ .isPresent("org.jmolecules.architecture.hexagonal.Port", JMoleculesGroupings.class.getClassLoader());
+
+ private static final boolean JMOLECULES_LAYERS_PRESENT = ClassUtils
+ .isPresent("org.jmolecules.architecture.layered", JMoleculesGroupings.class.getClassLoader());
+
+ private static final boolean JMOLECULES_ONION_PRESENT = ClassUtils
+ .isPresent("org.jmolecules.architecture.onion.classical.ApplicationRing",
+ JMoleculesGroupings.class.getClassLoader());
+
+ public static Grouping[] getGroupings() {
+
+ var groupings = new ArrayList();
+
+ if (JMOLECULES_CQRS_PRESENT) {
+
+ groupings.add(of("Commands", packageOrTypeAnnotatedWith(Command.class)));
+ groupings.add(of("Command dispatchers", packageOrTypeAnnotatedWith(CommandDispatcher.class)));
+ groupings.add(of("Command handlers", packageOrTypeAnnotatedWith(CommandHandler.class)));
+ groupings.add(of("Query models", packageOrTypeAnnotatedWith(QueryModel.class)));
+ }
+
+ if (JMOLECULES_HEXAGONAL_PRESENT) {
+
+ groupings.add(of("Primary ports", packageOrTypeAnnotatedWith(PrimaryPort.class)));
+ groupings.add(of("Secondary ports", packageOrTypeAnnotatedWith(SecondaryPort.class)));
+ groupings.add(of("Ports", packageOrTypeAnnotatedWith(Port.class)));
+
+ groupings.add(of("Application", packageOrTypeAnnotatedWith(Application.class)));
+
+ groupings.add(of("Primary adapters", packageOrTypeAnnotatedWith(PrimaryAdapter.class)));
+ groupings.add(of("Secondary adapters", packageOrTypeAnnotatedWith(SecondaryAdapter.class)));
+ groupings.add(of("Adapters", packageOrTypeAnnotatedWith(Adapter.class)));
+ }
+
+ if (JMOLECULES_LAYERS_PRESENT) {
+
+ groupings.add(of("Application layer", packageOrTypeAnnotatedWith(ApplicationLayer.class)));
+ groupings.add(of("Domain layer", packageOrTypeAnnotatedWith(DomainLayer.class)));
+ groupings.add(of("Infrastructure layer", packageOrTypeAnnotatedWith(InfrastructureLayer.class)));
+ groupings.add(of("Interface layer", packageOrTypeAnnotatedWith(InterfaceLayer.class)));
+ }
+
+ if (JMOLECULES_ONION_PRESENT) {
+
+ groupings.add(of("Application ring", packageOrTypeAnnotatedWith(ApplicationRing.class)));
+ groupings.add(of("Domain ring", packageOrTypeAnnotatedWith(DomainRing.class)));
+ groupings.add(of("Infrastructure ring", packageOrTypeAnnotatedWith(InfrastructureRing.class)));
+
+ groupings.add(of("Application service ring", packageOrTypeAnnotatedWith(ApplicationServiceRing.class)));
+ groupings.add(of("Domain service ring", packageOrTypeAnnotatedWith(DomainServiceRing.class)));
+ groupings.add(of("Domain model ring", packageOrTypeAnnotatedWith(DomainModelRing.class)));
+ groupings.add(of("Infrastructure ring",
+ packageOrTypeAnnotatedWith(
+ org.jmolecules.architecture.onion.classical.InfrastructureRing.class)));
+ }
+
+ return groupings.toArray(Grouping[]::new);
+ }
+ }
+
+ private static Predicate packageOrTypeAnnotatedWith(Class extends Annotation> annotation) {
+
+ return bean -> {
+
+ var type = bean.getType();
+ var pkg = type.getPackage();
+
+ return type.isAnnotatedWith(annotation) || type.isMetaAnnotatedWith(annotation) //
+ || pkg.isAnnotatedWith(annotation) || pkg.isMetaAnnotatedWith(annotation);
+ };
+ }
+}
diff --git a/src/docs/asciidoc/60-documentation.adoc b/src/docs/asciidoc/60-documentation.adoc
index 02d71686..2ac2b624 100644
--- a/src/docs/asciidoc/60-documentation.adoc
+++ b/src/docs/asciidoc/60-documentation.adoc
@@ -245,7 +245,8 @@ _Others_
It consists of the following sections:
* __The application module's base package.__
-* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words beans that are located in either the API package or any <>.
+* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words, beans that are located in either the API package or any <>.
+This will detect component stereotypes defined by https://github.com/xmolecules/jmolecules/tree/main/jmolecules-architecture[jMolecules architecture abstractions], but also standard Spring stereotype annotations.
* __Exposed aggregate roots__ -- Any entities that we find repositories for or explicitly declared as aggregate via jMolecules.
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.