Skip to content

Commit 2454ecb

Browse files
committed
GH-177 - Named interfaces names now default to the local package name.
Both package- and type-level declarations now use the local package name as the named interface's name. This allows to, at the same time, easily declared named interfaces based on packages but also a nice decoupling of the interface definition and the package layout as individual types can be assigned to such interfaces no matter where they are actually declared.
1 parent 6d2fdd6 commit 2454ecb

File tree

13 files changed

+270
-159
lines changed

13 files changed

+270
-159
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
import org.springframework.core.annotation.AliasFor;
2525

2626
/**
27-
* Annotation to mark a package as named interface of a {@link ApplicationModule} (either implicit or explicitly
28-
* annotated).
27+
* Annotation to mark a package as named interface of a {@link ApplicationModule} or assign a type to a named interface.
2928
*
3029
* @author Oliver Drotbohm
3130
*/
@@ -35,19 +34,21 @@
3534
public @interface NamedInterface {
3635

3736
/**
38-
* The name(s) of the named interface. Declaring multiple values here is useful in case named interfaces are defined
39-
* based on types and a particular type is supposed to be part of multiple named interfaces.
37+
* The name(s) of the named interface. If declared on a package, the package's local name will be used as default
38+
* name. Declaring multiple values here is useful in case named interfaces are defined based on types and a particular
39+
* type is supposed to be part of multiple named interfaces.
4040
*
41-
* @return
41+
* @return will never be {@literal null}.
4242
*/
4343
@AliasFor("name")
4444
String[] value() default {};
4545

4646
/**
47-
* The name(s) of the named interface. Declaring multiple values here is useful in case named interfaces are defined
48-
* based on types and a particular type is supposed to be part of multiple named interfaces.
47+
* The name(s) of the named interface. If declared on a package, the package's local name will be used as default
48+
* name. Declaring multiple values here is useful in case named interfaces are defined based on types and a particular
49+
* type is supposed to be part of multiple named interfaces.
4950
*
50-
* @return
51+
* @return will never be {@literal null}.
5152
*/
5253
@AliasFor("value")
5354
String[] name() default {};

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
import com.tngtech.archunit.base.DescribedIterable;
3333
import com.tngtech.archunit.base.DescribedPredicate;
3434
import com.tngtech.archunit.core.domain.JavaClass;
35+
import com.tngtech.archunit.core.domain.JavaModifier;
3536
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
37+
import com.tngtech.archunit.core.domain.properties.HasModifiers;
3638
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
3739
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
3840

@@ -121,7 +123,7 @@ public JavaPackage toSingle() {
121123
* @return will never be {@literal null}.
122124
*/
123125
public String getLocalName() {
124-
return name.substring(name.lastIndexOf(".") + 1);
126+
return name.substring(name.lastIndexOf('.') + 1);
125127
}
126128

127129
/**
@@ -143,6 +145,17 @@ public Classes getClasses() {
143145
return packageClasses;
144146
}
145147

148+
/**
149+
* Returns the classes exposed by this package, i.e. only public ones. Also filters the {@code package-info} type.
150+
*
151+
* @return will never be {@literal null}.
152+
*/
153+
public Classes getExposedClasses() {
154+
155+
return packageClasses.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) //
156+
.that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME)));
157+
}
158+
146159
/**
147160
* Returns all sub-packages that carry the given annotation type.
148161
*

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

Lines changed: 51 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,13 @@
1515
*/
1616
package org.springframework.modulith.core;
1717

18-
import java.util.Arrays;
1918
import java.util.Iterator;
2019
import java.util.List;
2120

2221
import org.springframework.util.Assert;
2322

24-
import com.tngtech.archunit.base.DescribedPredicate;
2523
import com.tngtech.archunit.core.domain.JavaClass;
2624
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
27-
import com.tngtech.archunit.core.domain.JavaModifier;
28-
import com.tngtech.archunit.core.domain.properties.HasModifiers;
2925

