Skip to content

Commit f38c642

Browse files
committed
GH-846 - Fix module identifier lookup for jMolecules @module(id = …).
We now properly translate jMolecules' @module declaration configuring the id attribute explicitly into the identifier used for an application module. Introduce @ApplicationModule(id = …) for Spring Modulith-native identifier customization.
1 parent 38721f9 commit f38c642

File tree

8 files changed

+232
-35
lines changed

8 files changed

+232
-35
lines changed

spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232

3333
public static final String OPEN_TOKEN = \\_(ツ)_/¯";
3434

35+
/**
36+
* The identifier of the module. Must not contain a double colon ({@code ::}).
37+
*
38+
* @return will never be {@literal null}.
39+
*/
40+
String id() default "";
41+
3542
/**
3643
* The human readable name of the module to be used for display and documentation purposes.
3744
*

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleSource.java

Lines changed: 139 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
*/
1616
package org.springframework.modulith.core;
1717

18+
import java.lang.annotation.Annotation;
1819
import java.util.Objects;
20+
import java.util.Optional;
21+
import java.util.function.Function;
1922
import java.util.stream.Stream;
2023

24+
import org.springframework.modulith.ApplicationModule;
25+
import org.springframework.modulith.core.Types.JMoleculesTypes;
2126
import org.springframework.util.Assert;
27+
import org.springframework.util.StringUtils;
2228

