Skip to content

Commit d9f298c

Browse files
committed
GH-183 - Improvements in named interface declarations.
Type based named interfaces on types declared in a module's API package still caused the type to be included in the unnamed interface. This is now fixed by explicitly removing named interface types from the unnamed interface. We now also detect API package types assigned to a named interface without an explicit name as the package name defaulting doesn't work in this case. Furthermore, named interfaces are now sorted alphabetically to make the unnamed one always appear first.
1 parent 0e6c41b commit d9f298c

File tree

7 files changed

+140
-22
lines changed

7 files changed

+140
-22
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.concurrent.ConcurrentHashMap;
2020
import java.util.stream.Collectors;
2121
import java.util.stream.Stream;
22+
import java.util.stream.StreamSupport;
2223

2324
import org.springframework.lang.Nullable;
2425
import org.springframework.util.Assert;
@@ -79,6 +80,22 @@ public static FormatableType of(Class<?> type) {
7980
return CACHE.computeIfAbsent(type.getName(), FormatableType::new);
8081
}
8182

83+
/**
84+
* Formats the given {@link JavaClass}es by rendering a comma-separated list with the abbreviated class names.
85+
*
86+
* @param types must not be {@literal null}.
87+
* @return will never be {@literal null}.
88+
*/
89+
public static String format(Iterable<JavaClass> types) {
90+
91+
Assert.notNull(types, "Types must not be null!");
92+
93+
return StreamSupport.stream(types.spliterator(), false)
94+
.map(FormatableType::of)
95+
.map(FormatableType::getAbbreviatedFullName)
96+
.collect(Collectors.joining(", "));
97+
}
98+
8299
/**
83100
* Creates a new {@link FormatableType} for the given fully-qualified type name.
84101
*

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

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

18+
import static com.tngtech.archunit.base.DescribedPredicate.*;
19+
import static org.springframework.modulith.core.Types.*;
20+
1821
import java.util.Iterator;
1922
import java.util.List;
2023

24+
import org.springframework.core.annotation.AnnotatedElementUtils;
2125
import org.springframework.util.Assert;
2226

27+
import com.tngtech.archunit.base.DescribedPredicate;
2328
import com.tngtech.archunit.core.domain.JavaClass;
2429
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
30+
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
2531

2632
/**
2733
* A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with
@@ -35,6 +41,8 @@
3541
public class NamedInterface implements Iterable<JavaClass> {
3642

3743
static final String UNNAMED_NAME = "<<UNNAMED>>";
44+
private static final DescribedPredicate<CanBeAnnotated> ANNOTATED_NAMED_INTERFACE = //
45+
isAnnotatedWith(org.springframework.modulith.NamedInterface.class);
3846

3947
private final String name;
4048
private final Classes classes;
@@ -90,7 +98,21 @@ static NamedInterface of(String name, Classes classes) {
9098
* @return will never be {@literal null}.
9199
*/
92100
static NamedInterface unnamed(JavaPackage javaPackage) {
93-
return new NamedInterface(UNNAMED_NAME, javaPackage.toSingle().getExposedClasses());
101+
102+
var basePackageClasses = javaPackage.toSingle().getExposedClasses();
103+
104+
// Types that declare the annotation but no explicit name
105+
var withDefaultedNamedInterface = basePackageClasses.stream()
106+
.filter(ANNOTATED_NAMED_INTERFACE)
107+
.filter(NamedInterface::withDefaultedNamedInterface)
108+
.toList();
109+
110+
// Illegal in the base package
111+
Assert.state(withDefaultedNamedInterface.isEmpty(),
112+
() -> "Cannot use named interface defaulting for type(s) %s located in base package!"
113+
.formatted(FormatableType.format(withDefaultedNamedInterface)));
114+
115+
return new NamedInterface(UNNAMED_NAME, basePackageClasses.that(not(ANNOTATED_NAMED_INTERFACE)));
94116
}
95117

96118
/**
@@ -174,7 +196,9 @@ NamedInterface merge(NamedInterface other) {
174196
*/
175197
@Override
176198
public String toString() {
177-
return "NamedInterface: name=%s, types=%s".formatted(name, classes);
199+
200+
return "NamedInterface: name=%s, types=[%s]" //
201+
.formatted(name, classes.isEmpty() ? "" : " " + FormatableType.format(classes) + " ");
178202
}
179203

180204
/**
@@ -196,4 +220,19 @@ static List<String> getDefaultedNames(org.springframework.modulith.NamedInterfac
196220
? List.of(packageName.substring(packageName.lastIndexOf('.') + 1))
197221
: List.of(declaredNames);
198222
}
223+
224+
/**
225+
* Returns whether the given {@link JavaClass} is annotated with {@link org.springframework.modulith.NamedInterface}
226+
* but does not declare a name explicitly.
227+
*
228+
* @param type must not be {@literal null}.
229+
* @return
230+
*/
231+
private static boolean withDefaultedNamedInterface(JavaClass type) {
232+
233+
var annotation = AnnotatedElementUtils.getMergedAnnotation(type.reflect(),
234+
org.springframework.modulith.NamedInterface.class);
235+
236+
return annotation != null && annotation.name().length == 0;
237+
}
199238
}

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collections;
20+
import java.util.Comparator;
2021
import java.util.Iterator;
2122
import java.util.List;
2223
import java.util.Optional;
@@ -49,7 +50,9 @@ private NamedInterfaces(List<NamedInterface> namedInterfaces) {
4950

5051
Assert.notNull(namedInterfaces, "Named interfaces must not be null!");
5152

52-
this.namedInterfaces = namedInterfaces;
53+
this.namedInterfaces = namedInterfaces.stream()
54+
.sorted(Comparator.comparing(NamedInterface::getName))
55+
.toList();
5356
}
5457