3026
/**
3127
* A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with
@@ -36,23 +32,24 @@
3632
* @author Oliver Drotbohm
3733
* @see org.springframework.modulith.ApplicationModule#allowedDependencies()
3834
*/
39-
public abstract class NamedInterface implements Iterable<JavaClass> {
35+
public class NamedInterface implements Iterable<JavaClass> {
4036

41-
private static final String UNNAMED_NAME = "<<UNNAMED>>";
42-
private static final String PACKAGE_INFO_NAME = "package-info";
37+
static final String UNNAMED_NAME = "<<UNNAMED>>";
4338

44-
protected final String name;
39+
private final String name;
40+
private final Classes classes;
4541

4642
/**
4743
* Creates a new {@link NamedInterface} with the given name.
4844
*
4945
* @param name must not be {@literal null} or empty.
5046
*/
51-
protected NamedInterface(String name) {
47+
private NamedInterface(String name, Classes classes) {
5248

5349
Assert.hasText(name, "Name must not be null or empty!");
5450

5551
this.name = name;
52+
this.classes = classes;
5653
}
5754

5855
/**
@@ -61,15 +58,17 @@ protected NamedInterface(String name) {
6158
* @param javaPackage must not be {@literal null}.
6259
* @return will never be {@literal null}.
6360
*/
64-
public static List<NamedInterface> of(JavaPackage javaPackage) {
61+
static List<NamedInterface> of(JavaPackage javaPackage) {
6562

66-
String[] name = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
67-
.map(it -> it.name()) //
63+
var names = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
64+
.map(it -> getDefaultedNames(it, javaPackage.getName())) //
6865
.orElseThrow(() -> new IllegalArgumentException(
6966
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
7067

71-
return Arrays.stream(name) //
72-
.<NamedInterface> map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
68+
var classes = javaPackage.toSingle().getExposedClasses();
69+
70+
return names.stream()
71+
.<NamedInterface> map(it -> new NamedInterface(it, classes)) //
7372
.toList();
7473
}
7574

@@ -81,8 +80,8 @@ public static List<NamedInterface> of(JavaPackage javaPackage) {
8180
* @param basePackage must not be {@literal null}.
8281
* @return will never be {@literal null}.
8382
*/
84-
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
85-
return new TypeBasedNamedInterface(name, classes, basePackage);
83+
static NamedInterface of(String name, Classes classes) {
84+
return new NamedInterface(name, classes);
8685
}
8786

8887
/**
@@ -92,7 +91,7 @@ public static TypeBasedNamedInterface of(String name, Classes classes, JavaPacka
9291
* @return will never be {@literal null}.
9392
*/
9493
static NamedInterface unnamed(JavaPackage javaPackage) {
95-
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
94+
return new NamedInterface(UNNAMED_NAME, javaPackage.toSingle().getExposedClasses());
9695
}
9796

9897
/**
@@ -120,7 +119,7 @@ public boolean contains(JavaClass type) {
120119

121120
Assert.notNull(type, "JavaClass must not be null!");
122121

123-
return getClasses().contains(type);
122+
return classes.contains(type);
124123
}
125124

126125
/**
@@ -132,7 +131,7 @@ public boolean contains(Class<?> type) {
132131

133132
Assert.notNull(type, "Type must not be null!");
134133

135-
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
134+
return !classes.that(Predicates.equivalentTo(type)).isEmpty();
136135
}
137136

138137
/**
@@ -144,7 +143,7 @@ boolean hasSameNameAs(NamedInterface other) {
144143

145144
Assert.notNull(other, "NamedInterface must not be null!");
146145

147-
return this.name.equals(other.name);
146+
return name.equals(other.name);
148147
}
149148

150149
/*
@@ -153,128 +152,49 @@ boolean hasSameNameAs(NamedInterface other) {
153152
*/
154153
@Override
155154
public Iterator<JavaClass> iterator() {
156-
return getClasses().iterator();
155+
return classes.iterator();
157156
}
158157

159158
/**
160-
* Returns all {@link Classes} making up this {@link NamedInterface}.
159+
* Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}.
161160
*
161+
* @param other must not be {@literal null}.
162162
* @return will never be {@literal null}.
163163
*/
164-
protected abstract Classes getClasses();
164+
NamedInterface merge(NamedInterface other) {
165+
166+
Assert.isTrue(this.name.equals(other.name),
167+
() -> "Named interfaces name must be equal to %s but was %s".formatted(name, other.name));
168+
169+
return new NamedInterface(name, classes.and(other.classes));
170+
}
171+
172+
/*
173+
* (non-Javadoc)
174+
* @see java.lang.Object#toString()
175+
*/
176+
@Override
177+
public String toString() {
178+
return "NamedInterface: name=%s, types=%s".formatted(name, classes);
179+
}
165180

166181
/**
167-
* Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}.
182+
* Returns the names declared in the given {@link org.springframework.modulith.NamedInterface} annotation or defaults
183+
* to the local name of the given package if none declared.
168184
*
169-
* @param other must not be {@literal null}.
185+
* @param annotation must not be {@literal null}.
186+
* @param packageName must not be {@literal null}.
170187
* @return will never be {@literal null}.
171188
*/
172-
public abstract NamedInterface merge(TypeBasedNamedInterface other);
173-
174-
private static class PackageBasedNamedInterface extends NamedInterface {
175-
176-
private final Classes classes;
177-
private final JavaPackage javaPackage;
178-
179-
public PackageBasedNamedInterface(String name, JavaPackage pkg) {
180-
181-
super(name);
182-
183-
Assert.notNull(pkg, "Package must not be null!");
184-
Assert.hasText(name, "Package name must not be null or empty!");
185-
186-
this.classes = pkg.toSingle().getClasses() //
187-
.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) //
188-
.that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME)));
189-
190-
this.javaPackage = pkg;
191-
}
192-
193-
private PackageBasedNamedInterface(String name, Classes classes, JavaPackage pkg) {
194-
195-
super(name);
196-
this.classes = classes;
197-
this.javaPackage = pkg;
198-
}
199-
200-
/*
201-
* (non-Javadoc)
202-
* @see org.springframework.modulith.model.NamedInterface#getClasses()
203-
*/
204-
@Override
205-
public Classes getClasses() {
206-
return classes;
207-
}
208-
209-
/*
210-
* (non-Javadoc)
211-
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
212-
*/
213-
@Override
214-
public NamedInterface merge(TypeBasedNamedInterface other) {
215-
return new PackageBasedNamedInterface(name, classes.and(other.classes), javaPackage);
216-
}
217-
218-
/*
219-
* (non-Javadoc)
220-
* @see org.springframework.modulith.model.NamedInterface#toString()
221-
*/
222-
@Override
223-
public String toString() {
224-
return String.format("%s - Public types residing in %s:\n%s\n", name, javaPackage.getName(),
225-
classes.format(javaPackage.getName()));
226-
}
227-
}
189+
static List<String> getDefaultedNames(org.springframework.modulith.NamedInterface annotation, String packageName) {
190+
191+
Assert.notNull(annotation, "NamedInterface must not be null!");
192+
Assert.hasText(packageName, "Package name must not be null or empty!");
193+
194+
var declaredNames = annotation.name();
228195

229-
public static class TypeBasedNamedInterface extends NamedInterface {
230-
231-
private final Classes classes;
232-
private final JavaPackage pkg;
233-
234-
/**
235-
* Creates a new {@link TypeBasedNamedInterface} with the given name, {@link Classes} and {@link JavaPackage}.
236-
*
237-
* @param name must not be {@literal null} or empty.
238-
* @param types must not be {@literal null}.
239-
* @param pkg must not be {@literal null}.
240-
*/
241-
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
242-
243-
super(name);
244-
245-
Assert.notNull(types, "Classes must not be null!");
246-
Assert.notNull(pkg, "JavaPackage must not be null!");
247-
248-
this.classes = types;
249-
this.pkg = pkg;
250-
}
251-
252-
/*
253-
* (non-Javadoc)
254-
* @see org.springframework.modulith.model.NamedInterface#getClasses()
255-
*/
256-
@Override
257-
public Classes getClasses() {
258-
return classes;
259-
}
260-
261-
/*
262-
* (non-Javadoc)
263-
* @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface)
264-
*/
265-
@Override
266-
public NamedInterface merge(TypeBasedNamedInterface other) {
267-
return new TypeBasedNamedInterface(name, classes.and(other.classes), pkg);
268-
}
269-
270-
/*
271-
* (non-Javadoc)
272-
* @see org.springframework.modulith.model.NamedInterface#toString()
273-
*/
274-
@Override
275-
public String toString() {
276-
return String.format("%s - Types underneath base package %s:\n%s\n", name, pkg.getName(),
277-
classes.format(pkg.getName()));
278-
}
196+
return declaredNames.length == 0
197+
? List.of(packageName.substring(packageName.lastIndexOf('.') + 1))
198+
: List.of(declaredNames);
279199
}
280200
}

0 commit comments

Comments
 (0)