Skip to content

Commit 9568f29

Browse files
committed
GH-267 - Explicitly declared empty allowed dependencies now forbids any dependency.
The default for @ApplicationModule(allowedDependencies) is now a single element list with a dedicated token we recognize as "all dependencies allowed". This allows users to declare an empty array explicitly to disallow any outgoing dependencies for an application module. Previously, such a declaration would have allowed any dependency.
1 parent cec759a commit 9568f29

File tree

11 files changed

+220
-37
lines changed

11 files changed

+220
-37
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
@Retention(RetentionPolicy.RUNTIME)
3030
public @interface ApplicationModule {
3131

32+
public static final String OPEN_TOKEN = \\_(ツ)_/¯";
33+
3234
/**
3335
* The human readable name of the module to be used for display and documentation purposes.
3436
*
@@ -37,14 +39,17 @@
3739
String displayName() default "";
3840

3941
/**
40-
* List the names of modules that the module is allowed to depend on. Shared modules defined in {@link Modulith} will
41-
* be allowed, too. Names listed are local ones, unless the application has configured
42-
* {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
42+
* List the names of modules that the module is allowed to depend on. Shared modules defined in
43+
* {@link Modulith}/{@link Modulithic} will be allowed, too. Names listed are local ones, unless the application has
44+
* configured {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
4345
* {@link NamedInterface}s need to be separated by a double colon {@code ::}, e.g. {@code module::API} if
4446
* {@code module} is the logical module name and {@code API} is the name of the named interface.
47+
* <p>
48+
* Declaring an empty array will allow no dependencies to other modules. To not restrict the dependencies at all,
49+
* leave the attribute at its default value.
4550
*
4651
* @return will never be {@literal null}.
4752
* @see NamedInterface
4853
*/
49-
String[] allowedDependencies() default {};
54+
String[] allowedDependencies() default { OPEN_TOKEN };
5055
}

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

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -374,20 +374,20 @@ Classes getSpringBeansInternal() {
374374
}
375375

376376
/**
377-
* Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
377+
* Returns all declared module dependencies, either explicitly declared or defined as shared on the given
378378
* {@link ApplicationModules} instance.
379379
*
380380
* @param modules must not be {@literal null}.
381381
* @return
382382
*/
383-
DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {
383+
DeclaredDependencies getDeclaredDependencies(ApplicationModules modules) {
384384

385385
Assert.notNull(modules, "Modules must not be null!");
386386

387-
var allowedDependencyNames = information.getAllowedDependencies();
387+
var allowedDependencyNames = information.getDeclaredDependencies();
388388

389-
if (allowedDependencyNames.isEmpty()) {
390-
return new DeclaredDependencies(Collections.emptyList());
389+
if (DeclaredDependencies.isOpen(allowedDependencyNames)) {
390+
return DeclaredDependencies.open();
391391
}
392392

393393
var explicitlyDeclaredModules = allowedDependencyNames.stream() //
@@ -398,7 +398,7 @@ DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {
398398

399399
return Stream.concat(explicitlyDeclaredModules, sharedDependencies) //
400400
.distinct() //
401-
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new));
401+
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed));
402402
}
403403

