1515 */
1616package org .springframework .modulith .core ;
1717
18+ import java .lang .annotation .Annotation ;
1819import java .util .Objects ;
20+ import java .util .Optional ;
21+ import java .util .function .Function ;
1922import java .util .stream .Stream ;
2023
24+ import org .springframework .modulith .ApplicationModule ;
25+ import org .springframework .modulith .core .Types .JMoleculesTypes ;
2126import 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
3238 */
3339public 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}
0 commit comments