2329
/**
2430
* The source of an {@link ApplicationModule}. Essentially a {@link JavaPackage} and associated naming strategy for the
@@ -32,70 +38,82 @@
3238
*/
3339
public class ApplicationModuleSource {
3440

41+
private static final ApplicationModuleSourceMetadata ANNOTATION_IDENTIFIER_SOURCE = ApplicationModuleSourceMetadata
42+
.delegating(
43+
JMoleculesTypes.getIdentifierSource(),
44+
ApplicationModuleSourceMetadata.forAnnotation(ApplicationModule.class, ApplicationModule::id));
45+
3546
private final JavaPackage moduleBasePackage;
36-
private final String moduleName;
47+
private final ApplicationModuleIdentifier identifier;
3748

3849
/**
3950
* Creates a new {@link ApplicationModuleSource} for the given module base package and module name.
4051
*
4152
* @param moduleBasePackage must not be {@literal null}.
4253
* @param moduleName must not be {@literal null} or empty.
4354
*/
44-
private ApplicationModuleSource(JavaPackage moduleBasePackage, String moduleName) {
55+
private ApplicationModuleSource(JavaPackage moduleBasePackage, ApplicationModuleIdentifier identifier) {
4556

4657
Assert.notNull(moduleBasePackage, "JavaPackage must not be null!");
47-
Assert.hasText(moduleName, "Module name must not be null or empty!");
4858

4959
this.moduleBasePackage = moduleBasePackage;
50-
this.moduleName = moduleName;
60+
this.identifier = identifier;
5161
}
5262

5363
/**
5464
* Returns a {@link Stream} of {@link ApplicationModuleSource}s by applying the given
5565
* {@link ApplicationModuleDetectionStrategy} to the given base package.
5666
*
57-
* @param pkg must not be {@literal null}.
67+
* @param rootPackage must not be {@literal null}.
5868
* @param strategy must not be {@literal null}.
5969
* @param fullyQualifiedModuleNames whether to use fully qualified module names.
6070
* @return will never be {@literal null}.
6171
*/
62-
public static Stream<ApplicationModuleSource> from(JavaPackage pkg, ApplicationModuleDetectionStrategy strategy,
63-
boolean fullyQualifiedModuleNames) {
72+
public static Stream<ApplicationModuleSource> from(JavaPackage rootPackage,
73+
ApplicationModuleDetectionStrategy strategy, boolean fullyQualifiedModuleNames) {
6474

65-
Assert.notNull(pkg, "Base package must not be null!");
75+
Assert.notNull(rootPackage, "Root package must not be null!");
6676
Assert.notNull(strategy, "ApplicationModuleDetectionStrategy must not be null!");
6777

68-
return strategy.getModuleBasePackages(pkg)
69-
.flatMap(it -> it.andSubPackagesAnnotatedWith(org.springframework.modulith.ApplicationModule.class))
70-
.map(it -> new ApplicationModuleSource(it, fullyQualifiedModuleNames ? it.getName() : pkg.getTrailingName(it)));
78+
return strategy.getModuleBasePackages(rootPackage)
79+
.flatMap(ANNOTATION_IDENTIFIER_SOURCE::withNestedPackages)
80+
.map(it -> {
81+
82+
var id = ANNOTATION_IDENTIFIER_SOURCE.lookupIdentifier(it)
83+
.orElseGet(() -> ApplicationModuleIdentifier.of(
84+
fullyQualifiedModuleNames ? it.getName() : rootPackage.getTrailingName(it)));
85+
86+
return new ApplicationModuleSource(it, id);
87+
});
7188
}
7289

7390
/**
7491
* Creates a new {@link ApplicationModuleSource} for the given {@link JavaPackage} and name.
7592
*
7693
* @param pkg must not be {@literal null}.
77-
* @param name must not be {@literal null} or empty.
94+
* @param identifier must not be {@literal null} or empty.
7895
* @return will never be {@literal null}.
7996
*/
80-
public static ApplicationModuleSource from(JavaPackage pkg, String name) {
81-
82-
Assert.hasText(name, "Name must not be null or empty!");
83-
84-
return new ApplicationModuleSource(pkg, name);
97+
static ApplicationModuleSource from(JavaPackage pkg, String identifier) {
98+
return new ApplicationModuleSource(pkg, ApplicationModuleIdentifier.of(identifier));
8599
}
86100

87101
/**
102+
* Returns the base package for the module.
103+
*
88104
* @return will never be {@literal null}.
89105
*/
90106
public JavaPackage getModuleBasePackage() {
91107
return moduleBasePackage;
92108
}
93109

94110
/**
95-
* @return will never be {@literal null} or empty.
111+
* Returns the {@link ApplicationModuleIdentifier} to be used for the module.
112+
*
113+
* @return will never be {@literal null}.
96114
*/
97-
public String getModuleName() {
98-
return moduleName;
115+
public ApplicationModuleIdentifier getIdentifier() {
116+
return identifier;
99117
}
100118

101119
/*
@@ -113,7 +131,7 @@ public boolean equals(Object obj) {
113131
return false;
114132
}
115133

116-
return Objects.equals(this.moduleName, that.moduleName)
134+
return Objects.equals(this.identifier, that.identifier)
117135
&& Objects.equals(this.moduleBasePackage, that.moduleBasePackage);
118136
}
119137

@@ -123,6 +141,105 @@ public boolean equals(Object obj) {
123141
*/
124142
@Override
125143
public int hashCode() {
126-
return Objects.hash(moduleName, moduleBasePackage);
144+
return Objects.hash(identifier, moduleBasePackage);
145+
}
146+
147+
/*
148+
* (non-Javadoc)
149+
* @see java.lang.Object#toString()
150+
*/
151+
@Override
152+
public String toString() {
153+
return "ApplicationModuleSource(" + identifier + ", " + moduleBasePackage.getName() + ")";
154+
}
155+
156+
/**
157+
* An intermediate abstraction to detect both the {@link ApplicationModuleIdentifier} and potentially nested module
158+
* declarations for the {@link JavaPackage}s returned from the first pass of module detection.
159+
*
160+
* @author Oliver Drotbohm
161+
* @see ApplicationModuleDetectionStrategy
162+
*/
163+
interface ApplicationModuleSourceMetadata {
164+
165+
/**
166+
* Returns an optional {@link ApplicationModuleIdentifier} obtained by the annotation on the given package.
167+
*
168+
* @param pkg must not be {@literal null}.
169+
* @return will never be {@literal null}.
170+
*/
171+
Optional<ApplicationModuleIdentifier> lookupIdentifier(JavaPackage pkg);
172+
173+
/**
174+
* Return a {@link Stream} of {@link JavaPackage}s that are
175+
*
176+
* @param pkg must not be {@literal null}.
177+
* @return will never be {@literal null}.
178+
*/
179+
Stream<JavaPackage> withNestedPackages(JavaPackage pkg);
180+
181+
/**
182+
* Creates a new {@link ApplicationModuleSourceFactory} detecting the {@link ApplicationModuleIdentifier} based on a
183+
* particular annotation's attribute. It also detects nested {@link JavaPackage}s annotated with the given
184+
* annotation as nested module base packages.
185+
*
186+
* @param <T> an annotation type
187+
* @param annotation must not be {@literal null}.
188+
* @param extractor must not be {@literal null}.
189+
* @return will never be {@literal null}.
190+
*/
191+
static <T extends Annotation> ApplicationModuleSourceMetadata forAnnotation(Class<T> annotation,
192+
Function<T, String> extractor) {
193+
194+
Assert.notNull(annotation, "Annotation type must not be null!");
195+
Assert.notNull(extractor, "Attribute extractor must not be null!");
196+
197+
return new ApplicationModuleSourceMetadata() {
198+
199+
@Override
200+
public Optional<ApplicationModuleIdentifier> lookupIdentifier(JavaPackage pkg) {
201+
202+
return pkg.getAnnotation(annotation)
203+
.map(extractor)
204+
.filter(StringUtils::hasText)
205+
.map(ApplicationModuleIdentifier::of);
206+
}
207+
208+
@Override
209+
public Stream<JavaPackage> withNestedPackages(JavaPackage pkg) {
210+
return pkg.getSubPackagesAnnotatedWith(annotation);
211+
}
212+
};
213+
}
214+
215+
/**
216+
* Returns an {@link ApplicationModuleSourceFactory} delegating to the given ones, chosing the first identifier
217+
* found and assembling nested packages of all delegate {@link ApplicationModuleSourceFactory} instances.
218+
*
219+
* @param delegates must not be {@literal null}.
220+
* @return will never be {@literal null}.
221+
*/
222+
private static ApplicationModuleSourceMetadata delegating(ApplicationModuleSourceMetadata... delegates) {
223+
224+
return new ApplicationModuleSourceMetadata() {
225+
226+
@Override
227+
public Stream<JavaPackage> withNestedPackages(JavaPackage pkg) {
228+
229+
return Stream.concat(Stream.of(pkg), Stream.of(delegates)
230+
.filter(Objects::nonNull)
231+
.flatMap(it -> it.withNestedPackages(pkg)));
232+
}
233+
234+
@Override
235+
public Optional<ApplicationModuleIdentifier> lookupIdentifier(JavaPackage pkg) {
236+
237+
return Stream.of(delegates)
238+
.filter(Objects::nonNull)
239+
.flatMap(it -> it.lookupIdentifier(pkg).stream())
240+
.findFirst();
241+
}
242+
};
243+
}
127244
}
128245
}

spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525

2626
import org.jmolecules.archunit.JMoleculesArchitectureRules;
2727
import org.jmolecules.archunit.JMoleculesDddRules;
28+
import org.jmolecules.ddd.annotation.Module;
2829
import org.springframework.lang.Nullable;
2930
import org.springframework.modulith.PackageInfo;
31+
import org.springframework.modulith.core.ApplicationModuleSource.ApplicationModuleSourceMetadata;
3032
import org.springframework.util.Assert;
3133
import org.springframework.util.ClassUtils;
3234

@@ -87,16 +89,18 @@ public static boolean isModulePresent() {
8789
}
8890

8991
@Nullable
90-
@SuppressWarnings("unchecked")
9192
public static Class<? extends Annotation> getModuleAnnotationTypeIfPresent() {
93+
return isModulePresent() ? Module.class : null;
94+
}
9295

93-
try {
94-
return isModulePresent()
95-
? (Class<? extends Annotation>) ClassUtils.forName(MODULE, JMoleculesTypes.class.getClassLoader())
96-
: null;
97-
} catch (Exception o_O) {
98-
return null;
99-
}
96+
/**
97+
* Returns an {@link ApplicationModuleSourceMetadata} for the {@link Module} annotation if present.
98+
*
99+
* @return will never be {@literal null}.
100+
*/
101+
@Nullable
102+
public static ApplicationModuleSourceMetadata getIdentifierSource() {
103+
return isModulePresent() ? ApplicationModuleSourceMetadata.forAnnotation(Module.class, Module::id) : null;
100104
}
101105

102106
/**
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example;
17+
18+
/**
19+
*
20+
* @author Oliver Drotbohm
21+
*/
22+
public class Example {
23+
24+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
@org.jmolecules.ddd.annotation.Module
1+
@org.jmolecules.ddd.annotation.Module(id = "customId")
22
package example.jmolecules;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
@org.springframework.modulith.ApplicationModule
1+
@org.springframework.modulith.ApplicationModule(id = "secondCustomized")
22
package example.ni.nested.b.second;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.core;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import example.Example;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Unit tests for {@link ApplicationModuleSource}.
26+
*
27+
* @author Oliver Drotbohm
28+
* @since 1.3
29+
*/
30+
class ApplicationModuleSourceUnitTests {
31+
32+
@Test // GH-846
33+
void detectsSources() {
34+
35+
var rootPackage = TestUtils.getPackage(Example.class);
36+
var detectionStrategy = ApplicationModuleDetectionStrategy.directSubPackage();
37+
38+
var sources = ApplicationModuleSource.from(rootPackage, detectionStrategy, false);
39+
40+
assertThat(sources)
41+
.extracting(ApplicationModuleSource::getIdentifier)
42+
.extracting(ApplicationModuleIdentifier::toString)
43+
.contains("ninvalid", "customId", "invalid", "ni", "ni.nested.b.first", "secondCustomized", "ni.nested");
44+
}
45+
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModulesUnitTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ void discoversComplexModuleArrangement() {
3636
.extracting(Object::toString)
3737
.containsExactlyInAnyOrder(
3838
"invalid",
39-
"jmolecules",
39+
"customId",
4040
"ni",
4141
"ni.nested",
4242
"ni.nested.b.first",
43-
"ni.nested.b.second",
43+
"secondCustomized",
4444
"springbean");
4545
}
4646

@@ -63,7 +63,7 @@ void detectsModuleNesting() {
6363
assertThat(ni.getNestedModules(modules))
6464
.extracting(ApplicationModule::getIdentifier)
6565
.extracting(Object::toString)
66-
.containsExactlyInAnyOrder("ni.nested", "ni.nested.b.first", "ni.nested.b.second");
66+
.containsExactlyInAnyOrder("ni.nested", "ni.nested.b.first", "secondCustomized");
6767
}
6868

6969
@Test // GH 578

0 commit comments

Comments
 (0)