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 07b9cd32..157972e2 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 @@ -25,7 +25,8 @@ import java.lang.annotation.Target; * * @author Oliver Drotbohm */ -@Target({ ElementType.PACKAGE, ElementType.ANNOTATION_TYPE }) +@PackageInfo +@Target({ ElementType.PACKAGE, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface ApplicationModule { diff --git a/spring-modulith-api/src/main/java/org/springframework/modulith/PackageInfo.java b/spring-modulith-api/src/main/java/org/springframework/modulith/PackageInfo.java new file mode 100644 index 00000000..492676cd --- /dev/null +++ b/spring-modulith-api/src/main/java/org/springframework/modulith/PackageInfo.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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 org.springframework.modulith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to mark a type as an alternative to Java's native {@code package-info.java}. Classes annotated like + * this will be considered, too, when looking up annotations on a package. + * + * @author Oliver Drotbohm + * @since 1.2 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface PackageInfo {} 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 d17b23e2..d32f1f6f 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 @@ -18,7 +18,7 @@ package org.springframework.modulith.core; import static com.tngtech.archunit.base.DescribedPredicate.*; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; import static java.lang.System.*; -import static org.springframework.modulith.core.Types.*; +import static org.springframework.modulith.core.SyntacticSugar.*; import static org.springframework.modulith.core.Types.JavaXTypes.*; import static org.springframework.modulith.core.Types.SpringDataTypes.*; import static org.springframework.modulith.core.Types.SpringTypes.*; @@ -84,6 +84,8 @@ public class ApplicationModule { */ ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) { + Assert.notNull(basePackage, "Base package must not be null!"); + this.basePackage = basePackage; this.information = ApplicationModuleInformation.of(basePackage); this.namedInterfaces = isOpen() 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 72012bbf..76381b7f 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 @@ -15,18 +15,23 @@ */ package org.springframework.modulith.core; +import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; +import org.jmolecules.ddd.annotation.Module; import org.springframework.modulith.ApplicationModule; import org.springframework.modulith.ApplicationModule.Type; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import com.tngtech.archunit.core.domain.JavaClass; + /** * Abstraction for low-level module information. Used to support different annotations to configure metadata about a * module. @@ -43,13 +48,15 @@ interface ApplicationModuleInformation { */ public static ApplicationModuleInformation of(JavaPackage javaPackage) { + var lookup = AnnotationLookup.of(javaPackage, __ -> true); + if (ClassUtils.isPresent("org.jmolecules.ddd.annotation.Module", ApplicationModuleInformation.class.getClassLoader()) - && JMoleculesModule.supports(javaPackage)) { - return new JMoleculesModule(javaPackage); + && JMoleculesModule.supports(lookup)) { + return new JMoleculesModule(lookup); } - return new SpringModulithModule(javaPackage); + return new SpringModulithModule(lookup); } /** @@ -84,14 +91,14 @@ interface ApplicationModuleInformation { */ static class JMoleculesModule implements ApplicationModuleInformation { - private final Optional annotation; + private final Optional annotation; - public static boolean supports(JavaPackage javaPackage) { - return javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class).isPresent(); + public static boolean supports(AnnotationLookup lookup) { + return lookup.lookup(Module.class).isPresent(); } - public JMoleculesModule(JavaPackage javaPackage) { - this.annotation = javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class); + public JMoleculesModule(AnnotationLookup lookup) { + this.annotation = lookup.lookup(Module.class); } /* @@ -102,11 +109,11 @@ interface ApplicationModuleInformation { public Optional getDisplayName() { Supplier> fallback = () -> annotation // - .map(org.jmolecules.ddd.annotation.Module::value) // + .map(Module::value) // .filter(StringUtils::hasText); return annotation // - .map(org.jmolecules.ddd.annotation.Module::name) // + .map(Module::name) // .filter(StringUtils::hasText) .or(fallback); } @@ -144,11 +151,11 @@ interface ApplicationModuleInformation { * * @param javaPackage must not be {@literal null}. */ - public static boolean supports(JavaPackage javaPackage) { + public static boolean supports(AnnotationLookup lookup) { - Assert.notNull(javaPackage, "Java package must not be null!"); + Assert.notNull(lookup, "Annotation lookup must not be null!"); - return javaPackage.getAnnotation(ApplicationModule.class).isPresent(); + return lookup.lookup(ApplicationModule.class).isPresent(); } /** @@ -156,8 +163,8 @@ interface ApplicationModuleInformation { * * @param javaPackage must not be {@literal null}. */ - public SpringModulithModule(JavaPackage javaPackage) { - this.annotation = javaPackage.getAnnotation(ApplicationModule.class); + public SpringModulithModule(AnnotationLookup lookup) { + this.annotation = lookup.lookup(ApplicationModule.class); } /* @@ -194,4 +201,21 @@ interface ApplicationModuleInformation { return annotation.map(it -> it.type().equals(Type.OPEN)).orElse(false); } } + + interface AnnotationLookup { + + static AnnotationLookup of(JavaPackage javaPackage, + Predicate typeSelector) { + + return new AnnotationLookup() { + + @Override + public Optional lookup(Class annotation) { + return javaPackage.findAnnotation(annotation); + } + }; + } + + Optional lookup(Class annotation); + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java index 6d42acfd..d4f40f91 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java @@ -15,6 +15,7 @@ */ package org.springframework.modulith.core; +import static org.springframework.modulith.core.SyntacticSugar.*; import static org.springframework.modulith.core.Types.JavaXTypes.*; import java.util.ArrayList; @@ -194,8 +195,8 @@ public abstract class ArchitecturallyEvidentType { * Methods (meta-)annotated with @EventListener. */ private static final Predicate IS_ANNOTATED_EVENT_LISTENER = it -> // - Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(it) // - || Types.isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).test(it); + isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(it) // + || isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).test(it); /** * {@code ApplicationListener.onApplicationEvent(…)} @@ -232,7 +233,7 @@ public abstract class ArchitecturallyEvidentType { */ @Override public boolean isRepository() { - return Types.isAnnotatedWith(SpringTypes.AT_REPOSITORY).test(getType()); + return isAnnotatedWith(SpringTypes.AT_REPOSITORY).test(getType()); } /* @@ -241,7 +242,7 @@ public abstract class ArchitecturallyEvidentType { */ @Override public boolean isService() { - return Types.isAnnotatedWith(SpringTypes.AT_SERVICE).test(getType()); + return isAnnotatedWith(SpringTypes.AT_SERVICE).test(getType()); } /* @@ -250,7 +251,7 @@ public abstract class ArchitecturallyEvidentType { */ @Override public boolean isController() { - return Types.isAnnotatedWith(SpringTypes.AT_CONTROLLER).test(getType()); + return isAnnotatedWith(SpringTypes.AT_CONTROLLER).test(getType()); } /* @@ -268,7 +269,7 @@ public abstract class ArchitecturallyEvidentType { */ @Override public boolean isConfigurationProperties() { - return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType()); + return isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType()); } /* @@ -365,15 +366,15 @@ public abstract class ArchitecturallyEvidentType { */ @Override public boolean isController() { - return Types.isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController") + return isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController") .test(getType()); } } static class JMoleculesArchitecturallyEvidentType extends ArchitecturallyEvidentType { - private static final Predicate IS_ANNOTATED_EVENT_LISTENER = Types - .isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::test; + private static final Predicate IS_ANNOTATED_EVENT_LISTENER = isAnnotatedWith( + JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::test; JMoleculesArchitecturallyEvidentType(JavaClass type) { super(type); @@ -388,7 +389,7 @@ public abstract class ArchitecturallyEvidentType { JavaClass type = getType(); - return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).test(type) || // + return isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).test(type) || // type.isAssignableTo(org.jmolecules.ddd.types.Entity.class); } @@ -401,7 +402,7 @@ public abstract class ArchitecturallyEvidentType { JavaClass type = getType(); - return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).test(type) // + return isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).test(type) // || type.isAssignableTo(org.jmolecules.ddd.types.AggregateRoot.class); } @@ -414,7 +415,7 @@ public abstract class ArchitecturallyEvidentType { JavaClass type = getType(); - return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).test(type) + return isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).test(type) || type.isAssignableTo(org.jmolecules.ddd.types.Repository.class); } @@ -427,7 +428,7 @@ public abstract class ArchitecturallyEvidentType { JavaClass type = getType(); - return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).test(type); + return isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).test(type); } /* @@ -448,7 +449,7 @@ public abstract class ArchitecturallyEvidentType { JavaClass type = getType(); - return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type) + return isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type) || type.isAssignableTo(org.jmolecules.ddd.types.ValueObject.class); } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/DependencyType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/DependencyType.java index 8ea8279a..3079036d 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/DependencyType.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/DependencyType.java @@ -15,6 +15,8 @@ */ package org.springframework.modulith.core; +import static org.springframework.modulith.core.SyntacticSugar.*; + import java.util.Arrays; import java.util.Collection; import java.util.function.Predicate; @@ -125,8 +127,8 @@ public enum DependencyType { } static DependencyType forCodeUnit(JavaCodeUnit codeUnit) { - return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) // - || Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) // + return isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) // + || isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) // ? EVENT_LISTENER : DEFAULT; } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java index f47ce708..dfb9bf94 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java @@ -16,6 +16,9 @@ package org.springframework.modulith.core; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.*; +import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.*; +import static org.springframework.modulith.core.SyntacticSugar.*; import java.lang.annotation.Annotation; import java.util.Collection; @@ -28,6 +31,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.modulith.PackageInfo; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; @@ -35,8 +39,6 @@ import com.tngtech.archunit.base.DescribedIterable; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaModifier; -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; -import com.tngtech.archunit.core.domain.properties.HasModifiers; /** * An abstraction of a Java package. @@ -46,6 +48,9 @@ import com.tngtech.archunit.core.domain.properties.HasModifiers; public class JavaPackage implements DescribedIterable { private static final String PACKAGE_INFO_NAME = "package-info"; + private static final String MULTIPLE_TYPES_ANNOTATED_WITH = "Expected maximum of one type in package %s to be annotated with %s, but got %s!"; + private static final DescribedPredicate ARE_PACKAGE_INFOS = // + has(simpleName(PACKAGE_INFO_NAME)).or(is(metaAnnotatedWith(PackageInfo.class))); private final String name; private final Classes classes; @@ -96,7 +101,7 @@ public class JavaPackage implements DescribedIterable { Assert.notNull(type, "Type must not be null!"); - return type.getSimpleName().equals(PACKAGE_INFO_NAME); + return ARE_PACKAGE_INFOS.test(type); } /** @@ -152,8 +157,9 @@ public class JavaPackage implements DescribedIterable { */ public Classes getExposedClasses() { - return packageClasses.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) // - .that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME))); + return packageClasses // + .that(doNotHave(simpleName(PACKAGE_INFO_NAME))) // + .that(have(modifier(JavaModifier.PUBLIC))); } /** @@ -166,8 +172,7 @@ public class JavaPackage implements DescribedIterable { Assert.notNull(annotation, "Annotation must not be null!"); - return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) // - .and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() // + return packageClasses.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() // .map(JavaClass::getPackageName) // .distinct() // .map(it -> of(classes, it)); @@ -228,13 +233,46 @@ public class JavaPackage implements DescribedIterable { */ public Optional getAnnotation(Class annotationType) { - return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) // - .and(CanBeAnnotated.Predicates.annotatedWith(annotationType))) // + return packageClasses.that(have(simpleName(PACKAGE_INFO_NAME)) // + .and(are(metaAnnotatedWith(annotationType)))) // .toOptional() // .map(it -> it.reflect()) .map(it -> AnnotatedElementUtils.getMergedAnnotation(it, annotationType)); } + /** + * Finds the annotation of the given type declared on the package itself or any type located the direct package's + * types . + * + * @param the type of the annotation. + * @param annotationType must not be {@literal null}. + * @param typeFilter must not be {@literal null}. + * @return will never be {@literal null}. + * @since 1.2 + * @throws IllegalStateException in case multiple types in the current package are annotated with the given + * annotation. + */ + public Optional findAnnotation(Class annotationType) { + + return getAnnotation(annotationType) + .or(() -> { + + var annotatedTypes = toSingle().packageClasses + .that(are(metaAnnotatedWith(PackageInfo.class).and(are(metaAnnotatedWith(annotationType))))) + .stream() + .map(it -> it.getAnnotationOfType(annotationType)) + .toList(); + + if (annotatedTypes.size() > 1) { + + throw new IllegalStateException(MULTIPLE_TYPES_ANNOTATED_WITH.formatted(name, + FormatableType.of(annotationType).getAbbreviatedFullName(), annotatedTypes)); + } + + return annotatedTypes.isEmpty() ? Optional.empty() : Optional.of(annotatedTypes.get(0)); + }); + } + /* * (non-Javadoc) * @see com.tngtech.archunit.base.HasDescription#getDescription() diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java index f2104482..08f31449 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java @@ -16,6 +16,7 @@ package org.springframework.modulith.core; import static com.tngtech.archunit.base.DescribedPredicate.*; +import static org.springframework.modulith.core.SyntacticSugar.*; import static org.springframework.modulith.core.Types.*; import java.util.Iterator; @@ -71,7 +72,7 @@ public class NamedInterface implements Iterable { */ static List of(JavaPackage javaPackage) { - var names = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) // + var names = javaPackage.findAnnotation(org.springframework.modulith.NamedInterface.class) // .map(it -> getDefaultedNames(it, javaPackage.getName())) // .orElseThrow(() -> new IllegalArgumentException( String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage))); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/SyntacticSugar.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/SyntacticSugar.java new file mode 100644 index 00000000..8c278036 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/SyntacticSugar.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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 org.springframework.modulith.core; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates; + +/** + * Helper to make the composition of {@link DescribedPredicate}s more readable. + * + * @author Oliver Drotbohm + * @since 1.2 + */ +class SyntacticSugar { + + static DescribedPredicate isAnnotatedWith(Class type) { + return isAnnotatedWith(type.getName()); + } + + static DescribedPredicate isAnnotatedWith(String type) { + return Predicates.metaAnnotatedWith(type); + } + + static DescribedPredicate are(DescribedPredicate predicate) { + return predicate; + } + + static DescribedPredicate has(DescribedPredicate predicate) { + return predicate; + } + + static DescribedPredicate have(DescribedPredicate predicate) { + return predicate; + } + + static DescribedPredicate is(DescribedPredicate predicate) { + return predicate; + } + + static DescribedPredicate doNotHave(DescribedPredicate predicate) { + return DescribedPredicate.not(predicate); + } +} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java index 1c9a1a90..462dbdbd 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java @@ -16,18 +16,18 @@ package org.springframework.modulith.core; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; +import static org.springframework.modulith.core.SyntacticSugar.*; import java.lang.annotation.Annotation; import org.springframework.lang.Nullable; +import org.springframework.modulith.PackageInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates; /** * @author Oliver Drotbohm @@ -141,20 +141,11 @@ class Types { } static DescribedPredicate isSpringDataRepository() { - return assignableTo(SpringDataTypes.REPOSITORY) // + return is(assignableTo(SpringDataTypes.REPOSITORY)) // .or(isAnnotatedWith(SpringDataTypes.AT_REPOSITORY_DEFINITION)); } } - static DescribedPredicate isAnnotatedWith(Class type) { - return isAnnotatedWith(type.getName()); - } - - static DescribedPredicate isAnnotatedWith(String type) { - return Predicates.annotatedWith(type) // - .or(Predicates.metaAnnotatedWith(type)); - } - /** * Creates a new {@link DescribedPredicate} to match classes * @@ -170,7 +161,12 @@ class Types { @Override public boolean test(JavaClass t) { - return t.getPackage().isMetaAnnotatedWith(type); + + var pkg = t.getPackage(); + + return pkg.isMetaAnnotatedWith(type) + || pkg.getClasses().stream() + .anyMatch(it -> it.isMetaAnnotatedWith(PackageInfo.class) && it.isMetaAnnotatedWith(type)); } }; } diff --git a/spring-modulith-core/src/test/java/example/ni/ontype/Exposed.java b/spring-modulith-core/src/test/java/example/ni/ontype/Exposed.java new file mode 100644 index 00000000..b730aca5 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/ontype/Exposed.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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.ni.ontype; + +/** + * @author Oliver Drotbohm + */ +public class Exposed {} diff --git a/spring-modulith-core/src/test/java/example/ni/ontype/Marker.java b/spring-modulith-core/src/test/java/example/ni/ontype/Marker.java new file mode 100644 index 00000000..92f0dbd3 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/ontype/Marker.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.ni.ontype; + +import org.springframework.modulith.NamedInterface; +import org.springframework.modulith.PackageInfo; + +/** + * @author Oliver Drotbohm + */ +@PackageInfo +@NamedInterface +class Marker { + +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java index 7ecd2337..9505971b 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java @@ -23,6 +23,7 @@ import example.ni.api.ApiType; import example.ni.internal.AdditionalSpiType; import example.ni.internal.DefaultedNamedInterfaceType; import example.ni.internal.Internal; +import example.ni.ontype.Exposed; import example.ni.spi.SpiType; import example.ninvalid.InvalidDefaultNamedInterface; @@ -46,13 +47,14 @@ class NamedInterfacesUnitTests { var interfaces = NamedInterfaces.discoverNamedInterfaces(javaPackage); assertThat(interfaces).map(NamedInterface::getName) - .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal"); + .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype"); assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class); assertInterfaceContains(interfaces, "api", ApiType.class, AnnotatedNamedInterfaceType.class); assertInterfaceContains(interfaces, "spi", SpiType.class, AdditionalSpiType.class); assertInterfaceContains(interfaces, "kpi", AdditionalSpiType.class); assertInterfaceContains(interfaces, "internal", DefaultedNamedInterfaceType.class); + assertInterfaceContains(interfaces, "ontype", Exposed.class); } @Test // GH-183 @@ -72,7 +74,7 @@ class NamedInterfacesUnitTests { var interfaces = NamedInterfaces.forOpen(javaPackage); assertThat(interfaces).map(NamedInterface::getName) - .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal"); + .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype"); assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class); } diff --git a/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerOne.java b/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerOne.java new file mode 100644 index 00000000..e6ad28a7 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerOne.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 pkg.multipleontype; + +import org.springframework.modulith.ApplicationModule; + +/** + * @author Oliver Drotbohm + */ +@ApplicationModule +class MarkerOne {} diff --git a/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerTwo.java b/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerTwo.java new file mode 100644 index 00000000..f488c557 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/pkg/multipleontype/MarkerTwo.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 pkg.multipleontype; + +import org.springframework.modulith.ApplicationModule; + +/** + * @author Oliver Drotbohm + */ +@ApplicationModule +class MarkerTwo {} diff --git a/spring-modulith-integration-test/src/main/java/pkg/onpackage/package-info.java b/spring-modulith-integration-test/src/main/java/pkg/onpackage/package-info.java new file mode 100644 index 00000000..09b1de9a --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/pkg/onpackage/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.ApplicationModule(displayName = "onPackage") +package pkg.onpackage; diff --git a/spring-modulith-integration-test/src/main/java/pkg/ontype/Marker.java b/spring-modulith-integration-test/src/main/java/pkg/ontype/Marker.java new file mode 100644 index 00000000..43d4f51e --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/pkg/ontype/Marker.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 pkg.ontype; + +import org.springframework.modulith.ApplicationModule; + +/** + * @author Oliver Drotbohm + */ +@ApplicationModule(displayName = "onType") +public class Marker {} diff --git a/spring-modulith-integration-test/src/main/java/pkg/ontype/nested/Marker.java b/spring-modulith-integration-test/src/main/java/pkg/ontype/nested/Marker.java new file mode 100644 index 00000000..125fd696 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/pkg/ontype/nested/Marker.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 pkg.ontype.nested; + +import org.springframework.modulith.ApplicationModule; + +/** + * @author Oliver Drotbohm + */ +@ApplicationModule(displayName = "onTypeNested") +public class Marker {} diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModuleInformationUnitTests.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModuleInformationUnitTests.java new file mode 100644 index 00000000..af3f2f17 --- /dev/null +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModuleInformationUnitTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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 org.springframework.modulith.core; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; + +/** + * Unit tests for {@link ApplicationModuleInformation}. + * + * @author Oliver Drotbohm + */ +class ApplicationModuleInformationUnitTests { + + static final JavaClasses PKG_CLASSES = new ClassFileImporter() // + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) // + .importPackages("pkg"); + + @Test // GH-522 + void detectsApplicationInformationOnType() { + + var pkg = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.ontype"); + var information = ApplicationModuleInformation.of(pkg); + + assertThat(information.getDisplayName()).hasValue("onType"); + } +} diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/JavaPackageUnitTests.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/JavaPackageUnitTests.java index 0d44c37a..39ea4417 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/JavaPackageUnitTests.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/JavaPackageUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.modulith.core; import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; +import org.springframework.modulith.ApplicationModule; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; @@ -29,11 +30,15 @@ import com.tngtech.archunit.core.importer.ImportOption; class JavaPackageUnitTests { static final JavaClasses ALL_CLASSES = new ClassFileImporter() // - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) // .importPackages("com.acme.myproject"); + static final JavaClasses PKG_CLASSES = new ClassFileImporter() // + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) // + .importPackages("pkg"); + @Test - void testName() throws Exception { + void detectsDirectSubPackages() throws Exception { Classes classes = Classes.of(ALL_CLASSES); JavaPackage pkg = JavaPackage.of(classes, "com.acme.myproject.complex"); @@ -43,4 +48,35 @@ class JavaPackageUnitTests { .extracting(JavaPackage::getLocalName) // .contains("api", "internal", "spi"); } + + @Test // GH-522 + void findsAnnotationOnPackageInfo() { + + var annotation = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.onpackage") // + .getAnnotation(ApplicationModule.class); + + assertThat(annotation).hasValueSatisfying(it -> { + assertThat(it.displayName()).isEqualTo("onPackage"); + }); + } + + @Test // GH-522 + void findsAnnotationOnPackageType() { + + var annotation = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.ontype") // + .findAnnotation(ApplicationModule.class); + + assertThat(annotation).hasValueSatisfying(it -> { + assertThat(it.displayName()).isEqualTo("onType"); + }); + } + + @Test // GH-522 + void rejectsMultipleAnnotationsOnType() { + + assertThatIllegalStateException().isThrownBy(() -> { + JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.multipleontype") // + .findAnnotation(ApplicationModule.class); + }); + } } diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index b7664d77..7d2087e4 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -114,7 +114,8 @@ In a fully-modularized application, using open application modules usually hints [[modules.explicit-dependencies]] === Explicit Application Module Dependencies -A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type. +A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the package, represented through the `package-info.java` file. +As, for example, Kotlin lacks support for that file, you can also use the annotation on a single type located in the application module's root package. .Inventory explicitly configuring module dependencies [tabs] @@ -132,10 +133,12 @@ Kotlin:: + [source, kotlin, role="secondary", chomp="none"] ---- -@org.springframework.modulith.ApplicationModule( - allowedDependencies = "order" -) package example.inventory + +import org.springframework.modulith.ApplicationModule + +@ApplicationModule(allowedDependencies = "order") +class ModuleMetadata {} ---- ====== @@ -207,7 +210,7 @@ Note how each module is listed, the contained Spring components are identified, By default and as described in xref:fundamentals.adoc#modules.advanced[Advanced Application Modules], an application module's base package is considered the API package and thus is the only package to allow incoming dependencies from other modules. In case you would like to expose additional packages to other modules, you need to use __named interfaces__. -You achieve that by annotating the `package-info.java` file of those packages with `@NamedInterface`. +You achieve that by annotating the `package-info.java` file of those packages with `@NamedInterface` or a type explicitly annotated with `@org.springframework.modulith.PackageInfo`. .A package arrangement to encapsulate an SPI named interface [source, text, subs="macros, quotes"] @@ -240,8 +243,14 @@ Kotlin:: + [source, kotlin, role="secondary", chomp="none"] ---- -@org.springframework.modulith.NamedInterface("spi") package example.order.spi + +import org.springframework.modulith.PackageInfo +import org.springframework.modulith.NamedInterface + +@PackageInfo +@NamedInterface("spi") +class ModuleMetadata {} ---- ====== The effect of that declaration is twofold: first, code in other application modules is allowed to refer to `SomeSpiInterface`.