diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/AnnotationModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/AnnotationModulithMetadata.java index d1bc0dec..fa44ee5e 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/AnnotationModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/AnnotationModulithMetadata.java @@ -115,6 +115,15 @@ class AnnotationModulithMetadata implements ModulithMetadata { return Arrays.stream(annotation.sharedModules()); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.ModulithMetadata#getSharedModuleIdentifiers() + */ + @Override + public Stream getSharedModuleIdentifiers() { + return getSharedModuleNames().map(ApplicationModuleIdentifier::of); + } + /* * (non-Javadoc) * @see org.springframework.modulith.model.ModulithMetadata#getSystemName() diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index 71a34072..fd891f45 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -143,9 +143,21 @@ public class ApplicationModule implements Comparable { * Returns the logical name of the module. * * @return will never be {@literal null} or empty. + * @deprecated since 1.3, use {@link #getIdentifier()} instead. */ + @Deprecated public String getName() { - return source.getModuleName(); + return getIdentifier().toString(); + } + + /** + * Returns the logical identifier of the module. + * + * @return will never be {@literal null}. + * @since 1.3 + */ + public ApplicationModuleIdentifier getIdentifier() { + return ApplicationModuleIdentifier.of(source.getModuleName()); } /** @@ -306,7 +318,7 @@ public class ApplicationModule implements Comparable { return getType(type.getName()) .map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal())) .orElseThrow(() -> new IllegalArgumentException("Couldn't find type %s in module %s!".formatted( - FormattableType.of(type).getAbbreviatedFullName(this), getName()))); + FormattableType.of(type).getAbbreviatedFullName(this), getIdentifier()))); } /** @@ -404,11 +416,11 @@ public class ApplicationModule implements Comparable { if (modules != null) { modules.getParentOf(this).ifPresent(it -> { - builder.append("> Parent module: ").append(it.getName()).append("\n"); + builder.append("> Parent module: ").append(it.getIdentifier()).append("\n"); }); } - builder.append("> Logical name: ").append(getName()).append('\n'); + builder.append("> Logical name: ").append(getIdentifier()).append('\n'); builder.append("> Base package: ").append(basePackage.getName()).append('\n'); builder.append("> Excluded packages: "); @@ -438,8 +450,12 @@ public class ApplicationModule implements Comparable { List dependencies = getBootstrapDependencies(modules).toList(); builder.append("> Direct module dependencies: "); - builder.append(dependencies.isEmpty() ? "none" - : dependencies.stream().map(ApplicationModule::getName).collect(Collectors.joining(", "))); + builder.append(dependencies.isEmpty() + ? "none" + : dependencies.stream() + .map(ApplicationModule::getIdentifier) + .map(ApplicationModuleIdentifier::toString) + .collect(Collectors.joining(", "))); builder.append('\n'); } @@ -739,7 +755,7 @@ public class ApplicationModule implements Comparable { } private String getQualifiedName(NamedInterface namedInterface) { - return namedInterface.getQualifiedName(getName()); + return namedInterface.getQualifiedName(getIdentifier()); } private Collection doGetNestedModules(ApplicationModules modules, boolean recursive) { @@ -877,7 +893,7 @@ public class ApplicationModule implements Comparable { var target = modules.getModuleByName(targetModuleName) .orElseThrow(() -> new IllegalArgumentException( - INVALID_EXPLICIT_MODULE_DEPENDENCY.formatted(source.getName(), targetModuleName))); + INVALID_EXPLICIT_MODULE_DEPENDENCY.formatted(source.getIdentifier(), targetModuleName))); if (WILDCARD.equals(namedInterfaceName)) { return new DeclaredDependency(target, null); @@ -888,7 +904,8 @@ public class ApplicationModule implements Comparable { ? namedInterfaces.getUnnamedInterface() : namedInterfaces.getByName(namedInterfaceName) .orElseThrow(() -> new IllegalArgumentException( - INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfaceName, source.getName(), identifier))); + INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfaceName, source.getIdentifier(), + identifier))); return new DeclaredDependency(target, namedInterface); } @@ -943,7 +960,7 @@ public class ApplicationModule implements Comparable { @Override public String toString() { - var result = target.getName(); + var result = target.getIdentifier().toString(); if (namedInterface == null) { return result + " :: " + WILDCARD; @@ -1230,14 +1247,14 @@ public class ApplicationModule implements Comparable { .toList(); var targetString = targetNamedInterfaces.isEmpty() - ? "module '%s'".formatted(targetModule.getName()) + ? "module '%s'".formatted(targetModule.getIdentifier()) : "named interface(s) '%s'".formatted( targetNamedInterfaces.stream() .map(targetModule::getQualifiedName) .collect(Collectors.joining(", "))); var message = "Module '%s' depends on %s via %s -> %s. Allowed targets: %s." // - .formatted(originModule.getName(), targetString, source.getName(), target.getName(), + .formatted(originModule.getIdentifier(), targetString, source.getName(), target.getName(), declaredDependencies.toString()); return violations.and(new Violation(message)); @@ -1256,7 +1273,7 @@ public class ApplicationModule implements Comparable { if (!targetModule.isExposed(target)) { var violationText = INTERNAL_REFERENCE - .formatted(originModule.getName(), target.getName(), targetModule.getName()); + .formatted(originModule.getIdentifier(), target.getName(), targetModule.getIdentifier()); return violations.and(new Violation(violationText + lineSeparator() + description)); } @@ -1266,7 +1283,7 @@ public class ApplicationModule implements Comparable { if (!haveSameParentOrDirectParentRelationship(originModule, targetModule, modules)) { var violationText = INVALID_SUB_MODULE_REFERENCE - .formatted(originModule.getName(), targetModule.getName(), + .formatted(originModule.getIdentifier(), targetModule.getIdentifier(), FormattableType.of(source).getAbbreviatedFullName(originModule), FormattableType.of(target).getAbbreviatedFullName(targetModule)); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java index 46a1a554..06251ac9 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDependencies.java @@ -85,7 +85,8 @@ public class ApplicationModuleDependencies { Assert.hasText(name, "Module name must not be null or empty!"); return modules.stream() - .map(ApplicationModule::getName) + .map(ApplicationModule::getIdentifier) + .map(Object::toString) .anyMatch(name::equals); } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifier.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifier.java new file mode 100644 index 00000000..670dbbb5 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifier.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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; + +import java.util.Objects; + +import org.springframework.util.Assert; + +/** + * An identifier of an {@link ApplicationModule}. + * + * @author Oliver Drotbohm + * @since 1.3 + */ +public class ApplicationModuleIdentifier implements Comparable { + + private final String identifier; + + /** + * Creates a new {@link ApplicationModuleIdentifier} for the given {@link String}. + * + * @param identifier must not be {@literal null} . + */ + private ApplicationModuleIdentifier(String identifier) { + + Assert.hasText(identifier, "Identitifier must not be null or empty!"); + Assert.isTrue(!identifier.contains("::"), "Identifier must not contain a double-colon!"); + + this.identifier = identifier; + } + + /** + * Returns the {@link ApplicationModuleIdentifier} for the given source {@link String}. + * + * @param identifier must not be {@literal null}, empty or contain a double colon ({@code ::}). + * @return will never be {@literal null}. + */ + public static ApplicationModuleIdentifier of(String identifier) { + return new ApplicationModuleIdentifier(identifier); + } + + /* + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(ApplicationModuleIdentifier o) { + return identifier.compareTo(o.identifier); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return identifier; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (!(obj instanceof ApplicationModuleIdentifier that)) { + return false; + } + + return Objects.equals(this.identifier, that.identifier); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(identifier); + } +} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java index 2871f943..e71db887 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java @@ -81,12 +81,12 @@ public class ApplicationModules implements Iterable { } private final ModulithMetadata metadata; - private final Map modules; + private final Map modules; private final JavaClasses allClasses; private final List rootPackages; private final Supplier> rootModules; private final Set sharedModules; - private final List orderedNames; + private final List orderedNames; private boolean verified; @@ -155,7 +155,7 @@ public class ApplicationModules implements Iterable { JavaPackages.onlySubPackagesOf(it.getModuleBasePackage(), sources.stream().map(ApplicationModuleSource::getModuleBasePackage).toList())); // }) - .collect(toMap(ApplicationModule::getName, Function.identity())); + .collect(toMap(ApplicationModule::getIdentifier, Function.identity())); this.rootPackages = Stream.concat(packages.stream(), contributions.getRootPackages()) // .distinct() @@ -168,8 +168,8 @@ public class ApplicationModules implements Iterable { this.sharedModules = Collections.emptySet(); - Supplier> fallback = () -> modules.values().stream() - .map(ApplicationModule::getName) + Supplier> fallback = () -> modules.values().stream() + .map(ApplicationModule::getIdentifier) .sorted() .toList(); @@ -188,12 +188,12 @@ public class ApplicationModules implements Iterable { * @param rootPackages must not be {@literal null}. * @param rootModules must not be {@literal null}. * @param sharedModules must not be {@literal null}. - * @param orderedNames must not be {@literal null}. + * @param orderedIdentifiers must not be {@literal null}. * @param verified */ - private ApplicationModules(ModulithMetadata metadata, Map modules, JavaClasses allClasses, - List rootPackages, Supplier> rootModules, - Set sharedModules, List orderedNames, boolean verified) { + private ApplicationModules(ModulithMetadata metadata, Map modules, + JavaClasses allClasses, List rootPackages, Supplier> rootModules, + Set sharedModules, List orderedIdentifiers, boolean verified) { Assert.notNull(metadata, "ModulithMetadata must not be null!"); Assert.notNull(modules, "Application modules must not be null!"); @@ -201,7 +201,7 @@ public class ApplicationModules implements Iterable { Assert.notNull(rootPackages, "Root JavaPackages must not be null!"); Assert.notNull(rootModules, "Root modules must not be null!"); Assert.notNull(sharedModules, "Shared ApplicationModules must not be null!"); - Assert.notNull(orderedNames, "Ordered application module names must not be null!"); + Assert.notNull(orderedIdentifiers, "Ordered application module identifiers must not be null!"); this.metadata = metadata; this.modules = modules; @@ -209,7 +209,7 @@ public class ApplicationModules implements Iterable { this.rootPackages = rootPackages; this.rootModules = rootModules; this.sharedModules = sharedModules; - this.orderedNames = orderedNames; + this.orderedNames = orderedIdentifiers; this.verified = verified; } @@ -370,7 +370,7 @@ public class ApplicationModules implements Iterable { Assert.hasText(name, "Module name must not be null or empty!"); - return Optional.ofNullable(modules.get(name)); + return Optional.ofNullable(modules.get(ApplicationModuleIdentifier.of(name))); } /** @@ -601,7 +601,7 @@ public class ApplicationModules implements Iterable { .map(it -> Class.class.isInstance(it) ? Class.class.cast(it) : it.getClass()) .map(Class::getName) .flatMap(this::getModuleByType) - .map(ApplicationModule::getName) + .map(ApplicationModule::getIdentifier) .map(orderedNames::indexOf) .orElse(null); } @@ -612,12 +612,12 @@ public class ApplicationModules implements Iterable { * @param moduleName must not be {@literal null}. * @return */ - private ApplicationModule getRequiredModule(String moduleName) { + private ApplicationModule getRequiredModule(ApplicationModuleIdentifier identifier) { - var module = modules.get(moduleName); + var module = modules.get(identifier); if (module == null) { - throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName)); + throw new IllegalArgumentException(String.format("Module %s does not exist!", identifier)); } return module; @@ -648,7 +648,7 @@ public class ApplicationModules implements Iterable { var metadata = key.getMetadata(); var modules = new ApplicationModules(metadata, key.getIgnored(), key.getOptions()); - var sharedModules = metadata.getSharedModuleNames() // + var sharedModules = metadata.getSharedModuleIdentifiers() // .map(modules::getRequiredModule) // .collect(Collectors.toSet()); @@ -764,7 +764,7 @@ public class ApplicationModules implements Iterable { private static class TopologicalSorter { @Nullable - private static List topologicallySortModules(ApplicationModules modules) { + private static List topologicallySortModules(ApplicationModules modules) { Graph graph = new DefaultDirectedGraph<>(DefaultEdge.class); @@ -782,12 +782,12 @@ public class ApplicationModules implements Iterable { }); }); - var names = new ArrayList(); + var names = new ArrayList(); var iterator = new TopologicalOrderIterator<>(graph); try { - iterator.forEachRemaining(it -> names.add(0, it.getName())); + iterator.forEachRemaining(it -> names.add(0, it.getIdentifier())); return names; } catch (IllegalArgumentException o_O) { @@ -807,7 +807,8 @@ public class ApplicationModules implements Iterable { return getModuleByType(javaClass) .filter(Predicate.not(ApplicationModule::isOpen)) - .map(ApplicationModule::getName) + .map(ApplicationModule::getIdentifier) + .map(ApplicationModuleIdentifier::toString) .map(SliceIdentifier::of) .orElse(SliceIdentifier.ignore()); } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ModulithMetadata.java index a0a1bffd..6bd3aa1e 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ModulithMetadata.java @@ -103,9 +103,20 @@ public interface ModulithMetadata { * Returns the name of shared modules, i.e. modules that are supposed to always be included in bootstraps. * * @return will never be {@literal null}. + * @deprecated since 1.3, use {@link #getSharedModuleIdentifiers()} instead. */ + @Deprecated Stream getSharedModuleNames(); + /** + * Returns the {@link ApplicationModuleIdentifier}s of shared modules, i.e. modules that are supposed to always be + * included in bootstraps. + * + * @return will never be {@literal null}. + * @since 1.3 + */ + Stream getSharedModuleIdentifiers(); + /** * Returns the name of the system. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java index 8e7c44d2..20f912f9 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java @@ -133,7 +133,7 @@ public class NamedInterface implements Iterable { return name; } - String getQualifiedName(String qualifier) { + String getQualifiedName(ApplicationModuleIdentifier qualifier) { return qualifier + " :: " + name; } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/SpringBootModulithMetadata.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/SpringBootModulithMetadata.java index be365289..5a86dca7 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/SpringBootModulithMetadata.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/SpringBootModulithMetadata.java @@ -132,6 +132,15 @@ class SpringBootModulithMetadata implements ModulithMetadata { return Stream.empty(); } + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.ModulithMetadata#getSharedModuleIdentifiers() + */ + @Override + public Stream getSharedModuleIdentifiers() { + return Stream.empty(); + } + /* * (non-Javadoc) * @see org.springframework.modulith.model.ModulithMetadata#getSystemName() diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java index 3a389bac..624a73db 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModuleDependency; +import org.springframework.modulith.core.ApplicationModuleIdentifier; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.DependencyType; import org.springframework.modulith.core.NamedInterface; @@ -119,11 +120,11 @@ public class ApplicationModulesExporter { return Json.toString(toMap(Details.FULL)); } - private Map toMap(Details details) { + private Map toMap(Details details) { return modules.stream() .collect( - Collectors.toMap(ApplicationModule::getName, it -> toInfo(it, modules, details), (l, r) -> r, + Collectors.toMap(ApplicationModule::getIdentifier, it -> toInfo(it, modules, details), (l, r) -> r, LinkedHashMap::new)); } @@ -157,7 +158,7 @@ public class ApplicationModulesExporter { private static Map toInfo(Entry> types) { return Map.of( // - "target", types.getKey().getName(), // + "target", types.getKey().getIdentifier().toString(), // "types", types.getValue() // ); } diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java index 195762df..96a6d681 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java @@ -32,7 +32,8 @@ class ApplicationModulesUnitTests { void discoversComplexModuleArrangement() { assertThat(modules) - .extracting(ApplicationModule::getName) + .extracting(ApplicationModule::getIdentifier) + .extracting(Object::toString) .containsExactlyInAnyOrder( "invalid", "jmolecules", @@ -55,11 +56,13 @@ class ApplicationModulesUnitTests { assertThat(ni.getParentModule(modules)).isEmpty(); assertThat(ni.getDirectlyNestedModules(modules)) - .extracting(ApplicationModule::getName) + .extracting(ApplicationModule::getIdentifier) + .extracting(Object::toString) .containsExactlyInAnyOrder("ni.nested"); assertThat(ni.getNestedModules(modules)) - .extracting(ApplicationModule::getName) + .extracting(ApplicationModule::getIdentifier) + .extracting(Object::toString) .containsExactlyInAnyOrder("ni.nested", "ni.nested.b.first", "ni.nested.b.second"); }