GH-522 - Support for package info types.

Introduce @PackageInfo annotation to allow marking a types as alternative to Java's native package-info.java so that annotation lookups on a JavaPackage will also find annotations placed on that type. Useful to declare package scoped metadata like @ApplicationModule and @NamedInterface for languages that do not support packages well enough (read: Kotlin).
This commit is contained in:
Oliver Drotbohm
2024-03-05 08:35:45 +01:00
parent f3c111f769
commit 5f7e6a2479
21 changed files with 461 additions and 66 deletions

View File

@@ -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()

View File

@@ -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<org.jmolecules.ddd.annotation.Module> annotation;
private final Optional<Module> 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 <A extends Annotation> JMoleculesModule(AnnotationLookup lookup) {
this.annotation = lookup.lookup(Module.class);
}
/*
@@ -102,11 +109,11 @@ interface ApplicationModuleInformation {
public Optional<String> getDisplayName() {
Supplier<Optional<String>> 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<JavaClass> typeSelector) {
return new AnnotationLookup() {
@Override
public <A extends Annotation> Optional<A> lookup(Class<A> annotation) {
return javaPackage.findAnnotation(annotation);
}
};
}
<A extends Annotation> Optional<A> lookup(Class<A> annotation);
}
}

View File

@@ -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<JavaMethod> 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<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = Types
.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::test;
private static final Predicate<JavaMethod> 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);
}
}

View File

@@ -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;
}

View File

@@ -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<JavaClass> {
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<JavaClass> 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<JavaClass> {
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<JavaClass> {
*/
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<JavaClass> {
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<JavaClass> {
*/
public <A extends Annotation> Optional<A> getAnnotation(Class<A> 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 <A> 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 <A extends Annotation> Optional<A> findAnnotation(Class<A> 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()

View File

@@ -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<JavaClass> {
*/
static List<NamedInterface> 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)));

View File

@@ -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<CanBeAnnotated> isAnnotatedWith(Class<? extends Annotation> type) {
return isAnnotatedWith(type.getName());
}
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.metaAnnotatedWith(type);
}
static <T> DescribedPredicate<T> are(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> has(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> have(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> is(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> doNotHave(DescribedPredicate<T> predicate) {
return DescribedPredicate.not(predicate);
}
}

View File

@@ -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<JavaClass> isSpringDataRepository() {
return assignableTo(SpringDataTypes.REPOSITORY) //
return is(assignableTo(SpringDataTypes.REPOSITORY)) //
.or(isAnnotatedWith(SpringDataTypes.AT_REPOSITORY_DEFINITION));
}
}
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<? extends Annotation> type) {
return isAnnotatedWith(type.getName());
}
static DescribedPredicate<CanBeAnnotated> 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));
}
};
}

View File

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

View File

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

View File

@@ -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);
}