diff --git a/spring-modulith-api/src/main/java/org/springframework/modulith/NamedInterface.java b/spring-modulith-api/src/main/java/org/springframework/modulith/NamedInterface.java index 5341f7d9..8dab978f 100644 --- a/spring-modulith-api/src/main/java/org/springframework/modulith/NamedInterface.java +++ b/spring-modulith-api/src/main/java/org/springframework/modulith/NamedInterface.java @@ -24,8 +24,7 @@ import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; /** - * Annotation to mark a package as named interface of a {@link ApplicationModule} (either implicit or explicitly - * annotated). + * Annotation to mark a package as named interface of a {@link ApplicationModule} or assign a type to a named interface. * * @author Oliver Drotbohm */ @@ -35,19 +34,21 @@ import org.springframework.core.annotation.AliasFor; public @interface NamedInterface { /** - * The name(s) of the named interface. Declaring multiple values here is useful in case named interfaces are defined - * based on types and a particular type is supposed to be part of multiple named interfaces. + * The name(s) of the named interface. If declared on a package, the package's local name will be used as default + * name. Declaring multiple values here is useful in case named interfaces are defined based on types and a particular + * type is supposed to be part of multiple named interfaces. * - * @return + * @return will never be {@literal null}. */ @AliasFor("name") String[] value() default {}; /** - * The name(s) of the named interface. Declaring multiple values here is useful in case named interfaces are defined - * based on types and a particular type is supposed to be part of multiple named interfaces. + * The name(s) of the named interface. If declared on a package, the package's local name will be used as default + * name. Declaring multiple values here is useful in case named interfaces are defined based on types and a particular + * type is supposed to be part of multiple named interfaces. * - * @return + * @return will never be {@literal null}. */ @AliasFor("value") String[] name() 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 03708dc8..0ca9c9fe 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 @@ -32,7 +32,9 @@ import org.springframework.util.Assert; 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; import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier; import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers; @@ -121,7 +123,7 @@ public class JavaPackage implements DescribedIterable { * @return will never be {@literal null}. */ public String getLocalName() { - return name.substring(name.lastIndexOf(".") + 1); + return name.substring(name.lastIndexOf('.') + 1); } /** @@ -143,6 +145,17 @@ public class JavaPackage implements DescribedIterable { return packageClasses; } + /** + * Returns the classes exposed by this package, i.e. only public ones. Also filters the {@code package-info} type. + * + * @return will never be {@literal null}. + */ + public Classes getExposedClasses() { + + return packageClasses.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) // + .that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME))); + } + /** * Returns all sub-packages that carry the given annotation type. * 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 37ac57a4..36942267 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 @@ -15,17 +15,13 @@ */ package org.springframework.modulith.core; -import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.springframework.util.Assert; -import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClass.Predicates; -import com.tngtech.archunit.core.domain.JavaModifier; -import com.tngtech.archunit.core.domain.properties.HasModifiers; /** * A named interface into an {@link ApplicationModule}. This can either be a package, explicitly annotated with @@ -36,23 +32,24 @@ import com.tngtech.archunit.core.domain.properties.HasModifiers; * @author Oliver Drotbohm * @see org.springframework.modulith.ApplicationModule#allowedDependencies() */ -public abstract class NamedInterface implements Iterable { +public class NamedInterface implements Iterable { - private static final String UNNAMED_NAME = "<>"; - private static final String PACKAGE_INFO_NAME = "package-info"; + static final String UNNAMED_NAME = "<>"; - protected final String name; + private final String name; + private final Classes classes; /** * Creates a new {@link NamedInterface} with the given name. * * @param name must not be {@literal null} or empty. */ - protected NamedInterface(String name) { + private NamedInterface(String name, Classes classes) { Assert.hasText(name, "Name must not be null or empty!"); this.name = name; + this.classes = classes; } /** @@ -61,15 +58,17 @@ public abstract class NamedInterface implements Iterable { * @param javaPackage must not be {@literal null}. * @return will never be {@literal null}. */ - public static List of(JavaPackage javaPackage) { + static List of(JavaPackage javaPackage) { - String[] name = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) // - .map(it -> it.name()) // + var names = javaPackage.getAnnotation(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))); - return Arrays.stream(name) // - . map(it -> new PackageBasedNamedInterface(it, javaPackage)) // + var classes = javaPackage.toSingle().getExposedClasses(); + + return names.stream() + . map(it -> new NamedInterface(it, classes)) // .toList(); } @@ -81,8 +80,8 @@ public abstract class NamedInterface implements Iterable { * @param basePackage must not be {@literal null}. * @return will never be {@literal null}. */ - public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) { - return new TypeBasedNamedInterface(name, classes, basePackage); + static NamedInterface of(String name, Classes classes) { + return new NamedInterface(name, classes); } /** @@ -92,7 +91,7 @@ public abstract class NamedInterface implements Iterable { * @return will never be {@literal null}. */ static NamedInterface unnamed(JavaPackage javaPackage) { - return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage); + return new NamedInterface(UNNAMED_NAME, javaPackage.toSingle().getExposedClasses()); } /** @@ -120,7 +119,7 @@ public abstract class NamedInterface implements Iterable { Assert.notNull(type, "JavaClass must not be null!"); - return getClasses().contains(type); + return classes.contains(type); } /** @@ -132,7 +131,7 @@ public abstract class NamedInterface implements Iterable { Assert.notNull(type, "Type must not be null!"); - return !getClasses().that(Predicates.equivalentTo(type)).isEmpty(); + return !classes.that(Predicates.equivalentTo(type)).isEmpty(); } /** @@ -144,7 +143,7 @@ public abstract class NamedInterface implements Iterable { Assert.notNull(other, "NamedInterface must not be null!"); - return this.name.equals(other.name); + return name.equals(other.name); } /* @@ -153,128 +152,49 @@ public abstract class NamedInterface implements Iterable { */ @Override public Iterator iterator() { - return getClasses().iterator(); + return classes.iterator(); } - /** - * Returns all {@link Classes} making up this {@link NamedInterface}. - * - * @return will never be {@literal null}. - */ - protected abstract Classes getClasses(); - /** * Merges the current {@link NamedInterface} with the given {@link TypeBasedNamedInterface}. * * @param other must not be {@literal null}. * @return will never be {@literal null}. */ - public abstract NamedInterface merge(TypeBasedNamedInterface other); + NamedInterface merge(NamedInterface other) { - private static class PackageBasedNamedInterface extends NamedInterface { + Assert.isTrue(this.name.equals(other.name), + () -> "Named interfaces name must be equal to %s but was %s".formatted(name, other.name)); - private final Classes classes; - private final JavaPackage javaPackage; - - public PackageBasedNamedInterface(String name, JavaPackage pkg) { - - super(name); - - Assert.notNull(pkg, "Package must not be null!"); - Assert.hasText(name, "Package name must not be null or empty!"); - - this.classes = pkg.toSingle().getClasses() // - .that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) // - .that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME))); - - this.javaPackage = pkg; - } - - private PackageBasedNamedInterface(String name, Classes classes, JavaPackage pkg) { - - super(name); - this.classes = classes; - this.javaPackage = pkg; - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#getClasses() - */ - @Override - public Classes getClasses() { - return classes; - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface) - */ - @Override - public NamedInterface merge(TypeBasedNamedInterface other) { - return new PackageBasedNamedInterface(name, classes.and(other.classes), javaPackage); - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#toString() - */ - @Override - public String toString() { - return String.format("%s - Public types residing in %s:\n%s\n", name, javaPackage.getName(), - classes.format(javaPackage.getName())); - } + return new NamedInterface(name, classes.and(other.classes)); } - public static class TypeBasedNamedInterface extends NamedInterface { + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "NamedInterface: name=%s, types=%s".formatted(name, classes); + } - private final Classes classes; - private final JavaPackage pkg; + /** + * Returns the names declared in the given {@link org.springframework.modulith.NamedInterface} annotation or defaults + * to the local name of the given package if none declared. + * + * @param annotation must not be {@literal null}. + * @param packageName must not be {@literal null}. + * @return will never be {@literal null}. + */ + static List getDefaultedNames(org.springframework.modulith.NamedInterface annotation, String packageName) { - /** - * Creates a new {@link TypeBasedNamedInterface} with the given name, {@link Classes} and {@link JavaPackage}. - * - * @param name must not be {@literal null} or empty. - * @param types must not be {@literal null}. - * @param pkg must not be {@literal null}. - */ - public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) { + Assert.notNull(annotation, "NamedInterface must not be null!"); + Assert.hasText(packageName, "Package name must not be null or empty!"); - super(name); + var declaredNames = annotation.name(); - Assert.notNull(types, "Classes must not be null!"); - Assert.notNull(pkg, "JavaPackage must not be null!"); - - this.classes = types; - this.pkg = pkg; - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#getClasses() - */ - @Override - public Classes getClasses() { - return classes; - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#merge(org.springframework.modulith.model.NamedInterface.TypeBasedNamedInterface) - */ - @Override - public NamedInterface merge(TypeBasedNamedInterface other) { - return new TypeBasedNamedInterface(name, classes.and(other.classes), pkg); - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.NamedInterface#toString() - */ - @Override - public String toString() { - return String.format("%s - Types underneath base package %s:\n%s\n", name, pkg.getName(), - classes.format(pkg.getName())); - } + return declaredNames.length == 0 + ? List.of(packageName.substring(packageName.lastIndexOf('.') + 1)) + : List.of(declaredNames); } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java index f644e869..da374037 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java @@ -24,7 +24,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.modulith.core.NamedInterface.TypeBasedNamedInterface; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -151,7 +150,7 @@ public class NamedInterfaces implements Iterable { * @param others must not be {@literal null}. * @return will never be {@literal null}. */ - NamedInterfaces and(List others) { + NamedInterfaces and(List others) { Assert.notNull(others, "Other TypeBasedNamedInterfaces must not be null!"); @@ -162,23 +161,18 @@ public class NamedInterfaces implements Iterable { return this; } - for (TypeBasedNamedInterface candidate : others) { + for (NamedInterface candidate : others) { - var existing = namedInterfaces.stream() // - .filter(it -> it.hasSameNameAs(candidate)) // + var existing = this.namedInterfaces.stream() // + .filter(candidate::hasSameNameAs) // .findFirst(); // Merge existing with new and add to result - existing.ifPresent(it -> { + existing.ifPresentOrElse(it -> { namedInterfaces.add(it.merge(candidate)); - namedInterfaces.add(it); unmergedInterface.remove(it); - }); - - // Simply add candidate - if (!existing.isPresent()) { - namedInterfaces.add(candidate); - } + }, + () -> namedInterfaces.add(candidate)); } namedInterfaces.addAll(unmergedInterface); @@ -195,7 +189,7 @@ public class NamedInterfaces implements Iterable { return new NamedInterfaces(result); } - private static List ofAnnotatedTypes(JavaPackage basePackage) { + private static List ofAnnotatedTypes(JavaPackage basePackage) { var mappings = new LinkedMultiValueMap(); @@ -210,13 +204,12 @@ public class NamedInterfaces implements Iterable { var annotation = AnnotatedElementUtils.getMergedAnnotation(it.reflect(), org.springframework.modulith.NamedInterface.class); - for (String name : annotation.name()) { - mappings.add(name, it); - } + NamedInterface.getDefaultedNames(annotation, it.getPackageName()) + .forEach(name -> mappings.add(name, it)); }); return mappings.entrySet().stream() // - .map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) // + .map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()))) // .toList(); } } diff --git a/spring-modulith-core/src/test/java/example/ni/RootType.java b/spring-modulith-core/src/test/java/example/ni/RootType.java new file mode 100644 index 00000000..577ce0f0 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/RootType.java @@ -0,0 +1,21 @@ +/* + * 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.ni; + +/** + * @author Oliver Drotbohm + */ +public interface RootType {} diff --git a/spring-modulith-core/src/test/java/example/ni/api/ApiType.java b/spring-modulith-core/src/test/java/example/ni/api/ApiType.java new file mode 100644 index 00000000..b7aeb921 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/api/ApiType.java @@ -0,0 +1,21 @@ +/* + * 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.ni.api; + +/** + * @author Oliver Drotbohm + */ +public interface ApiType {} diff --git a/spring-modulith-core/src/test/java/example/ni/api/package-info.java b/spring-modulith-core/src/test/java/example/ni/api/package-info.java new file mode 100644 index 00000000..3d478dac --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/api/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.NamedInterface +package example.ni.api; diff --git a/spring-modulith-core/src/test/java/example/ni/internal/AdditionalSpiType.java b/spring-modulith-core/src/test/java/example/ni/internal/AdditionalSpiType.java new file mode 100644 index 00000000..e6cf18ee --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/internal/AdditionalSpiType.java @@ -0,0 +1,24 @@ +/* + * 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.ni.internal; + +import org.springframework.modulith.NamedInterface; + +/** + * @author Oliver Drotbohm + */ +@NamedInterface({ "spi", "kpi" }) +public interface AdditionalSpiType {} diff --git a/spring-modulith-core/src/test/java/example/ni/internal/DefaultedNamedInterfaceType.java b/spring-modulith-core/src/test/java/example/ni/internal/DefaultedNamedInterfaceType.java new file mode 100644 index 00000000..a65fee93 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/internal/DefaultedNamedInterfaceType.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.ni.internal; + +import org.springframework.modulith.NamedInterface; + +/** + * @author Oliver Drotbohm + */ +@NamedInterface +public interface DefaultedNamedInterfaceType { + +} diff --git a/spring-modulith-core/src/test/java/example/ni/spi/SpiType.java b/spring-modulith-core/src/test/java/example/ni/spi/SpiType.java new file mode 100644 index 00000000..0339d3f6 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/spi/SpiType.java @@ -0,0 +1,21 @@ +/* + * 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.ni.spi; + +/** + * @author Oliver Drotbohm + */ +public interface SpiType {} diff --git a/spring-modulith-core/src/test/java/example/ni/spi/package-info.java b/spring-modulith-core/src/test/java/example/ni/spi/package-info.java new file mode 100644 index 00000000..b04925f7 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/spi/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.NamedInterface +package example.ni.spi; 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 new file mode 100644 index 00000000..a6b30475 --- /dev/null +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java @@ -0,0 +1,65 @@ +/* + * 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 org.springframework.modulith.core; + +import static org.assertj.core.api.Assertions.*; + +import example.ni.RootType; +import example.ni.api.ApiType; +import example.ni.internal.AdditionalSpiType; +import example.ni.internal.DefaultedNamedInterfaceType; +import example.ni.spi.SpiType; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import com.tngtech.archunit.core.domain.JavaClass; + +/** + * Unit tests for {@link NamedInterfaces}. + * + * @author Oliver Drotbohm + */ +class NamedInterfacesUnitTests { + + @Test + void discoversNamedInterfaces() { + + var classes = TestUtils.getClasses(RootType.class); + var javaPackage = JavaPackage.of(classes, RootType.class.getPackageName()); + + var interfaces = NamedInterfaces.discoverNamedInterfaces(javaPackage); + + assertThat(interfaces).map(NamedInterface::getName) + .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal"); + + assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class); + assertInterfaceContains(interfaces, "api", ApiType.class); + assertInterfaceContains(interfaces, "spi", SpiType.class, AdditionalSpiType.class); + assertInterfaceContains(interfaces, "kpi", AdditionalSpiType.class); + assertInterfaceContains(interfaces, "internal", DefaultedNamedInterfaceType.class); + } + + private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class... types) { + + var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new); + + assertThat(interfaces.getByName(name)).hasValueSatisfying(it -> { + assertThat(it).map(JavaClass::getName).containsExactlyInAnyOrder(classNames); + }); + } +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java index 32e0ce38..f1d813e6 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java @@ -66,6 +66,8 @@ class TestUtils { Assert.notNull(packageType, "Package type must not be null!"); - return getClasses().that(resideInAPackage(packageType.getPackage().getName())); + return Classes.of(new ClassFileImporter() + .importPackagesOf(packageType) + .that(resideInAPackage(packageType.getPackage().getName() + ".."))); } }