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 -> {