diff --git a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java
index 1919f90b..bbe9faec 100644
--- a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java
+++ b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java
@@ -29,6 +29,8 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationModule {
+ public static final String OPEN_TOKEN = "¯\\_(ツ)_/¯";
+
/**
* The human readable name of the module to be used for display and documentation purposes.
*
@@ -37,14 +39,17 @@ public @interface ApplicationModule {
String displayName() default "";
/**
- * List the names of modules that the module is allowed to depend on. Shared modules defined in {@link Modulith} will
- * be allowed, too. Names listed are local ones, unless the application has configured
- * {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
+ * List the names of modules that the module is allowed to depend on. Shared modules defined in
+ * {@link Modulith}/{@link Modulithic} will be allowed, too. Names listed are local ones, unless the application has
+ * configured {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
* {@link NamedInterface}s need to be separated by a double colon {@code ::}, e.g. {@code module::API} if
* {@code module} is the logical module name and {@code API} is the name of the named interface.
+ *
+ * Declaring an empty array will allow no dependencies to other modules. To not restrict the dependencies at all,
+ * leave the attribute at its default value.
*
* @return will never be {@literal null}.
* @see NamedInterface
*/
- String[] allowedDependencies() default {};
+ String[] allowedDependencies() default { OPEN_TOKEN };
}
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
index 6be3139e..df544d3c 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
@@ -374,20 +374,20 @@ public class ApplicationModule {
}
/**
- * Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
+ * Returns all declared module dependencies, either explicitly declared or defined as shared on the given
* {@link ApplicationModules} instance.
*
* @param modules must not be {@literal null}.
* @return
*/
- DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {
+ DeclaredDependencies getDeclaredDependencies(ApplicationModules modules) {
Assert.notNull(modules, "Modules must not be null!");
- var allowedDependencyNames = information.getAllowedDependencies();
+ var allowedDependencyNames = information.getDeclaredDependencies();
- if (allowedDependencyNames.isEmpty()) {
- return new DeclaredDependencies(Collections.emptyList());
+ if (DeclaredDependencies.isOpen(allowedDependencyNames)) {
+ return DeclaredDependencies.open();
}
var explicitlyDeclaredModules = allowedDependencyNames.stream() //
@@ -398,7 +398,7 @@ public class ApplicationModule {
return Stream.concat(explicitlyDeclaredModules, sharedDependencies) //
.distinct() //
- .collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new));
+ .collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed));
}
/**
@@ -655,6 +655,19 @@ public class ApplicationModule {
return namedInterface.contains(type);
}
+ /**
+ * Returns whether the {@link DeclaredDependency} contains the given {@link Class}.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ public boolean contains(Class> type) {
+
+ Assert.notNull(type, "Type must not be null!");
+
+ return namedInterface.contains(type);
+ }
+
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
@@ -701,38 +714,55 @@ public class ApplicationModule {
*/
static class DeclaredDependencies {
+ private static final String OPEN_TOKEN = "¯\\_(ツ)_/¯";
+
private final List dependencies;
+ private final boolean closed;
+
+ static boolean isOpen(List declaredDependencies) {
+ return declaredDependencies.size() == 1 && declaredDependencies.get(0).equals(OPEN_TOKEN);
+ }
+
+ public static DeclaredDependencies open() {
+ return new DeclaredDependencies(Collections.emptyList(), false);
+ }
+
+ public static DeclaredDependencies closed(List dependencies) {
+ return new DeclaredDependencies(dependencies, true);
+ }
/**
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
*
* @param dependencies must not be {@literal null}.
*/
- public DeclaredDependencies(List dependencies) {
+ private DeclaredDependencies(List dependencies, boolean closed) {
Assert.notNull(dependencies, "Dependencies must not be null!");
this.dependencies = dependencies;
+ this.closed = closed;
}
/**
- * Returns whether any of the dependencies contains the given {@link JavaClass}.
+ * Returns whether the given {@link JavaClass} is a valid dependency.
*
* @param type must not be {@literal null}.
+ * @return
*/
- public boolean contains(JavaClass type) {
-
- Assert.notNull(type, "JavaClass must not be null!");
-
- return dependencies.stream() //
- .anyMatch(it -> it.contains(type));
+ public boolean isAllowedDependency(JavaClass type) {
+ return isAllowedDependency(it -> it.contains(type));
}
- /**
- * Returns whether the {@link DeclaredDependencies} are empty.
- */
- public boolean isEmpty() {
- return dependencies.isEmpty();
+ public boolean isAllowedDependency(Class> type) {
+ return isAllowedDependency(it -> it.contains(type));
+ }
+
+ private boolean isAllowedDependency(Predicate predicate) {
+
+ Assert.notNull(predicate, "Predicate must not be null!");
+
+ return closed ? !dependencies.isEmpty() && contains(predicate) : dependencies.isEmpty() || contains(predicate);
}
/*
@@ -742,9 +772,9 @@ public class ApplicationModule {
@Override
public String toString() {
- return dependencies.stream() //
- .map(DeclaredDependency::toString)
- .collect(Collectors.joining(", "));
+ return dependencies.isEmpty() //
+ ? "none" //
+ : dependencies.stream().map(DeclaredDependency::toString).collect(Collectors.joining(", "));
}
/*
@@ -773,6 +803,18 @@ public class ApplicationModule {
public int hashCode() {
return Objects.hash(dependencies);
}
+
+ /**
+ * Returns whether any of the dependencies contains the given {@link JavaClass}.
+ *
+ * @param type must not be {@literal null}.
+ */
+ private boolean contains(Predicate condition) {
+
+ Assert.notNull(condition, "Condition must not be null!");
+
+ return dependencies.stream().anyMatch(condition);
+ }
}
static class QualifiedDependency {
@@ -880,16 +922,15 @@ public class ApplicationModule {
var originModule = getExistingModuleOf(source, modules);
var targetModule = getExistingModuleOf(target, modules);
- DeclaredDependencies allowedTargets = originModule.getAllowedDependencies(modules);
+ DeclaredDependencies declaredDependencies = originModule.getDeclaredDependencies(modules);
Violations violations = Violations.NONE;
// Check explicitly defined allowed targets
-
- if (!allowedTargets.isEmpty() && !allowedTargets.contains(target)) {
+ if (!declaredDependencies.isAllowedDependency(target)) {
var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." //
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
- allowedTargets.toString());
+ declaredDependencies.toString());
return violations.and(new IllegalStateException(message));
}
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java
index 6175d85e..21ee03a4 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java
@@ -16,7 +16,6 @@
package org.springframework.modulith.core;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
@@ -67,7 +66,7 @@ interface ApplicationModuleInformation {
*
* @return will never be {@literal null}.
*/
- List getAllowedDependencies();
+ List getDeclaredDependencies();
/**
* An {@link ApplicationModuleInformation} for the jMolecules {@link Module} annotation.
@@ -106,11 +105,11 @@ interface ApplicationModuleInformation {
/*
* (non-Javadoc)
- * @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
+ * @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
*/
@Override
- public List getAllowedDependencies() {
- return Collections.emptyList();
+ public List getDeclaredDependencies() {
+ return List.of(ApplicationModule.OPEN_TOKEN);
}
}
@@ -158,14 +157,14 @@ interface ApplicationModuleInformation {
/*
* (non-Javadoc)
- * @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
+ * @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
*/
@Override
- public List getAllowedDependencies() {
+ public List getDeclaredDependencies() {
return annotation //
.map(it -> Arrays.stream(it.allowedDependencies())) //
- .orElse(Stream.empty()) //
+ .orElse(Stream.of(ApplicationModule.OPEN_TOKEN)) //
.toList();
}
}
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/first/Declared.java b/spring-modulith-integration-test/src/main/java/example/declared/first/Declared.java
new file mode 100644
index 00000000..e69de29b
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/first/First.java b/spring-modulith-integration-test/src/main/java/example/declared/first/First.java
new file mode 100644
index 00000000..0a93135b
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/first/First.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.declared.first;
+
+import example.declared.second.Second;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+public class First {
+
+ First(Second second) {}
+}
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/first/package-info.java b/spring-modulith-integration-test/src/main/java/example/declared/first/package-info.java
new file mode 100644
index 00000000..4bc98127
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/first/package-info.java
@@ -0,0 +1,3 @@
+// No dependencies allowed
+@org.springframework.modulith.ApplicationModule(allowedDependencies = {})
+package example.declared.first;
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/fourth/Fourth.java b/spring-modulith-integration-test/src/main/java/example/declared/fourth/Fourth.java
new file mode 100644
index 00000000..8ae4ec4a
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/fourth/Fourth.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.declared.fourth;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+public class Fourth {
+
+}
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/second/Second.java b/spring-modulith-integration-test/src/main/java/example/declared/second/Second.java
new file mode 100644
index 00000000..0f436a0a
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/second/Second.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.declared.second;
+
+import example.declared.third.Third;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+public class Second {
+ Second(Third third) {}
+}
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/second/package-info.java b/spring-modulith-integration-test/src/main/java/example/declared/second/package-info.java
new file mode 100644
index 00000000..65d04d3c
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/second/package-info.java
@@ -0,0 +1,3 @@
+// No explicit allowed dependencies -> all allowed
+@org.springframework.modulith.ApplicationModule(displayName = "Second")
+package example.declared.second;
diff --git a/spring-modulith-integration-test/src/main/java/example/declared/third/Third.java b/spring-modulith-integration-test/src/main/java/example/declared/third/Third.java
new file mode 100644
index 00000000..a76a7740
--- /dev/null
+++ b/spring-modulith-integration-test/src/main/java/example/declared/third/Third.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example.declared.third;
+
+import example.declared.fourth.Fourth;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Oliver Drotbohm
+ */
+@Component
+public class Third {
+ Third(Fourth fourth) {}
+}
diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java
index 662e0398..a0e85d42 100644
--- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java
+++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java
@@ -17,6 +17,11 @@ package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
+import example.declared.first.First;
+import example.declared.fourth.Fourth;
+import example.declared.second.Second;
+import example.declared.third.Third;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -164,6 +169,22 @@ class ApplicationModulesIntegrationTest {
assertThat(source).containsExactly(ServiceComponentA.class, ServiceComponentB.class, String.class);
}
+ @Test // GH-267
+ void explicitEmptyAllowedModulesResultsInAllDependenciesRejected() {
+
+ var modules = ApplicationModules.of("example.declared");
+ var first = modules.getModuleByType(First.class).orElseThrow();
+ var second = modules.getModuleByType(Second.class).orElseThrow();
+ var third = modules.getModuleByType(Third.class).orElseThrow();
+
+ // Disallowed due to allowedDependencies = {}
+ assertThat(first.getDeclaredDependencies(modules).isAllowedDependency(Second.class)).isFalse();
+
+ // Allowed as allowedDependencies not set
+ assertThat(second.getDeclaredDependencies(modules).isAllowedDependency(Third.class)).isTrue();
+ assertThat(third.getDeclaredDependencies(modules).isAllowedDependency(Fourth.class)).isTrue();
+ }
+
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class>... types) {
Stream.of(types).forEach(type -> {