From 16bc4b3a6013fdfd479f20a589fd20a9f49c90b8 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 19 Apr 2023 17:31:25 +0200 Subject: [PATCH] GH-192 - Add default component groupings for jMolecules architecture abstractions. We now register default groupings for the architectural abstractions [0] in case they are available on the classpath but still fall back to the standard Spring Framework ones if not. In other words, if you e.g. use the jmolecules-hexagonal-architecture ones, types and packages annotated with @Port will cause the affected types to appear under a "Ports" section in the "Spring components" row in the Application Module Canvas. [0] https://github.com/xmolecules/jmolecules#available-libraries-1 --- spring-modulith-docs/pom.xml | 26 +++ .../modulith/docs/Documenter.java | 37 +++- .../modulith/docs/Groupings.java | 163 ++++++++++++++++++ src/docs/asciidoc/60-documentation.adoc | 3 +- 4 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Groupings.java 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 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 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`.