5558
/**
@@ -60,9 +63,9 @@ private NamedInterfaces(List<NamedInterface> namedInterfaces) {
6063
*/
6164
static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
6265

63-
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
64-
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
65-
.and(NamedInterface.unnamed(basePackage));
66+
return NamedInterfaces.of(NamedInterface.unnamed(basePackage))
67+
.and(ofAnnotatedPackages(basePackage))
68+
.and(ofAnnotatedTypes(basePackage));
6669
}
6770

6871
/**
@@ -150,14 +153,14 @@ public Iterator<NamedInterface> iterator() {
150153
* @param others must not be {@literal null}.
151154
* @return will never be {@literal null}.
152155
*/
153-
NamedInterfaces and(List<NamedInterface> others) {
156+
NamedInterfaces and(Iterable<NamedInterface> others) {
154157

155158
Assert.notNull(others, "Other NamedInterfaces must not be null!");
156159

157160
var namedInterfaces = new ArrayList<NamedInterface>();
158-
var unmergedInterface = this.namedInterfaces;
161+
var unmergedInterfaces = new ArrayList<>(this.namedInterfaces);
159162

160-
if (others.isEmpty()) {
163+
if (!others.iterator().hasNext()) {
161164
return this;
162165
}
163166

@@ -170,23 +173,18 @@ NamedInterfaces and(List<NamedInterface> others) {
170173
// Merge existing with new and add to result
171174
existing.ifPresentOrElse(it -> {
172175
namedInterfaces.add(it.merge(candidate));
173-
unmergedInterface.remove(it);
176+
unmergedInterfaces.remove(it);
174177
},
175178
() -> namedInterfaces.add(candidate));
176179
}
177180

178-
namedInterfaces.addAll(unmergedInterface);
181+
namedInterfaces.addAll(unmergedInterfaces);
179182

180183
return new NamedInterfaces(namedInterfaces);
181184
}
182185

183-
private NamedInterfaces and(NamedInterface namedInterface) {
184-
185-
var result = new ArrayList<NamedInterface>(namedInterfaces.size() + 1);
186-
result.addAll(namedInterfaces);
187-
result.add(namedInterface);
188-
189-
return new NamedInterfaces(result);
186+
private static NamedInterfaces of(NamedInterface interfaces) {
187+
return new NamedInterfaces(List.of(interfaces));
190188
}
191189

192190
private static List<NamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2023 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.ni;
17+
18+
import org.springframework.modulith.NamedInterface;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@NamedInterface("api")
24+
public interface AnnotatedNamedInterfaceType {
25+
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2023 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.ninvalid;
17+
18+
import org.springframework.modulith.NamedInterface;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@NamedInterface
24+
public interface InvalidDefaultNamedInterface {}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import example.ni.AnnotatedNamedInterfaceType;
2021
import example.ni.RootType;
2122
import example.ni.api.ApiType;
2223
import example.ni.internal.AdditionalSpiType;
2324
import example.ni.internal.DefaultedNamedInterfaceType;
2425
import example.ni.spi.SpiType;
26+
import example.ninvalid.InvalidDefaultNamedInterface;
2527

2628
import java.util.Arrays;
2729

@@ -39,21 +41,29 @@ class NamedInterfacesUnitTests {
3941
@Test
4042
void discoversNamedInterfaces() {
4143

42-
var classes = TestUtils.getClasses(RootType.class);
43-
var javaPackage = JavaPackage.of(classes, RootType.class.getPackageName());
44-
44+
var javaPackage = TestUtils.getPackage(RootType.class);
4545
var interfaces = NamedInterfaces.discoverNamedInterfaces(javaPackage);
4646

4747
assertThat(interfaces).map(NamedInterface::getName)
4848
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal");
4949

5050
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class);
51-
assertInterfaceContains(interfaces, "api", ApiType.class);
51+
assertInterfaceContains(interfaces, "api", ApiType.class, AnnotatedNamedInterfaceType.class);
5252
assertInterfaceContains(interfaces, "spi", SpiType.class, AdditionalSpiType.class);
5353
assertInterfaceContains(interfaces, "kpi", AdditionalSpiType.class);
5454
assertInterfaceContains(interfaces, "internal", DefaultedNamedInterfaceType.class);
5555
}
5656

57+
@Test // GH-183
58+
void rejectsDefaultingNamedInterfaceTypeInBasePackage() {
59+
60+
var javaPackage = TestUtils.getPackage(InvalidDefaultNamedInterface.class);
61+
62+
assertThatIllegalStateException().isThrownBy(() -> NamedInterfaces.discoverNamedInterfaces(javaPackage))
63+
.withMessageContaining("named interface defaulting")
64+
.withMessageContaining(InvalidDefaultNamedInterface.class.getSimpleName());
65+
}
66+
5767
private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class<?>... types) {
5868

5969
var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ public static Classes getClasses(Class<?> packageType) {
7070
.importPackagesOf(packageType)
7171
.that(resideInAPackage(packageType.getPackage().getName() + "..")));
7272
}
73+
74+
public static JavaPackage getPackage(Class<?> packageType) {
75+
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
76+
}
7377
}

0 commit comments

Comments
 (0)