404404
/**
@@ -655,6 +655,19 @@ public boolean contains(JavaClass type) {
655655
return namedInterface.contains(type);
656656
}
657657

658+
/**
659+
* Returns whether the {@link DeclaredDependency} contains the given {@link Class}.
660+
*
661+
* @param type must not be {@literal null}.
662+
* @return
663+
*/
664+
public boolean contains(Class<?> type) {
665+
666+
Assert.notNull(type, "Type must not be null!");
667+
668+
return namedInterface.contains(type);
669+
}
670+
658671
/*
659672
* (non-Javadoc)
660673
* @see java.lang.Object#toString()
@@ -701,38 +714,55 @@ public int hashCode() {
701714
*/
702715
static class DeclaredDependencies {
703716

717+
private static final String OPEN_TOKEN = \\_(ツ)_/¯";
718+
704719
private final List<DeclaredDependency> dependencies;
720+
private final boolean closed;
721+
722+
static boolean isOpen(List<String> declaredDependencies) {
723+
return declaredDependencies.size() == 1 && declaredDependencies.get(0).equals(OPEN_TOKEN);
724+
}
725+
726+
public static DeclaredDependencies open() {
727+
return new DeclaredDependencies(Collections.emptyList(), false);
728+
}
729+
730+
public static DeclaredDependencies closed(List<DeclaredDependency> dependencies) {
731+
return new DeclaredDependencies(dependencies, true);
732+
}
705733

706734
/**
707735
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
708736
*
709737
* @param dependencies must not be {@literal null}.
710738
*/
711-
public DeclaredDependencies(List<DeclaredDependency> dependencies) {
739+
private DeclaredDependencies(List<DeclaredDependency> dependencies, boolean closed) {
712740

713741
Assert.notNull(dependencies, "Dependencies must not be null!");
714742

715743
this.dependencies = dependencies;
744+
this.closed = closed;
716745
}
717746

718747
/**
719-
* Returns whether any of the dependencies contains the given {@link JavaClass}.
748+
* Returns whether the given {@link JavaClass} is a valid dependency.
720749
*
721750
* @param type must not be {@literal null}.
751+
* @return
722752
*/
723-
public boolean contains(JavaClass type) {
724-
725-
Assert.notNull(type, "JavaClass must not be null!");
753+
public boolean isAllowedDependency(JavaClass type) {
754+
return isAllowedDependency(it -> it.contains(type));
755+
}
726756

727-
return dependencies.stream() //
728-
.anyMatch(it -> it.contains(type));
757+
public boolean isAllowedDependency(Class<?> type) {
758+
return isAllowedDependency(it -> it.contains(type));
729759
}
730760

731-
/**
732-
* Returns whether the {@link DeclaredDependencies} are empty.
733-
*/
734-
public boolean isEmpty() {
735-
return dependencies.isEmpty();
761+
private boolean isAllowedDependency(Predicate<DeclaredDependency> predicate) {
762+
763+
Assert.notNull(predicate, "Predicate must not be null!");
764+
765+
return closed ? !dependencies.isEmpty() && contains(predicate) : dependencies.isEmpty() || contains(predicate);
736766
}
737767

738768
/*
@@ -742,9 +772,9 @@ public boolean isEmpty() {
742772
@Override
743773
public String toString() {
744774

745-
return dependencies.stream() //
746-
.map(DeclaredDependency::toString)
747-
.collect(Collectors.joining(", "));
775+
return dependencies.isEmpty() //
776+
? "none" //
777+
: dependencies.stream().map(DeclaredDependency::toString).collect(Collectors.joining(", "));
748778
}
749779

750780
/*
@@ -773,6 +803,18 @@ public boolean equals(Object obj) {
773803
public int hashCode() {
774804
return Objects.hash(dependencies);
775805
}
806+
807+
/**
808+
* Returns whether any of the dependencies contains the given {@link JavaClass}.
809+
*
810+
* @param type must not be {@literal null}.
811+
*/
812+
private boolean contains(Predicate<DeclaredDependency> condition) {
813+
814+
Assert.notNull(condition, "Condition must not be null!");
815+
816+
return dependencies.stream().anyMatch(condition);
817+
}
776818
}
777819

778820
static class QualifiedDependency {
@@ -880,16 +922,15 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
880922
var originModule = getExistingModuleOf(source, modules);
881923
var targetModule = getExistingModuleOf(target, modules);
882924

883-
DeclaredDependencies allowedTargets = originModule.getAllowedDependencies(modules);
925+
DeclaredDependencies declaredDependencies = originModule.getDeclaredDependencies(modules);
884926
Violations violations = Violations.NONE;
885927

886928
// Check explicitly defined allowed targets
887-
888-
if (!allowedTargets.isEmpty() && !allowedTargets.contains(target)) {
929+
if (!declaredDependencies.isAllowedDependency(target)) {
889930

890931
var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." //
891932
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
892-
allowedTargets.toString());
933+
declaredDependencies.toString());
893934

894935
return violations.and(new IllegalStateException(message));
895936
}

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package org.springframework.modulith.core;
1717

1818
import java.util.Arrays;
19-
import java.util.Collections;
2019
import java.util.List;
2120
import java.util.Optional;
2221
import java.util.function.Supplier;
@@ -67,7 +66,7 @@ default Optional<String> getDisplayName() {
6766
*
6867
* @return will never be {@literal null}.
6968
*/
70-
List<String> getAllowedDependencies();
69+
List<String> getDeclaredDependencies();
7170

7271
/**
7372
* An {@link ApplicationModuleInformation} for the jMolecules {@link Module} annotation.
@@ -106,11 +105,11 @@ public Optional<String> getDisplayName() {
106105

107106
/*
108107
* (non-Javadoc)
109-
* @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
108+
* @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
110109
*/
111110
@Override
112-
public List<String> getAllowedDependencies() {
113-
return Collections.emptyList();
111+
public List<String> getDeclaredDependencies() {
112+
return List.of(ApplicationModule.OPEN_TOKEN);
114113
}
115114
}
116115

@@ -158,14 +157,14 @@ public Optional<String> getDisplayName() {
158157

159158
/*
160159
* (non-Javadoc)
161-
* @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
160+
* @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
162161
*/
163162
@Override
164-
public List<String> getAllowedDependencies() {
163+
public List<String> getDeclaredDependencies() {
165164

166165
return annotation //
167166
.map(it -> Arrays.stream(it.allowedDependencies())) //
168-
.orElse(Stream.empty()) //
167+
.orElse(Stream.of(ApplicationModule.OPEN_TOKEN)) //
169168
.toList();
170169
}
171170
}

spring-modulith-integration-test/src/main/java/example/declared/first/Declared.java

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.declared.first;
17+
18+
import example.declared.second.Second;
19+
20+
import org.springframework.stereotype.Component;
21+
22+
/**
23+
* @author Oliver Drotbohm
24+
*/
25+
@Component
26+
public class First {
27+
28+
First(Second second) {}
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// No dependencies allowed
2+
@org.springframework.modulith.ApplicationModule(allowedDependencies = {})
3+
package example.declared.first;
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.declared.fourth;
17+
18+
import org.springframework.stereotype.Component;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@Component
24+
public class Fourth {
25+
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.declared.second;
17+
18+
import example.declared.third.Third;
19+
20+
import org.springframework.stereotype.Component;
21+
22+
/**
23+
* @author Oliver Drotbohm
24+
*/
25+
@Component
26+
public class Second {
27+
Second(Third third) {}
28+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// No explicit allowed dependencies -> all allowed
2+
@org.springframework.modulith.ApplicationModule(displayName = "Second")
3+
package example.declared.second;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.declared.third;
17+
18+
import example.declared.fourth.Fourth;
19+
20+
import org.springframework.stereotype.Component;
21+
22+
/**
23+
* @author Oliver Drotbohm
24+
*/
25+
@Component
26+
public class Third {
27+
Third(Fourth fourth) {}
28+
}

0 commit comments

Comments
 (0)