GH-1 - Initial port of Moduliths project.

Basically the state of commit c7cf939 of https://github.com/moduliths/moduliths for further development under the Spring umbrella.
This commit is contained in:
Oliver Drotbohm
2022-07-06 12:58:50 +02:00
commit 6c444769d7
236 changed files with 17771 additions and 0 deletions

83
moduliths-core/pom.xml Normal file
View File

@@ -0,0 +1,83 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.moduliths</groupId>
<artifactId>moduliths</artifactId>
<version>1.4.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Moduliths - Core</name>
<artifactId>moduliths-core</artifactId>
<properties>
<module.name>org.moduliths.core</module.name>
</properties>
<dependencies>
<dependency>
<groupId>org.moduliths</groupId>
<artifactId>moduliths-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>${archunit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<optional>true</optional>
</dependency>
<!-- jMolecules -->
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-events</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-archunit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2019-2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.moduliths.Modulithic;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link ModulithMetadata} backed by a {@link Modulithic} annotated type.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class AnnotationModulithMetadata implements ModulithMetadata {
private final Class<?> modulithType;
private final Modulithic annotation;
/**
* Creates a {@link ModulithMetadata} inspecting {@link Modulithic} annotation or return {@link Optional#empty()} if
* the type given does not carry the annotation.
*
* @param annotated must not be {@literal null}.
* @return
*/
public static Optional<ModulithMetadata> of(Class<?> annotated) {
Assert.notNull(annotated, "Modulith type must not be null!");
Modulithic annotation = AnnotatedElementUtils.findMergedAnnotation(annotated, Modulithic.class);
return Optional.ofNullable(annotation) //
.map(it -> new AnnotationModulithMetadata(annotated, it));
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getModulithSource() {
return modulithType;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getAdditionalPackages()
*/
@Override
public List<String> getAdditionalPackages() {
return Arrays.asList(annotation.additionalPackages());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#useFullyQualifiedModuleNames()
*/
@Override
public boolean useFullyQualifiedModuleNames() {
return annotation.useFullyQualifiedModuleNames();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getSharedModuleNames()
*/
@Override
public Stream<String> getSharedModuleNames() {
return Arrays.stream(annotation.sharedModules());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getSystemName()
*/
@Override
public Optional<String> getSystemName() {
return Optional.of(annotation.systemName()) //
.filter(StringUtils::hasText);
}
}

View File

@@ -0,0 +1,582 @@
/*
* Copyright 2020 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.moduliths.model;
import static org.moduliths.model.Types.JavaXTypes.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.moduliths.model.Types.JMoleculesTypes;
import org.moduliths.model.Types.SpringDataTypes;
import org.moduliths.model.Types.SpringTypes;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaType;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* A type that is architecturally relevant, i.e. it fulfills a significant role within the architecture.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class ArchitecturallyEvidentType {
private static Map<Key, ArchitecturallyEvidentType> CACHE = new HashMap<>();
private final @Getter JavaClass type;
/**
* Creates a new {@link AbstractArchitecturallyEvidentType} for the given {@link JavaType} and {@link Classes} of
* Spring components.
*
* @param type must not be {@literal null}.
* @param beanTypes must not be {@literal null}.
* @return
*/
public static ArchitecturallyEvidentType of(JavaClass type, Classes beanTypes) {
return CACHE.computeIfAbsent(Key.of(type, beanTypes), it -> {
List<ArchitecturallyEvidentType> delegates = new ArrayList<>();
if (JMoleculesTypes.isPresent()) {
delegates.add(new JMoleculesArchitecturallyEvidentType(type));
}
if (SpringDataTypes.isPresent()) {
delegates.add(new SpringDataAwareArchitecturallyEvidentType(type, beanTypes));
}
delegates.add(new SpringAwareArchitecturallyEvidentType(type));
return DelegatingType.of(type, delegates);
});
}
/**
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name.
*
* @return will never be {@literal null}.
*/
String getAbbreviatedFullName() {
return FormatableJavaClass.of(getType()).getAbbreviatedFullName();
}
/**
* Returns whether the type is an entity in the DDD sense.
*
* @return
*/
boolean isEntity() {
return isJpaEntity().apply(getType());
}
/**
* Returns whether the type is considered an aggregate root in the DDD sense.
*
* @return
*/
public abstract boolean isAggregateRoot();
/**
* Returns whether the type is considered a repository in the DDD sense.
*
* @return
*/
public abstract boolean isRepository();
public boolean isService() {
return false;
}
public boolean isController() {
return false;
}
public boolean isEventListener() {
return false;
}
public boolean isConfigurationProperties() {
return false;
}
/**
* Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For
* example, for an event listener this might be the event types the particular listener is interested in.
*
* @return
*/
public Stream<JavaClass> getReferenceTypes() {
return Stream.empty();
}
public Stream<ReferenceMethod> getReferenceMethods() {
return Stream.empty();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return type.getFullName();
}
private static Stream<JavaClass> distinctByName(Stream<JavaClass> types) {
Set<String> names = new HashSet<>();
return types.flatMap(it -> {
if (names.contains(it.getFullName())) {
return Stream.empty();
} else {
names.add(it.getFullName());
return Stream.of(it);
}
});
}
static class SpringAwareArchitecturallyEvidentType extends ArchitecturallyEvidentType {
/**
* Methods (meta-)annotated with @EventListener.
*/
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = it -> //
Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).apply(it) //
|| Types.isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).apply(it);
/**
* {@code ApplicationListener.onApplicationEvent(…)}
*/
private static final Predicate<JavaMethod> IS_IMPLEMENTING_EVENT_LISTENER = it -> //
it.getOwner().isAssignableTo(SpringTypes.APPLICATION_LISTENER) //
&& it.getName().equals("onApplicationEvent") //
&& !it.reflect().isSynthetic();
private static final Predicate<JavaMethod> IS_EVENT_LISTENER = IS_ANNOTATED_EVENT_LISTENER
.or(IS_IMPLEMENTING_EVENT_LISTENER);
public SpringAwareArchitecturallyEvidentType(JavaClass type) {
super(type);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
*/
@Override
public boolean isAggregateRoot() {
return false;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
*/
@Override
public boolean isRepository() {
return Types.isAnnotatedWith(SpringTypes.AT_REPOSITORY).apply(getType());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
*/
@Override
public boolean isService() {
return Types.isAnnotatedWith(SpringTypes.AT_SERVICE).apply(getType());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
*/
@Override
public boolean isController() {
return Types.isAnnotatedWith(SpringTypes.AT_CONTROLLER).apply(getType());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
*/
@Override
public boolean isEventListener() {
return getType().getMethods().stream().anyMatch(IS_EVENT_LISTENER);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isConfigurationProperties()
*/
@Override
public boolean isConfigurationProperties() {
return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).apply(getType());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#getOtherTypeReferences()
*/
@Override
public Stream<JavaClass> getReferenceTypes() {
if (isEventListener()) {
return distinctByName(getType().getMethods().stream() //
.filter(IS_EVENT_LISTENER) //
.flatMap(it -> it.getRawParameterTypes().stream()))
.sorted(Comparator.comparing(JavaClass::getSimpleName));
}
return super.getReferenceTypes();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#getReferenceMethods()
*/
@Override
public Stream<ReferenceMethod> getReferenceMethods() {
if (!isEventListener()) {
return super.getReferenceMethods();
}
return getType().getMethods().stream() //
.filter(IS_EVENT_LISTENER)
.sorted(Comparator.comparing(JavaMethod::getName)
.thenComparing(it -> it.getRawParameterTypes().size()))
.map(ReferenceMethod::new);
}
}
static class SpringDataAwareArchitecturallyEvidentType extends ArchitecturallyEvidentType {
private final Classes beanTypes;
SpringDataAwareArchitecturallyEvidentType(JavaClass type, Classes beanTypes) {
super(type);
this.beanTypes = beanTypes;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
*/
@Override
public boolean isEntity() {
return super.isEntity() //
|| getType().isAnnotatedWith("org.springframework.data.mongodb.core.mapping.Document");
}
/*
* (non-Javadoc)
* @see org.moduliths.model.DefaultArchitectuallyEvidentType#isAggregateRoot(org.moduliths.model.Classes)
*/
@Override
public boolean isAggregateRoot() {
return isEntity() && beanTypes.that(SpringDataTypes.isSpringDataRepository()).stream() //
.map(JavaClass::reflect) //
.map(AbstractRepositoryMetadata::getMetadata) //
.map(RepositoryMetadata::getDomainType) //
.anyMatch(it -> getType().isAssignableTo(it));
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
*/
@Override
public boolean isRepository() {
return SpringDataTypes.isSpringDataRepository().apply(getType());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
*/
@Override
public boolean isController() {
return Types.isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController").apply(getType());
}
}
static class JMoleculesArchitecturallyEvidentType extends ArchitecturallyEvidentType {
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = Types
.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::apply;
JMoleculesArchitecturallyEvidentType(JavaClass type) {
super(type);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
*/
@Override
public boolean isEntity() {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).apply(type) || //
type.isAssignableTo(org.jmolecules.ddd.types.Entity.class);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
*/
@Override
public boolean isAggregateRoot() {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).apply(type) //
|| type.isAssignableTo(org.jmolecules.ddd.types.AggregateRoot.class);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
*/
@Override
public boolean isRepository() {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).apply(type)
|| type.isAssignableTo(org.jmolecules.ddd.types.Repository.class);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
*/
@Override
public boolean isService() {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).apply(type);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
*/
@Override
public boolean isEventListener() {
return getType().getMethods().stream().anyMatch(IS_ANNOTATED_EVENT_LISTENER);
}
}
static class DelegatingType extends ArchitecturallyEvidentType {
private final Supplier<Boolean> isAggregateRoot, isRepository, isEntity, isService, isController, isEventListener,
isConfigurationProperties;
private final Supplier<Collection<JavaClass>> referenceTypes;
private final Supplier<Collection<ReferenceMethod>> referenceMethods;
DelegatingType(JavaClass type, Supplier<Boolean> isAggregateRoot,
Supplier<Boolean> isRepository, Supplier<Boolean> isEntity, Supplier<Boolean> isService,
Supplier<Boolean> isController, Supplier<Boolean> isEventListener, Supplier<Boolean> isConfigurationProperties,
Supplier<Collection<JavaClass>> referenceTypes, Supplier<Collection<ReferenceMethod>> referenceMethods) {
super(type);
this.isAggregateRoot = isAggregateRoot;
this.isRepository = isRepository;
this.isEntity = isEntity;
this.isService = isService;
this.isController = isController;
this.isEventListener = isEventListener;
this.isConfigurationProperties = isConfigurationProperties;
this.referenceTypes = referenceTypes;
this.referenceMethods = referenceMethods;
}
public static DelegatingType of(JavaClass type, List<ArchitecturallyEvidentType> types) {
Supplier<Boolean> isAggregateRoot = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isAggregateRoot));
Supplier<Boolean> isRepository = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isRepository));
Supplier<Boolean> isEntity = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEntity));
Supplier<Boolean> isService = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isService));
Supplier<Boolean> isController = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isController));
Supplier<Boolean> isEventListener = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEventListener));
Supplier<Boolean> isConfigurationProperties = Suppliers
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isConfigurationProperties));
Supplier<Collection<JavaClass>> referenceTypes = Suppliers.memoize(() -> types.stream() //
.flatMap(ArchitecturallyEvidentType::getReferenceTypes) //
.collect(Collectors.toList()));
Supplier<Collection<ReferenceMethod>> referenceMethods = Suppliers.memoize(() -> types.stream() //
.flatMap(ArchitecturallyEvidentType::getReferenceMethods) //
.collect(Collectors.toList()));
return new DelegatingType(type, isAggregateRoot, isRepository, isEntity, isService, isController,
isEventListener, isConfigurationProperties, referenceTypes, referenceMethods);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
*/
// @Override
@Override
public boolean isAggregateRoot() {
return isAggregateRoot.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
*/
// @Override
@Override
public boolean isRepository() {
return isRepository.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
*/
@Override
public boolean isEntity() {
return isEntity.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
*/
@Override
public boolean isService() {
return isService.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
*/
@Override
public boolean isController() {
return isController.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
*/
@Override
public boolean isEventListener() {
return isEventListener.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#isConfigurationProperties()
*/
@Override
public boolean isConfigurationProperties() {
return isConfigurationProperties.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#getOtherTypeReferences()
*/
@Override
public Stream<JavaClass> getReferenceTypes() {
return distinctByName(referenceTypes.get().stream());
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ArchitecturallyEvidentType#getReferenceMethods()
*/
@Override
public Stream<ReferenceMethod> getReferenceMethods() {
return referenceMethods.get().stream();
}
}
@Value(staticConstructor = "of")
private static class Key {
JavaClass type;
Classes beanTypes;
}
@Value
public final class ReferenceMethod {
private final JavaMethod method;
public boolean isAsync() {
return method.isAnnotatedWith(SpringTypes.AT_ASYNC) || method.isMetaAnnotatedWith(SpringTypes.AT_ASYNC);
}
public Optional<String> getTransactionPhase() {
return Optional.ofNullable(method.getAnnotationOfType(SpringTypes.AT_TX_EVENT_LISTENER))
.map(it -> it.get("phase"))
.map(Object::toString);
}
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2018-2021 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
*
* http://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.moduliths.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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.JavaClasses;
import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.domain.JavaType;
import com.tngtech.archunit.core.domain.properties.HasName;
/**
* @author Oliver Gierke
*/
@ToString
@EqualsAndHashCode
public class Classes implements DescribedIterable<JavaClass> {
private final List<JavaClass> classes;
/**
* Creates a new {@link Classes} for the given {@link JavaClass}es.
*
* @param classes must not be {@literal null}.
*/
private Classes(List<JavaClass> classes) {
Assert.notNull(classes, "JavaClasses must not be null!");
this.classes = classes.stream() //
.sorted(Comparator.comparing(JavaClass::getName)) //
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
}
/**
* Creates a new {@link Classes} for the given {@link JavaClass}es.
*
* @param classes must not be {@literal null}.
* @return
*/
static Classes of(JavaClasses classes) {
return new Classes(StreamSupport.stream(classes.spliterator(), false) //
.collect(Collectors.toList()));
}
/**
* Creates a new {@link Classes} for the given {@link JavaClass}es.
*
* @param classes must not be {@literal null}.
* @return will never be {@literal null}.
*/
static Classes of(List<JavaClass> classes) {
return new Classes(classes);
}
/**
* Returns a {@link Collector} creating a {@link Classes} instance from a {@link Stream} of {@link JavaType}.
*
* @return will never be {@literal null}.
*/
static Collector<JavaClass, ?, Classes> toClasses() {
return Collectors.collectingAndThen(Collectors.toList(), Classes::of);
}
/**
* Returns {@link Classes} that match the given {@link DescribedPredicate}.
*
* @param predicate must not be {@literal null}.
* @return
*/
Classes that(DescribedPredicate<? super JavaClass> predicate) {
Assert.notNull(predicate, "Predicate must not be null!");
return classes.stream() //
.filter((Predicate<JavaClass>) it -> predicate.apply(it)) //
.collect(Collectors.collectingAndThen(Collectors.toList(), Classes::new));
}
Classes and(Classes classes) {
return and(classes.classes);
}
/**
* Returns a Classes with the current elements and the given other ones combined.
*
* @param others must not be {@literal null}.
* @return
*/
Classes and(Collection<JavaClass> others) {
Assert.notNull(others, "JavaClasses must not be null!");
if (others.isEmpty()) {
return this;
}
List<JavaClass> result = new ArrayList<>(classes);
others.forEach(it -> {
if (!result.contains(it)) {
result.add(it);
}
});
return new Classes(result);
}
public Stream<JavaClass> stream() {
return classes.stream();
}
boolean isEmpty() {
return !classes.iterator().hasNext();
}
Optional<JavaClass> toOptional() {
return isEmpty() ? Optional.empty() : Optional.of(classes.iterator().next());
}
boolean contains(JavaClass type) {
return !that(new SameClass(type)).isEmpty();
}
boolean contains(String className) {
return !that(HasName.Predicates.name(className)).isEmpty();
}
JavaClass getRequiredClass(Class<?> type) {
return classes.stream() //
.filter(it -> it.isEquivalentTo(type)) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(String.format("No JavaClass found for type %s!", type)));
}
/*
* (non-Javadoc)
* @see com.tngtech.archunit.base.HasDescription#getDescription()
*/
@Override
public String getDescription() {
return "";
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<JavaClass> iterator() {
return classes.iterator();
}
String format() {
return classes.stream() //
.map(Classes::format) //
.collect(Collectors.joining("\n"));
}
String format(String basePackage) {
return classes.stream() //
.map(it -> Classes.format(it, basePackage)) //
.collect(Collectors.joining("\n"));
}
private static String format(JavaClass type) {
return format(type, "");
}
static String format(JavaClass type, String basePackage) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(basePackage, "Base package must not be null!");
String prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
String name = StringUtils.hasText(basePackage) //
? type.getName().replace(basePackage, "") //
: type.getName();
return String.format(" %s %s", prefix, name);
}
private static class SameClass extends DescribedPredicate<JavaClass> {
private final JavaClass reference;
public SameClass(JavaClass reference) {
super(" is the same class as ");
this.reference = reference;
}
/*
* (non-Javadoc)
* @see com.tngtech.archunit.base.DescribedPredicate#apply(java.lang.Object)
*/
@Override
public boolean apply(@Nullable JavaClass input) {
return input != null && reference.getName().equals(input.getName());
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2019-2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.moduliths.Modulith;
import org.moduliths.Modulithic;
import org.moduliths.model.Types.SpringTypes;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
/**
* Creates a new {@link ModulithMetadata} representing the defaults of {@link Modulithic} but without the annotation
* present.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class DefaultModulithMetadata implements ModulithMetadata {
private static final Class<? extends Annotation> AT_SPRING_BOOT_APPLICATION = Types
.loadIfPresent(SpringTypes.AT_SPRING_BOOT_APPLICATION);
private final @NonNull Object modulithSource;
/**
* Creates a new {@link ModulithMetadata} representing the defaults of a class annotated but not customized with
* {@link Modulithic} or {@link Modulith}.
*
* @param annotated must not be {@literal null}.
* @return
*/
public static Optional<ModulithMetadata> of(Class<?> annotated) {
Assert.notNull(annotated, "Annotated type must not be null!");
return Optional.ofNullable(AT_SPRING_BOOT_APPLICATION) //
.filter(it -> AnnotatedElementUtils.hasAnnotation(annotated, it)) //
.map(__ -> new DefaultModulithMetadata(annotated));
}
/**
* Creates a new {@link ModulithMetadata} from the given package name.
*
* @param javaPackage must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.1
*/
public static ModulithMetadata of(String javaPackage) {
Assert.hasText(javaPackage, "Package name must not be null or empty!");
return new DefaultModulithMetadata(javaPackage);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getModulithSource()
*/
@Override
public Object getModulithSource() {
return modulithSource;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getAdditionalPackages()
*/
@Override
public List<String> getAdditionalPackages() {
return Collections.emptyList();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#useFullyQualifiedModuleNames()
*/
@Override
public boolean useFullyQualifiedModuleNames() {
return false;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getSharedModuleNames()
*/
@Override
public Stream<String> getSharedModuleNames() {
return Stream.empty();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModulithMetadata#getSystemName()
*/
@Override
public Optional<String> getSystemName() {
return Optional.empty();
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2020 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.moduliths.model;
import lombok.Value;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.Assert;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaModifier;
/**
* A type that represents an event in a system.
*
* @author Oliver Drotbohm
* @since 1.1
*/
@Value
public class EventType {
private final JavaClass type;
/**
* The sources that create that event. Includes static factory methods that return an instance of the event type
* itself as well as constructor invocations, except ones from the factory methods.
*/
private final List<Source> sources;
/**
* Creates a new {@link EventType} for the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
*/
public EventType(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
this.type = type;
Stream<JavaAccess<?>> factoryMethodCalls = type.getMethods().stream()
.filter(method -> method.getModifiers().contains(JavaModifier.STATIC))
.filter(method -> method.getRawReturnType().equals(type))
.flatMap(method -> method.getCallsOfSelf().stream());
Stream<JavaAccess<?>> constructorCalls = type.getConstructors().stream()
.flatMap(constructor -> constructor.getCallsOfSelf().stream());
this.sources = Stream.concat(constructorCalls, factoryMethodCalls)
.filter(call -> !call.getOriginOwner().equals(type))
.map(JavaAccessSource::new)
.collect(Collectors.toList());
}
public boolean hasSources() {
return !this.sources.isEmpty();
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* Wrapper around {@link JavaClass} that allows creating additional formatted names.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class FormatableJavaClass {
private static final Map<JavaClass, FormatableJavaClass> CACHE = new ConcurrentHashMap<>();
private final JavaClass type;
private final Supplier<String> abbreviatedName;
public static FormatableJavaClass of(JavaClass type) {
return CACHE.computeIfAbsent(type, FormatableJavaClass::new);
}
private FormatableJavaClass(JavaClass type) {
Assert.notNull(type, "JavaClass must not be null!");
this.type = type;
this.abbreviatedName = Suppliers.memoize(() -> {
String abbreviatedPackage = Stream //
.of(type.getPackageName().split("\\.")) //
.map(it -> it.substring(0, 1)) //
.collect(Collectors.joining("."));
return abbreviatedPackage.concat(".") //
.concat(ClassUtils.getShortName(getFullName()));
});
}
/**
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name, e.g.
* {@code com.acme.MyType} will result in {@code c.a.MyType}.
*
* @return will never be {@literal null}.
*/
public String getAbbreviatedFullName() {
return abbreviatedName.get();
}
public String getAbbreviatedFullName(@Nullable Module module) {
if (module == null) {
return getAbbreviatedFullName();
}
String basePackageName = module.getBasePackage().getName();
if (!StringUtils.hasText(basePackageName)) {
return getAbbreviatedFullName();
}
String typePackageName = type.getPackageName();
if (basePackageName.equals(typePackageName)) {
return getAbbreviatedFullName();
}
if (!typePackageName.startsWith(basePackageName)) {
return getFullName();
}
return abbreviate(basePackageName) //
.concat(typePackageName.substring(basePackageName.length())) //
.concat(".") //
.concat(ClassUtils.getShortName(getFullName()));
}
/**
* Returns the type's full name.
*
* @return will never be {@literal null}.
*/
public String getFullName() {
return type.getName().replace("$", ".");
}
private static String abbreviate(String source) {
return Stream //
.of(source.split("\\.")) //
.map(it -> it.substring(0, 1)) //
.collect(Collectors.joining("."));
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2020 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.moduliths.model;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
/**
* A {@link Source} backed by an ArchUnit {@link JavaAccess}.
*
* @author Oliver Drotbohm
* @since 1.1
*/
class JavaAccessSource implements Source {
private final static Pattern LAMBDA_EXTRACTOR = Pattern.compile("lambda\\$(.*)\\$.*");
private final FormatableJavaClass type;
private final JavaCodeUnit method;
private final String name;
/**
* Creates a new {@link JavaAccessSource} for the given {@link JavaAccess}.
*
* @param access must not be {@literal null}.
*/
public JavaAccessSource(JavaAccess<?> access) {
this.type = FormatableJavaClass.of(access.getOriginOwner());
this.method = access.getOrigin();
String name = method.getName();
Matcher matcher = LAMBDA_EXTRACTOR.matcher(name);
this.name = matcher.matches() ? matcher.group(1) : name;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.Source#toString(org.moduliths.model.Module)
*/
@Override
public String toString(Module module) {
boolean noParameters = method.getRawParameterTypes().isEmpty();
return String.format("%s.%s(%s)", type.getAbbreviatedFullName(module), name, noParameters ? "" : "");
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2018 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
*
* http://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.moduliths.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.properties.CanBeAnnotated;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* @author Oliver Gierke
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JavaPackage implements DescribedIterable<JavaClass> {
private static final String PACKAGE_INFO_NAME = "package-info";
private final @Getter String name;
private final Classes classes;
private final Classes packageClasses;
private final Supplier<Set<JavaPackage>> directSubPackages;
private JavaPackage(Classes classes, String name, boolean includeSubPackages) {
this.classes = classes;
this.packageClasses = classes.that(resideInAPackage(includeSubPackages ? name.concat("..") : name));
this.name = name;
this.directSubPackages = Suppliers.memoize(() -> packageClasses.stream() //
.map(it -> it.getPackageName()) //
.filter(it -> !it.equals(name)) //
.map(it -> extractDirectSubPackage(it)) //
.distinct() //
.map(it -> of(classes, it)) //
.collect(Collectors.toSet()));
}
public static JavaPackage of(Classes classes, String name) {
return new JavaPackage(classes, name, true);
}
public static boolean isPackageInfoType(JavaClass type) {
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
}
public JavaPackage toSingle() {
return new JavaPackage(classes, name, false);
}
public String getLocalName() {
return name.substring(name.lastIndexOf(".") + 1);
}
public Collection<JavaPackage> getDirectSubPackages() {
return directSubPackages.get();
}
/**
* Returns all classes residing in the current package and potentially in sub-packages if the current package was
* created to include them.
*
* @return
*/
public Classes getClasses() {
return packageClasses;
}
/**
* Extract the direct sub-package name of the given candidate.
*
* @param candidate
* @return
*/
private String extractDirectSubPackage(String candidate) {
if (candidate.length() <= name.length()) {
return candidate;
}
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
return candidate.substring(0, endIndex);
}
public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotation> annotation) {
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
.map(JavaClass::getPackageName) //
.distinct() //
.map(it -> of(classes, it));
}
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
return packageClasses.that(predicate);
}
public boolean contains(JavaClass type) {
return packageClasses.contains(type);
}
public boolean contains(String className) {
return packageClasses.contains(className);
}
public Stream<JavaClass> stream() {
return packageClasses.stream();
}
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotationType))) //
.toOptional() //
.map(it -> it.getAnnotationOfType(annotationType));
}
/*
* (non-Javadoc)
* @see com.tngtech.archunit.base.HasDescription#getDescription()
*/
@Override
public String getDescription() {
return classes.getDescription();
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<JavaClass> iterator() {
return classes.iterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return new StringBuilder(name) //
.append("\n") //
.append(getClasses().format(name)) //
.append('\n') //
.toString();
}
}

View File

@@ -0,0 +1,833 @@
/*
* Copyright 2018-2020 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
*
* http://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.moduliths.model;
import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static java.lang.System.*;
import static org.moduliths.model.Classes.*;
import static org.moduliths.model.Types.*;
import static org.moduliths.model.Types.JavaXTypes.*;
import static org.moduliths.model.Types.SpringDataTypes.*;
import static org.moduliths.model.Types.SpringTypes.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.moduliths.model.Types.JMoleculesTypes;
import org.moduliths.model.Types.SpringTypes;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
import com.tngtech.archunit.core.domain.JavaConstructor;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaMember;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.SourceCodeLocation;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* @author Oliver Gierke
*/
@EqualsAndHashCode(doNotUseGetters = true)
public class Module {
private final @Getter JavaPackage basePackage;
private final ModuleInformation information;
private final @Getter NamedInterfaces namedInterfaces;
private final boolean useFullyQualifiedModuleNames;
private final Supplier<Classes> springBeans;
private final Supplier<Classes> entities;
private final Supplier<List<EventType>> publishedEvents;
Module(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
this.basePackage = basePackage;
this.information = ModuleInformation.of(basePackage);
this.namedInterfaces = NamedInterfaces.discoverNamedInterfaces(basePackage);
this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames;
this.springBeans = Suppliers.memoize(() -> filterSpringBeans(basePackage));
this.entities = Suppliers.memoize(() -> findEntities(basePackage));
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
}
public String getName() {
return useFullyQualifiedModuleNames ? basePackage.getName() : basePackage.getLocalName();
}
public String getDisplayName() {
return information.getDisplayName();
}
public List<Module> getDependencies(Modules modules, DependencyType... type) {
return getAllModuleDependencies(modules) //
.filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) //
.map(it -> modules.getModuleByType(it.target)) //
.distinct() //
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)) //
.collect(Collectors.toList());
}
/**
* Returns all event types the current module exposes an event listener for.
*
* @param modules must not be {@literal null}.
* @return
*/
public List<JavaClass> getEventsListenedTo(Modules modules) {
Assert.notNull(modules, "Modules must not be null!");
return getAllModuleDependencies(modules) //
.filter(it -> it.type == DependencyType.EVENT_LISTENER) //
.map(ModuleDependency::getTarget) //
.collect(Collectors.toList());
}
/**
* Returns all {@link EventType}s published by the module.
*
* @return will never be {@literal null}.
*/
public List<EventType> getPublishedEvents() {
return publishedEvents.get();
}
/**
* Returns all types that are considered aggregate roots.
*
* @return will never be {@literal null}.
*/
public List<JavaClass> getAggregateRoots() {
return entities.get().stream() //
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal())) //
.filter(ArchitecturallyEvidentType::isAggregateRoot) //
.map(ArchitecturallyEvidentType::getType) //
.flatMap(this::resolveModuleSuperTypes) //
.distinct() //
.collect(Collectors.toList());
}
/**
* Returns all types that are considered aggregate roots.
*
* @param modules must not be {@literal null}.
* @return
* @deprecated since 1.3, use {@link #getAggregateRoots()} instead.
*/
@Deprecated
public List<JavaClass> getAggregateRoots(Modules modules) {
Assert.notNull(modules, "Modules must not be null!");
return getAggregateRoots();
}
/**
* Returns all modules that contain types which the types of the current module depend on.
*
* @param modules must not be {@literal null}.
* @return
*/
public Stream<Module> getBootstrapDependencies(Modules modules) {
Assert.notNull(modules, "Modules must not be null!");
return getBootstrapDependencies(modules, DependencyDepth.IMMEDIATE);
}
public Stream<Module> getBootstrapDependencies(Modules modules, DependencyDepth depth) {
Assert.notNull(modules, "Modules must not be null!");
Assert.notNull(depth, "Dependency depth must not be null!");
return streamDependencies(modules, depth);
}
/**
* Returns all {@link JavaPackage} for the current module including the ones by its dependencies.
*
* @param modules must not be {@literal null}.
* @param depth must not be {@literal null}.
* @return
*/
public Stream<JavaPackage> getBasePackages(Modules modules, DependencyDepth depth) {
Assert.notNull(modules, "Modules must not be null!");
Assert.notNull(depth, "Dependency depth must not be null!");
Stream<Module> dependencies = streamDependencies(modules, depth);
return Stream.concat(Stream.of(this), dependencies) //
.map(Module::getBasePackage);
}
public List<SpringBean> getSpringBeans() {
return getSpringBeansInternal().stream() //
.map(it -> SpringBean.of(it, this)) //
.collect(Collectors.toList());
}
Classes getSpringBeansInternal() {
return springBeans.get();
}
public boolean contains(JavaClass type) {
return basePackage.contains(type);
}
public boolean contains(@Nullable Class<?> type) {
return type != null && getType(type.getName()).isPresent();
}
/**
* Returns the {@link JavaClass} for the given candidate simple of fully-qualified type name.
*
* @param candidate must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.1
*/
public Optional<JavaClass> getType(String candidate) {
Assert.hasText(candidate, "Candidate must not be null or emtpy!");
return basePackage.stream()
.filter(hasSimpleOrFullyQualifiedName(candidate))
.findFirst();
}
/**
* Returns whether the given {@link JavaClass} is exposed by the current module, i.e. whether it's part of any of the
* module's named interfaces.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean isExposed(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
}
public void verifyDependencies(Modules modules) {
detectDependencies(modules).throwIfPresent();
}
public Violations detectDependencies(Modules modules) {
return getAllModuleDependencies(modules) //
.map(it -> it.isValidDependencyWithin(modules)) //
.reduce(Violations.NONE, Violations::and);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toString(null);
}
public String toString(@Nullable Modules modules) {
StringBuilder builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n");
builder.append("> Logical name: ").append(getName()).append('\n');
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
if (namedInterfaces.hasExplicitInterfaces()) {
builder.append("> Named interfaces:\n");
namedInterfaces.forEach(it -> builder.append(" + ") //
.append(it.toString()) //
.append('\n'));
}
if (modules != null) {
List<Module> dependencies = getBootstrapDependencies(modules).collect(Collectors.toList());
builder.append("> Direct module dependencies: ");
builder.append(dependencies.isEmpty() ? "none"
: dependencies.stream().map(Module::getName).collect(Collectors.joining(", ")));
builder.append('\n');
}
Classes beans = getSpringBeansInternal();
if (beans.isEmpty()) {
builder.append("> Spring beans: none\n");
} else {
builder.append("> Spring beans:\n");
beans.forEach(it -> builder.append(" ") //
.append(Classes.format(it, basePackage.getName()))//
.append('\n'));
}
return builder.toString();
}
/**
* Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
* {@link Modules} instance.
*
* @param modules must not be {@literal null}.
* @return
*/
List<Module> getAllowedDependencies(Modules modules) {
Assert.notNull(modules, "Modules must not be null!");
List<String> allowedDependencyNames = information.getAllowedDependencies();
if (allowedDependencyNames.isEmpty()) {
return Collections.emptyList();
}
Stream<Module> explicitlyDeclaredModules = allowedDependencyNames.stream() //
.map(modules::getModuleByName) //
.flatMap(it -> it.map(Stream::of).orElse(Stream.empty()));
return Stream.concat(explicitlyDeclaredModules, modules.getSharedModules().stream()) //
.distinct() //
.collect(Collectors.toList());
}
/**
* Returns whether the given module contains a type with the given simple or fully qualified name.
*
* @param candidate must not be {@literal null} or empty.
* @return
* @since 1.1
*/
boolean contains(String candidate) {
Assert.hasText(candidate, "Candidate must not be null or empty!");
return getType(candidate).isPresent();
}
private List<EventType> findPublishedEvents() {
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
return basePackage.that(isEvent).stream() //
.map(EventType::new)
.collect(Collectors.toList());
}
/**
* Returns a {@link Stream} of all super types of the given one that are declared in the same module as well as the
* type itself.
*
* @param type must not be {@literal null}.
* @return
*/
private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return Stream.concat(//
type.getAllRawSuperclasses().stream().filter(this::contains), //
Stream.of(type));
}
private Stream<ModuleDependency> getAllModuleDependencies(Modules modules) {
return basePackage.stream() //
.flatMap(it -> getModuleDependenciesOf(it, modules));
}
private Stream<Module> streamDependencies(Modules modules, DependencyDepth depth) {
switch (depth) {
case NONE:
return Stream.empty();
case IMMEDIATE:
return getDirectModuleDependencies(modules);
case ALL:
default:
return getDirectModuleDependencies(modules) //
.flatMap(it -> Stream.concat(Stream.of(it), it.streamDependencies(modules, DependencyDepth.ALL))) //
.distinct();
}
}
private Stream<Module> getDirectModuleDependencies(Modules modules) {
return getSpringBeansInternal().stream() //
.flatMap(it -> ModuleDependency.fromType(it)) //
.filter(it -> isDependencyToOtherModule(it.target, modules)) //
.map(it -> modules.getModuleByType(it.target)) //
.distinct() //
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
}
private Stream<ModuleDependency> getModuleDependenciesOf(JavaClass type, Modules modules) {
Stream<ModuleDependency> injections = ModuleDependency.fromType(type) //
.filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); //
Stream<ModuleDependency> directDependencies = type.getDirectDependenciesFromSelf().stream() //
.filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) //
.map(ModuleDependency::new);
return Stream.concat(injections, directDependencies).distinct();
}
private boolean isDependencyToOtherModule(JavaClass dependency, Modules modules) {
return modules.contains(dependency) && !contains(dependency);
}
private Classes findEntities(JavaPackage source) {
return source.stream() //
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
.filter(ArchitecturallyEvidentType::isEntity) //
.map(ArchitecturallyEvidentType::getType).collect(toClasses());
}
private static Classes filterSpringBeans(JavaPackage source) {
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
.flatMap(it -> it.getMethods().stream()) //
.filter(SpringTypes::isAtBeanMethod) //
.map(JavaMethod::getRawReturnType) //
.collect(Collectors.groupingBy(it -> source.contains(it)));
Classes repositories = source.that(isSpringDataRepository());
Classes coreComponents = source.that(not(INTERFACES).and(isComponent()));
Classes configurationProperties = source.that(isConfigurationProperties());
return coreComponents //
.and(repositories) //
.and(configurationProperties) //
.and(collect.getOrDefault(true, Collections.emptyList())) //
.and(collect.getOrDefault(false, Collections.emptyList()));
}
private static Predicate<JavaClass> hasSimpleOrFullyQualifiedName(String candidate) {
return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate);
}
public enum DependencyDepth {
NONE,
IMMEDIATE,
ALL;
}
@EqualsAndHashCode
@RequiredArgsConstructor
static class ModuleDependency {
private static final List<String> INJECTION_TYPES = Arrays.asList(//
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
private final @NonNull @Getter JavaClass origin, target;
private final @NonNull String description;
private final @NonNull DependencyType type;
ModuleDependency(Dependency dependency) {
this(dependency.getOriginClass(), //
dependency.getTargetClass(), //
dependency.getDescription(), //
DependencyType.forDependency(dependency));
}
boolean hasType(DependencyType type) {
return this.type.equals(type);
}
Violations isValidDependencyWithin(Modules modules) {
Module originModule = getExistingModuleOf(origin, modules);
Module targetModule = getExistingModuleOf(target, modules);
List<Module> allowedTargets = originModule.getAllowedDependencies(modules);
Violations violations = Violations.NONE;
if (!allowedTargets.isEmpty() && !allowedTargets.contains(targetModule)) {
String allowedTargetsString = allowedTargets.stream() //
.map(Module::getName) //
.collect(Collectors.joining(", "));
String message = String.format("Module '%s' depends on module '%s' via %s -> %s. Allowed target modules: %s.",
originModule.getName(), targetModule.getName(), origin.getName(), target.getName(), allowedTargetsString);
violations = violations.and(new IllegalStateException(message));
}
if (!targetModule.isExposed(target)) {
String violationText = String.format("Module '%s' depends on non-exposed type %s within module '%s'!",
originModule.getName(), target.getName(), targetModule.getName());
violations = violations.and(new IllegalStateException(violationText + lineSeparator() + description));
}
return violations;
}
Module getExistingModuleOf(JavaClass javaClass, Modules modules) {
Optional<Module> module = modules.getModuleByType(javaClass);
return module.orElseThrow(() -> new IllegalStateException(
String.format("Origin/Target of a %s should always be within a module, but %s is not",
getClass().getSimpleName(), javaClass.getName())));
}
static ModuleDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
String description = createDescription(codeUnit, parameter, "parameter");
DependencyType type = DependencyType.forCodeUnit(codeUnit) //
.or(() -> DependencyType.forParameter(parameter));
return new ModuleDependency(codeUnit.getOwner(), parameter, description, type);
}
static ModuleDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
String description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
return new ModuleDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
DependencyType.DEFAULT);
}
static Stream<ModuleDependency> fromType(JavaClass source) {
return Stream.concat(Stream.concat(fromConstructorOf(source), fromMethodsOf(source)), fromFieldsOf(source));
}
private static Stream<ModuleDependency> fromConstructorOf(JavaClass source) {
Set<JavaConstructor> constructors = source.getConstructors();
return constructors.stream() //
.filter(it -> constructors.size() == 1 || isInjectionPoint(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> new InjectionModuleDependency(source, parameter, it)));
}
private static Stream<ModuleDependency> fromFieldsOf(JavaClass source) {
Stream<ModuleDependency> fieldInjections = source.getAllFields().stream() //
.filter(ModuleDependency::isInjectionPoint) //
.map(field -> new InjectionModuleDependency(source, field.getRawType(), field));
return fieldInjections;
}
private static Stream<ModuleDependency> fromMethodsOf(JavaClass source) {
Set<JavaMethod> methods = source.getAllMethods().stream() //
.filter(it -> !it.getOwner().isEquivalentTo(Object.class)) //
.collect(Collectors.toSet());
if (methods.isEmpty()) {
return Stream.empty();
}
Stream<ModuleDependency> returnTypes = methods.stream() //
.filter(it -> !it.getRawReturnType().isPrimitive()) //
.filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) //
.map(it -> fromCodeUnitReturnType(it));
Set<JavaMethod> injectionMethods = methods.stream() //
.filter(ModuleDependency::isInjectionPoint) //
.collect(Collectors.toSet());
Stream<ModuleDependency> methodInjections = injectionMethods.stream() //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> new InjectionModuleDependency(source, parameter, it)));
Stream<ModuleDependency> otherMethods = methods.stream() //
.filter(it -> !injectionMethods.contains(it)) //
.flatMap(it -> it.getRawParameterTypes().stream() //
.map(parameter -> fromCodeUnitParameter(it, parameter)));
return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes);
}
static Stream<ModuleDependency> allFrom(JavaCodeUnit codeUnit) {
Stream<ModuleDependency> parameterDependencies = codeUnit.getRawParameterTypes()//
.stream() //
.map(it -> fromCodeUnitParameter(codeUnit, it));
Stream<ModuleDependency> returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
return Stream.concat(parameterDependencies, returnType);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return type.format(FormatableJavaClass.of(origin), FormatableJavaClass.of(target));
}
private static String createDescription(JavaMember codeUnit, JavaClass declaringElement,
String declarationDescription) {
String type = declaringElement.getSimpleName();
String codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) //
? String.format("%s", declaringElement.getSimpleName()) //
: String.format("%s.%s", declaringElement.getSimpleName(), codeUnit.getName());
if (JavaCodeUnit.class.isInstance(codeUnit)) {
codeUnitDescription = String.format("%s(%s)", codeUnitDescription,
JavaCodeUnit.class.cast(codeUnit).getRawParameterTypes().stream() //
.map(JavaClass::getSimpleName) //
.collect(Collectors.joining(", ")));
}
String annotations = codeUnit.getAnnotations().stream() //
.filter(it -> INJECTION_TYPES.contains(it.getRawType().getName())) //
.map(it -> "@" + it.getRawType().getSimpleName()) //
.collect(Collectors.joining(" ", "", " "));
annotations = StringUtils.hasText(annotations) ? annotations : "";
String declaration = declarationDescription + " " + annotations + codeUnitDescription;
String location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString();
return String.format("%s declares %s in %s", type, declaration, location);
}
private static boolean isInjectionPoint(JavaMember unit) {
return INJECTION_TYPES.stream().anyMatch(type -> unit.isAnnotatedWith(type));
}
}
private static class InjectionModuleDependency extends ModuleDependency {
private final JavaMember member;
private final boolean isConfigurationClass;
/**
* @param origin
* @param target
* @param member
*/
public InjectionModuleDependency(JavaClass origin, JavaClass target, JavaMember member) {
super(origin, target, ModuleDependency.createDescription(member, origin, getDescriptionFor(member)),
DependencyType.USES_COMPONENT);
this.member = member;
this.isConfigurationClass = isConfiguration().apply(origin);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.ModuleDependency#isValidDependencyWithin(org.moduliths.model.Modules)
*/
@Override
Violations isValidDependencyWithin(Modules modules) {
Violations violations = super.isValidDependencyWithin(modules);
if (JavaField.class.isInstance(member) && !isConfigurationClass) {
Module module = getExistingModuleOf(member.getOwner(), modules);
violations = violations.and(new IllegalStateException(
String.format("Module %s uses field injection in %s. Prefer constructor injection instead!",
module.getDisplayName(), member.getFullName())));
}
return violations;
}
private static String getDescriptionFor(JavaMember member) {
if (JavaConstructor.class.isInstance(member)) {
return "constructor";
} else if (JavaMethod.class.isInstance(member)) {
return "injection method";
} else if (JavaField.class.isInstance(member)) {
return "injected field";
}
throw new IllegalArgumentException(String.format("Invalid member type %s!", member.toString()));
}
}
public enum DependencyType {
/**
* Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be
* bootstrapped to run the source module.
*/
USES_COMPONENT {
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module refers to an entity of the other.
*/
ENTITY {
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
/**
* Indicates that the module depends on the other by declaring an event listener for an event exposed by the other
* module. Thus, the target module does not have to be bootstrapped to run the source one.
*/
EVENT_LISTENER {
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(),
target.getAbbreviatedFullName());
}
},
DEFAULT {
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier)
*/
@Override
public DependencyType or(Supplier<DependencyType> supplier) {
return supplier.get();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
*/
@Override
public String format(FormatableJavaClass source, FormatableJavaClass target) {
return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
}
};
public static DependencyType forParameter(JavaClass type) {
return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT;
}
public static DependencyType forCodeUnit(JavaCodeUnit codeUnit) {
return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).apply(codeUnit) //
|| Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).apply(codeUnit) //
? EVENT_LISTENER
: DEFAULT;
}
public static DependencyType forDependency(Dependency dependency) {
return forParameter(dependency.getTargetClass());
}
public abstract String format(FormatableJavaClass source, FormatableJavaClass target);
public DependencyType or(Supplier<DependencyType> supplier) {
return this;
}
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
*/
public static Stream<DependencyType> allBut(Collection<DependencyType> types) {
Assert.notNull(types, "Types must not be null!");
Predicate<DependencyType> isIncluded = types::contains;
return Arrays.stream(values()) //
.filter(isIncluded.negate());
}
public static Stream<DependencyType> allBut(Stream<DependencyType> types) {
return allBut(types.collect(Collectors.toList()));
}
/**
* Returns all {@link DependencyType}s except the given ones.
*
* @param types must not be {@literal null}.
* @return
*/
public static Stream<DependencyType> allBut(DependencyType... types) {
Assert.notNull(types, "Types must not be null!");
return allBut(Arrays.asList(types));
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020-2021 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.moduliths.model;
import java.util.Objects;
import java.util.stream.Stream;
import org.moduliths.Module;
import org.moduliths.model.Types.JMoleculesTypes;
/**
* Default implementations of {@link ModuleDetectionStrategy}.
*
* @author Oliver Drotbohm
* @see ModuleDetectionStrategy#directSubPackage()
* @see ModuleDetectionStrategy#explictlyAnnotated()
*/
enum ModuleDetectionStrategies implements ModuleDetectionStrategy {
DIRECT_SUB_PACKAGES {
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleDetection#getModuleBasePackages(org.moduliths.model.JavaPackage)
*/
@Override
public Stream<JavaPackage> getModuleBasePackages(
JavaPackage basePackage) {
return basePackage.getDirectSubPackages().stream();
}
},
EXPLICITLY_ANNOTATED {
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleDetection#getModuleBasePackages(org.moduliths.model.JavaPackage)
*/
@Override
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
return Stream.of(Module.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent())
.filter(Objects::nonNull)
.flatMap(basePackage::getSubPackagesAnnotatedWith);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2020 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.moduliths.model;
import java.util.stream.Stream;
import org.moduliths.Module;
/**
* Strategy interface to customize which packages are considered module base packages.
*
* @author Oliver Drotbohm
*/
public interface ModuleDetectionStrategy {
/**
* Given the {@link JavaPackage} that Moduliths was initialized with, return the base packages for all modules in the
* system.
*
* @param basePackage will never be {@literal null}.
* @return must not be {@literal null}.
*/
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);
/**
* A {@link ModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package to be module
* base packages.
*
* @return will never be {@literal null}.
*/
static ModuleDetectionStrategy directSubPackage() {
return ModuleDetectionStrategies.DIRECT_SUB_PACKAGES;
}
/**
* A {@link ModuleDetectionStrategy} that considers packages explicitly annotated with {@link Module} module base
* packages.
*
* @return will never be {@literal null}.
*/
static ModuleDetectionStrategy explictlyAnnotated() {
return ModuleDetectionStrategies.EXPLICITLY_ANNOTATED;
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.moduliths.Module;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Abstraction for low-level module information. Used to support different annotations to configure metadata about a
* module.
*
* @author Oliver Drotbohm
*/
interface ModuleInformation {
public static ModuleInformation of(JavaPackage javaPackage) {
if (ClassUtils.isPresent("org.jmolecules.ddd.annotation.Module", ModuleInformation.class.getClassLoader())
&& MoleculesModule.supports(javaPackage)) {
return new MoleculesModule(javaPackage);
}
return new ModulithsModule(javaPackage);
}
String getDisplayName();
List<String> getAllowedDependencies();
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
static abstract class AbstractModuleInformation implements ModuleInformation {
private final JavaPackage javaPackage;
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleInformation#getName()
*/
@Override
public String getDisplayName() {
return javaPackage.getName();
}
}
static class MoleculesModule extends AbstractModuleInformation {
private final Optional<org.jmolecules.ddd.annotation.Module> annotation;
public static boolean supports(JavaPackage javaPackage) {
return javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class).isPresent();
}
public MoleculesModule(JavaPackage javaPackage) {
super(javaPackage);
this.annotation = javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleInformation#getName()
*/
@Override
public String getDisplayName() {
return annotation //
.map(org.jmolecules.ddd.annotation.Module::name) //
.filter(StringUtils::hasText)
.orElseGet(() -> annotation //
.map(org.jmolecules.ddd.annotation.Module::value) //
.filter(StringUtils::hasText) //
.orElseGet(super::getDisplayName));
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleInformation#getAllowedDependencies()
*/
@Override
public List<String> getAllowedDependencies() {
return Collections.emptyList();
}
}
static class ModulithsModule extends AbstractModuleInformation {
private final Optional<Module> annotation;
public static boolean supports(JavaPackage javaPackage) {
return javaPackage.getAnnotation(Module.class).isPresent();
}
public ModulithsModule(JavaPackage javaPackage) {
super(javaPackage);
this.annotation = javaPackage.getAnnotation(Module.class);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleInformation.AbstractModuleInformation#getName()
*/
@Override
public String getDisplayName() {
return annotation //
.map(Module::displayName) //
.filter(StringUtils::hasText) //
.orElseGet(super::getDisplayName);
}
/*
* (non-Javadoc)
* @see org.moduliths.model.ModuleInformation#getAllowedDependencies()
*/
@Override
public List<String> getAllowedDependencies() {
return annotation //
.map(it -> Arrays.stream(it.allowedDependencies())) //
.orElse(Stream.empty()) //
.collect(Collectors.toList());
}
}
}

View File

@@ -0,0 +1,451 @@
/*
* Copyright 2018-2020 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
*
* http://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.moduliths.model;
import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static java.util.stream.Collectors.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jmolecules.archunit.JMoleculesDddRules;
import org.moduliths.Modulith;
import org.moduliths.Modulithic;
import org.moduliths.model.Types.JMoleculesTypes;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.lang.FailureReport;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
/**
* @author Oliver Gierke
* @author Peter Gafert
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Modules implements Iterable<Module> {
private static final Map<CacheKey, Modules> CACHE = new HashMap<>();
private static final ModuleDetectionStrategy DETECTION_STRATEGY;
static {
List<ModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(ModuleDetectionStrategy.class,
Modules.class.getClassLoader());
if (loadFactories.size() > 1) {
throw new IllegalStateException(
String.format("Multiple module detection strategies configured. Only one supported! %s",
loadFactories));
}
DETECTION_STRATEGY = loadFactories.isEmpty() ? ModuleDetectionStrategies.DIRECT_SUB_PACKAGES : loadFactories.get(0);
}
private final ModulithMetadata metadata;
private final Map<String, Module> modules;
private final JavaClasses allClasses;
private final List<JavaPackage> rootPackages;
private final @With(AccessLevel.PRIVATE) @Getter Set<Module> sharedModules;
private boolean verified;
private Modules(ModulithMetadata metadata, Collection<String> packages, DescribedPredicate<JavaClass> ignored,
boolean useFullyQualifiedModuleNames) {
this.metadata = metadata;
this.allClasses = new ClassFileImporter() //
.withImportOption(new ImportOption.DoNotIncludeTests()) //
.importPackages(packages) //
.that(not(ignored));
Classes classes = Classes.of(allClasses);
this.modules = packages.stream() //
.map(it -> JavaPackage.of(classes, it))
.flatMap(DETECTION_STRATEGY::getModuleBasePackages) //
.map(it -> new Module(it, useFullyQualifiedModuleNames)) //
.collect(toMap(Module::getName, Function.identity()));
this.rootPackages = packages.stream() //
.map(it -> JavaPackage.of(classes, it).toSingle()) //
.collect(Collectors.toList());
this.sharedModules = Collections.emptySet();
}
/**
* Creates a new {@link Modules} relative to the given modulith type. Will inspect the {@link Modulith} annotation on
* the class given for advanced customizations of the module setup.
*
* @param modulithType must not be {@literal null}.
* @return
*/
public static Modules of(Class<?> modulithType) {
return of(modulithType, alwaysFalse());
}
/**
* Creates a new {@link Modules} relative to the given modulith type, a {@link ModuleDetectionStrategy} and a
* {@link DescribedPredicate} which types and packages to ignore. Will inspect the {@link Modulith} and
* {@link Modulithic} annotations on the class given for advanced customizations of the module setup.
*
* @param modulithType must not be {@literal null}.
* @param detection must not be {@literal null}.
* @param ignored must not be {@literal null}.
* @return
*/
public static Modules of(Class<?> modulithType, DescribedPredicate<JavaClass> ignored) {
CacheKey key = TypeKey.of(modulithType, ignored);
return CACHE.computeIfAbsent(key, it -> {
Assert.notNull(modulithType, "Modulith root type must not be null!");
Assert.notNull(ignored, "Predicate to describe ignored types must not be null!");
return of(key);
});
}
/**
* Creates a new {@link Modules} instance for the given package name.
*
* @param javaPackage must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.1
*/
public static Modules of(String javaPackage) {
return of(javaPackage, alwaysFalse());
}
/**
* Creates a new {@link Modules} instance for the given package name and ignored classes.
*
* @param javaPackage must not be {@literal null} or empty.
* @param ignored must not be {@literal null}.
* @return will never be {@literal null}.
* @since 1.1
*/
public static Modules of(String javaPackage, DescribedPredicate<JavaClass> ignored) {
CacheKey key = PackageKey.of(javaPackage, ignored);
return CACHE.computeIfAbsent(key, it -> {
Assert.hasText(javaPackage, "Base package must not be null or empty!");
Assert.notNull(ignored, "Predicate to describe ignored types must not be null!");
return of(key);
});
}
/**
* Creates a new {@link Modules} instance for the given {@link CacheKey}.
*
* @param key must not be {@literal null}.
* @return will never be {@literal null}.
*/
private static Modules of(CacheKey key) {
Assert.notNull(key, "Cache key must not be null!");
ModulithMetadata metadata = key.getMetadata();
Set<String> basePackages = new HashSet<>();
basePackages.add(key.getBasePackage());
basePackages.addAll(metadata.getAdditionalPackages());
Modules modules = new Modules(metadata, basePackages, key.getIgnored(),
metadata.useFullyQualifiedModuleNames());
Set<Module> sharedModules = metadata.getSharedModuleNames() //
.map(modules::getRequiredModule) //
.collect(Collectors.toSet());
return modules.withSharedModules(sharedModules);
}
public Object getModulithSource() {
return metadata.getModulithSource();
}
/**
* @return
* @deprecated since 1.1, as a {@link Modules} instance doesn't have to be created from a class in the first place.
* For generic use, use {@link #getModulithSource()} instead.
*/
@Deprecated
public Class<?> getModulithType() {
Object source = getModulithSource();
if (!Class.class.isInstance(source)) {
throw new IllegalStateException(String.format("Moduliths not created from a type but %s!", source));
}
return (Class<?>) source;
}
/**
* Returns whether the given {@link JavaClass} is contained within the {@link Modules}.
*
* @param type must not be {@literal null}.
* @return
*/
public boolean contains(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return modules.values().stream() //
.anyMatch(module -> module.contains(type));
}
/**
* Returns whether the given type is contained in one of the root packages (not including sub-packages) of the
* modules.
*
* @param className must not be {@literal null} or empty.
* @return
*/
public boolean withinRootPackages(String className) {
Assert.hasText(className, "Class name must not be null or empty!");
return rootPackages.stream().anyMatch(it -> it.contains(className));
}
/**
* Returns the {@link Module} with the given name.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public Optional<Module> getModuleByName(String name) {
Assert.hasText(name, "Module name must not be null or empty!");
return Optional.ofNullable(modules.get(name));
}
/**
* Returns the module that contains the given {@link JavaClass}.
*
* @param type must not be {@literal null}.
* @return
*/
public Optional<Module> getModuleByType(JavaClass type) {
Assert.notNull(type, "Type must not be null!");
return modules.values().stream() //
.filter(it -> it.contains(type)) //
.findFirst();
}
/**
* Returns the {@link Module} containing the type with the given simple or fully-qualified name.
*
* @param candidate must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.1
*/
public Optional<Module> getModuleByType(String candidate) {
Assert.hasText(candidate, "Candidate must not be null or empty!");
return modules.values().stream() //
.filter(it -> it.contains(candidate)) //
.findFirst();
}
public Optional<Module> getModuleForPackage(String name) {
return modules.values().stream() //
.filter(it -> name.startsWith(it.getBasePackage().getName())) //
.findFirst();
}
public void verify() {
if (verified) {
return;
}
Violations violations = detectViolations();
this.verified = true;
violations.throwIfPresent();
}
public Violations detectViolations() {
Violations violations = rootPackages.stream() //
.map(this::assertNoCyclesFor) //
.flatMap(it -> it.getDetails().stream()) //
.map(IllegalStateException::new) //
.collect(Violations.toViolations());
if (JMoleculesTypes.areRulesPresent()) {
EvaluationResult result = JMoleculesDddRules.all().evaluate(allClasses);
for (String message : result.getFailureReport().getDetails()) {
violations = violations.and(message);
}
}
return modules.values().stream() //
.map(it -> it.detectDependencies(this)) //
.reduce(violations, Violations::and);
}
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
EvaluationResult result = SlicesRuleDefinition.slices() //
.matching(rootPackage.getName().concat(".(*)..")) //
.should().beFreeOfCycles() //
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
return result.getFailureReport();
}
/**
* Returns all {@link Module}s.
*
* @return will never be {@literal null}.
*/
public Stream<Module> stream() {
return modules.values().stream();
}
/**
* Returns the system name if defined.
*
* @return
*/
public Optional<String> getSystemName() {
return metadata.getSystemName();
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Module> iterator() {
return modules.values().iterator();
}
/**
* Returns the module with the given name rejecting invalid module names.
*
* @param moduleName must not be {@literal null}.
* @return
*/
private Module getRequiredModule(String moduleName) {
Module module = modules.get(moduleName);
if (module == null) {
throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName));
}
return module;
}
public static class Filters {
public static DescribedPredicate<JavaClass> withoutModules(String... names) {
return Arrays.stream(names) //
.map(it -> withoutModule(it)) //
.reduce(DescribedPredicate.alwaysFalse(), DescribedPredicate::or, (__, right) -> right);
}
public static DescribedPredicate<JavaClass> withoutModule(String name) {
return resideInAPackage("..".concat(name).concat(".."));
}
}
private static interface CacheKey {
String getBasePackage();
DescribedPredicate<JavaClass> getIgnored();
ModulithMetadata getMetadata();
}
@Value(staticConstructor = "of")
private static final class TypeKey implements CacheKey {
Class<?> type;
DescribedPredicate<JavaClass> ignored;
/*
* (non-Javadoc)
* @see org.moduliths.model.Modules.CacheKey#getBasePackage()
*/
@Override
public String getBasePackage() {
return type.getPackage().getName();
}
/*
* (non-Javadoc)
* @see org.moduliths.model.Modules.CacheKey#getMetadata()
*/
@Override
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(type);
}
}
@Value(staticConstructor = "of")
private static final class PackageKey implements CacheKey {
String basePackage;
DescribedPredicate<JavaClass> ignored;
/*
* (non-Javadoc)
* @see org.moduliths.model.Modules.CacheKey#getMetadata()
*/
@Override
public ModulithMetadata getMetadata() {
return ModulithMetadata.of(basePackage);
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2019-2020 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.moduliths.model;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.moduliths.Modulith;
import org.moduliths.Modulithic;
import org.moduliths.model.Types.SpringTypes;
import org.springframework.util.Assert;
interface ModulithMetadata {
static final String ANNOTATION_MISSING = "Modules can only be retrieved from a root type, but %s is not annotated with either @%s, @%s or @%s!";
/**
* Creates a new {@link ModulithMetadata} for the given annotated type. Expecteds the type either be annotated with
* {@link Modulith}, {@link Modulithic} or {@link SpringBootApplication}.
*
* @param annotated must not be {@literal null}.
* @return
* @throws IllegalArgumentException in case none of the above mentioned annotations is present on the given type.
*/
public static ModulithMetadata of(Class<?> annotated) {
Assert.notNull(annotated, "Annotated type must not be null!");
Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException(
String.format(ANNOTATION_MISSING, annotated.getSimpleName(), Modulith.class.getSimpleName(),
Modulithic.class.getSimpleName(), SpringTypes.AT_SPRING_BOOT_APPLICATION));
Supplier<ModulithMetadata> withDefaults = () -> DefaultModulithMetadata.of(annotated).orElseThrow(exception);
return AnnotationModulithMetadata.of(annotated).orElseGet(withDefaults);
}
/**
* Creates a new {@link ModulithMetadata} instance for the given package.
*
* @param javaPackage must not be {@literal null} or empty.
* @return will never be {@literal null}.
* @since 1.1
*/
public static ModulithMetadata of(String javaPackage) {
return DefaultModulithMetadata.of(javaPackage);
}
/**
* Returns the source of the Moduliths setup. Either a type or a package.
*
* @return will never be {@literal null}.
* @since 1.1
*/
Object getModulithSource();
/**
* Returns the names of the packages that are supposed to be considered modulith base packages, i.e. for which to
* consider all direct sub-packages modules by default.
*
* @return will never be {@literal null}.
*/
List<String> getAdditionalPackages();
/**
* Whether to use fully-qualified module names, i.e. rather use the fully-qualified package name instead of the local
* one.
*
* @return
*/
boolean useFullyQualifiedModuleNames();
/**
* Returns the name of shared modules, i.e. modules that are supposed to always be included in bootstraps.
*
* @return will never be {@literal null}.
*/
Stream<String> getSharedModuleNames();
/**
* Returns the name of the system.
*
* @return will never be {@literal null}.
*/
Optional<String> getSystemName();
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2018 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
*
* http://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.moduliths.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
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;
/**
* @author Oliver Gierke
*/
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class NamedInterface implements Iterable<JavaClass> {
private static final String UNNAMED_NAME = "<<UNNAMED>>";
private static final String PACKAGE_INFO_NAME = "package-info";
protected final @Getter String name;
static NamedInterface unnamed(JavaPackage javaPackage) {
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
}
public static List<PackageBasedNamedInterface> of(JavaPackage javaPackage) {
String[] name = javaPackage.getAnnotation(org.moduliths.NamedInterface.class) //
.map(it -> it.value()) //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
return Arrays.stream(name) //
.map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
.collect(Collectors.toList());
}
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
return new TypeBasedNamedInterface(name, classes, basePackage);
}
public boolean isUnnamed() {
return name.equals(UNNAMED_NAME);
}
public boolean contains(JavaClass type) {
return getClasses().contains(type);
}
public boolean contains(Class<?> type) {
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
}
/**
* Returns whether the given {@link NamedInterface} has the same name as the current one.
*
* @param other
* @return
*/
boolean hasSameNameAs(NamedInterface other) {
return this.name.equals(other.name);
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<JavaClass> iterator() {
return getClasses().iterator();
}
protected abstract Classes getClasses();
public abstract NamedInterface merge(TypeBasedNamedInterface other);
static class PackageBasedNamedInterface extends NamedInterface {
private final @Getter 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.moduliths.model.NamedInterface#merge(org.moduliths.model.NamedInterface.TypeBasedNamedInterface)
*/
@Override
public NamedInterface merge(TypeBasedNamedInterface other) {
return new PackageBasedNamedInterface(name, classes.and(other.classes), javaPackage);
}
/*
* (non-Javadoc)
* @see org.moduliths.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()));
}
}
static class TypeBasedNamedInterface extends NamedInterface {
private final @Getter Classes classes;
private final JavaPackage pkg;
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
super(name);
this.classes = types;
this.pkg = pkg;
}
/*
* (non-Javadoc)
* @see org.moduliths.model.NamedInterface#merge(org.moduliths.model.NamedInterface.TypeBasedNamedInterface)
*/
@Override
public NamedInterface merge(TypeBasedNamedInterface other) {
return new TypeBasedNamedInterface(name, classes.and(other.classes), pkg);
}
/*
* (non-Javadoc)
* @see org.moduliths.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()));
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2018 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
*
* http://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.moduliths.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.moduliths.model.NamedInterface.TypeBasedNamedInterface;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* @author Oliver Gierke
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class NamedInterfaces implements Iterable<NamedInterface> {
public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList());
private final List<NamedInterface> namedInterfaces;
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
.orUnnamed(basePackage);
}
public static NamedInterfaces of(List<NamedInterface> interfaces) {
return interfaces.isEmpty() ? NONE : new NamedInterfaces(interfaces);
}
static NamedInterfaces ofAnnotatedPackages(JavaPackage basePackage) {
return basePackage //
.getSubPackagesAnnotatedWith(org.moduliths.NamedInterface.class) //
.flatMap(it -> NamedInterface.of(it).stream()) //
.collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of));
}
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
MultiValueMap<String, JavaClass> mappings = new LinkedMultiValueMap<>();
basePackage.stream() //
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
.forEach(it -> {
if (!it.isAnnotatedWith(org.moduliths.NamedInterface.class)) {
return;
}
org.moduliths.NamedInterface annotation = it
.getAnnotationOfType(org.moduliths.NamedInterface.class);
for (String name : annotation.value()) {
mappings.add(name, it);
}
});
return mappings.entrySet().stream() //
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
.collect(Collectors.toList());
}
public boolean hasExplicitInterfaces() {
return namedInterfaces.size() > 1 || !namedInterfaces.get(0).isUnnamed();
}
public Stream<NamedInterface> stream() {
return namedInterfaces.stream();
}
public NamedInterfaces and(List<TypeBasedNamedInterface> others) {
List<NamedInterface> namedInterfaces = new ArrayList<>();
List<NamedInterface> unmergedInterface = this.namedInterfaces;
for (TypeBasedNamedInterface candidate : others) {
Optional<NamedInterface> existing = namedInterfaces.stream() //
.filter(it -> it.hasSameNameAs(candidate)) //
.findFirst();
// Merge existing with new and add to result
existing.ifPresent(it -> {
namedInterfaces.add(it.merge(candidate));
namedInterfaces.add(it);
unmergedInterface.remove(it);
});
// Simply add candidate
if (!existing.isPresent()) {
namedInterfaces.add(candidate);
}
}
namedInterfaces.addAll(unmergedInterface);
return new NamedInterfaces(namedInterfaces);
}
public NamedInterfaces orUnnamed(JavaPackage basePackage) {
return namedInterfaces.isEmpty() //
? of(Collections.singletonList(NamedInterface.unnamed(basePackage))) //
: this;
}
public Optional<NamedInterface> getByName(String name) {
return namedInterfaces.stream().filter(it -> it.getName().equals(name)).findFirst();
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<NamedInterface> iterator() {
return namedInterfaces.iterator();
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2020 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.moduliths.model;
/**
* A {@link Source} of some type, bean definition etc. Essentially describes the origin of that bean, event etc.
*
* @author Oliver Drotbohm
* @since 1.1
*/
public interface Source {
/**
* Renders the source in human readable way.
*
* @param module must not be {@literal null}.
* @return
*/
String toString(Module module);
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.stream.Collectors;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* A Spring bean type.
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE)
public class SpringBean {
private final @Getter JavaClass type;
private final Module module;
/**
* Returns the fully-qualified name of the Spring bean type.
*
* @return
*/
public String getFullyQualifiedTypeName() {
return type.getFullName();
}
/**
* Returns all interfaces implemented by the bean that are part of the same module.
*
* @return
*/
public List<JavaClass> getInterfacesWithinModule() {
return type.getRawInterfaces().stream() //
.filter(module::contains) //
.collect(Collectors.toList());
}
public boolean isAnnotatedWith(Class<?> type) {
return Types.isAnnotatedWith(type).apply(this.type);
}
public ArchitecturallyEvidentType toArchitecturallyEvidentType() {
return ArchitecturallyEvidentType.of(type, module.getSpringBeansInternal());
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2020-2021 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.moduliths.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import lombok.experimental.UtilityClass;
import java.lang.annotation.Annotation;
import org.springframework.lang.Nullable;
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
*/
@UtilityClass
class Types {
@Nullable
@SuppressWarnings("unchecked")
<T> Class<T> loadIfPresent(String name) {
ClassLoader loader = Types.class.getClassLoader();
return ClassUtils.isPresent(name, loader) ? (Class<T>) ClassUtils.resolveClassName(name, loader) : null;
}
static class JMoleculesTypes {
private static final String BASE_PACKAGE = "org.jmolecules";
private static final String ANNOTATION_PACKAGE = BASE_PACKAGE + ".ddd.annotation";
private static final String AT_ENTITY = ANNOTATION_PACKAGE + ".Entity";
private static final String ARCHUNIT_RULES = BASE_PACKAGE + ".archunit.JMoleculesDddRules";
private static final String MODULE = ANNOTATION_PACKAGE + ".Module";
static final String AT_DOMAIN_EVENT_HANDLER = BASE_PACKAGE + ".event.annotation.DomainEventHandler";
static final String AT_DOMAIN_EVENT = BASE_PACKAGE + ".event.annotation.DomainEvent";
static final String DOMAIN_EVENT = BASE_PACKAGE + ".event.types.DomainEvent";
public static boolean isPresent() {
return ClassUtils.isPresent(AT_ENTITY, JMoleculesTypes.class.getClassLoader());
}
@Nullable
@SuppressWarnings("unchecked")
public static Class<? extends Annotation> getModuleAnnotationTypeIfPresent() {
try {
return isPresent()
? (Class<? extends Annotation>) ClassUtils.forName(MODULE, JMoleculesTypes.class.getClassLoader())
: null;
} catch (Exception o_O) {
return null;
}
}
public static boolean areRulesPresent() {
return ClassUtils.isPresent(ARCHUNIT_RULES, JMoleculesTypes.class.getClassLoader());
}
}
@UtilityClass
static class JavaXTypes {
private static final String BASE_PACKAGE = "javax";
static final String AT_ENTITY = BASE_PACKAGE + ".persistence.Entity";
static final String AT_INJECT = BASE_PACKAGE + ".inject.Inject";
static final String AT_RESOURCE = BASE_PACKAGE + ".annotation.Resource";
static DescribedPredicate<? super JavaClass> isJpaEntity() {
return isAnnotatedWith(AT_ENTITY);
}
}
@UtilityClass
static class SpringTypes {
private static final String BASE_PACKAGE = "org.springframework";
static final String APPLICATION_LISTENER = BASE_PACKAGE + ".context.ApplicationListener";
static final String AT_AUTOWIRED = BASE_PACKAGE + ".beans.factory.annotation.Autowired";
static final String AT_ASYNC = BASE_PACKAGE + ".scheduling.annotation.Async";
static final String AT_BEAN = BASE_PACKAGE + ".context.annotation.Bean";
static final String AT_COMPONENT = BASE_PACKAGE + ".stereotype.Component";
static final String AT_CONFIGURATION = BASE_PACKAGE + ".context.annotation.Configuration";
static final String AT_CONTROLLER = BASE_PACKAGE + ".stereotype.Controller";
static final String AT_EVENT_LISTENER = BASE_PACKAGE + ".context.event.EventListener";
static final String AT_REPOSITORY = BASE_PACKAGE + ".stereotype.Repository";
static final String AT_SERVICE = BASE_PACKAGE + ".stereotype.Service";
static final String AT_SPRING_BOOT_APPLICATION = BASE_PACKAGE + ".boot.autoconfigure.SpringBootApplication";
static final String AT_TX_EVENT_LISTENER = BASE_PACKAGE + ".transaction.event.TransactionalEventListener";
static final String AT_CONFIGURATION_PROPERTIES = BASE_PACKAGE + ".boot.context.properties.ConfigurationProperties";
static DescribedPredicate<? super JavaClass> isConfiguration() {
return isAnnotatedWith(AT_CONFIGURATION);
}
static DescribedPredicate<? super JavaClass> isComponent() {
return isAnnotatedWith(AT_COMPONENT);
}
static DescribedPredicate<? super JavaClass> isConfigurationProperties() {
return isAnnotatedWith(AT_CONFIGURATION_PROPERTIES);
}
static boolean isAtBeanMethod(JavaMethod method) {
return isAnnotatedWith(SpringTypes.AT_BEAN).apply(method);
}
}
@UtilityClass
static class SpringDataTypes {
private static final String BASE_PACKAGE = SpringTypes.BASE_PACKAGE + ".data";
static final String REPOSITORY = BASE_PACKAGE + ".repository.Repository";
static final String AT_REPOSITORY_DEFINITION = BASE_PACKAGE + ".repository.RepositoryDefinition";
static boolean isPresent() {
return ClassUtils.isPresent(REPOSITORY, SpringDataTypes.class.getClassLoader());
}
static DescribedPredicate<JavaClass> isSpringDataRepository() {
return assignableTo(SpringDataTypes.REPOSITORY) //
.or(isAnnotatedWith(SpringDataTypes.AT_REPOSITORY_DEFINITION));
}
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
return isAnnotatedWith(type.getName());
}
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.annotatedWith(type) //
.or(Predicates.metaAnnotatedWith(type));
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2020 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.moduliths.model;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.Assert;
/**
* Value type to gather and report architectural violations.
*
* @author Oliver Drotbohm
*/
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
public class Violations extends RuntimeException {
private static final long serialVersionUID = 6863781504675034691L;
public static Violations NONE = new Violations(Collections.emptyList());
private final List<RuntimeException> exceptions;
/**
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
*
* @return will never be {@literal null}.
*/
static Collector<RuntimeException, ?, Violations> toViolations() {
return Collectors.collectingAndThen(Collectors.toList(), Violations::of);
}
/*
* (non-Javadoc)
* @see java.lang.Throwable#getMessage()
*/
@Override
public String getMessage() {
return exceptions.stream() //
.map(RuntimeException::getMessage) //
.collect(Collectors.joining("\n- ", "- ", ""));
}
/**
* Returns whether there are violations available.
*
* @return
*/
public boolean hasViolations() {
return !exceptions.isEmpty();
}
/**
* Throws itself in case it's not an empty instance.
*/
public void throwIfPresent() {
if (hasViolations()) {
throw this;
}
}
/**
* Creates a new {@link Violations} with the given {@link RuntimeException} added to the current ones?
*
* @param exception must not be {@literal null}.
* @return
*/
Violations and(RuntimeException exception) {
Assert.notNull(exception, "Exception must not be null!");
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + 1);
newExceptions.addAll(exceptions);
newExceptions.add(exception);
return new Violations(newExceptions);
}
Violations and(Violations other) {
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + other.exceptions.size());
newExceptions.addAll(exceptions);
newExceptions.addAll(other.exceptions);
return new Violations(newExceptions);
}
Violations and(String violation) {
return and(new ArchitecturalViolation(violation));
}
private static class ArchitecturalViolation extends RuntimeException {
private static final long serialVersionUID = 3587887036508024142L;
public ArchitecturalViolation(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.model;

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2019 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
*
* http://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 com.acme.withatbean;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Oliver Drotbohm
*/
@Configuration
public class SampleConfiguration {
@Bean
DataSource dataSource() {
return null;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2020-2021 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 com.acme.withatbean;
/**
* @author Oliver Drotbohm
*/
public class TestEvents {
/**
* Method calling a factory method.
*/
public void method() {
JMoleculesAnnotated.of();
}
/**
* Method calling a constructor.
*/
public void constructorCall() {
new JMoleculesAnnotated();
}
// jMolecules
@org.jmolecules.event.annotation.DomainEvent
public static class JMoleculesAnnotated {
public static JMoleculesAnnotated of() {
return null;
}
}
public static class JMoleculesImplementing implements org.jmolecules.event.types.DomainEvent {}
}

View File

@@ -0,0 +1,2 @@
@org.jmolecules.ddd.annotation.Module
package jmolecules;

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020 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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.jupiter.api.Test;
import org.moduliths.Modulithic;
/**
* Unit tests for {@link AnnotationModulithMetadata}.
*
* @author Oliver Drotbohm
*/
class AnnotationModulithMetadataUnitTest {
@Test
void findsCustomizationsOnClass() {
assertThat(AnnotationModulithMetadata.of(Sample.class)).hasValueSatisfying(it -> {
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
});
}
@Test
void findsCustomizationsOnClassForMetaAnnotationUsage() {
assertThat(AnnotationModulithMetadata.of(MetaSample.class)).hasValueSatisfying(it -> {
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
});
}
@Modulithic(useFullyQualifiedModuleNames = true)
static class Sample {}
@Intermediate
static class MetaSample {}
@Retention(RetentionPolicy.RUNTIME)
@Modulithic(useFullyQualifiedModuleNames = true)
@interface Intermediate {
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2020 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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import javax.persistence.Entity;
import org.jmolecules.event.annotation.DomainEventHandler;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.moduliths.model.ArchitecturallyEvidentType.SpringAwareArchitecturallyEvidentType;
import org.moduliths.model.ArchitecturallyEvidentType.SpringDataAwareArchitecturallyEvidentType;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* Unit tests for {@link ArchitecturallyEvidentType}.
*
* @author Oliver Drotbohm
*/
class ArchitecturallyEvidentTypeUnitTest {
Classes classes = TestUtils.getClasses();
JavaClass self = classes.getRequiredClass(ArchitecturallyEvidentTypeUnitTest.class);
@Test
void abbreviatesFullyQualifiedTypeName() {
ArchitecturallyEvidentType type = ArchitecturallyEvidentType.of(self, classes);
assertThat(type.getAbbreviatedFullName()).isEqualTo("o.m.m.ArchitecturallyEvidentTypeUnitTest");
}
@Test
void doesNotConsiderArbitraryTypeAStereotype() {
ArchitecturallyEvidentType type = ArchitecturallyEvidentType.of(self, classes);
assertThat(type.isEntity()).isFalse();
assertThat(type.isAggregateRoot()).isFalse();
assertThat(type.isRepository()).isFalse();
}
@Test
void detectsSpringAnnotatedRepositories() {
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
classes.getRequiredClass(SpringRepository.class));
assertThat(type.isRepository()).isTrue();
}
@Test
void doesNotConsiderEntityAggregateRoot() {
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
classes.getRequiredClass(SampleEntity.class));
assertThat(type.isEntity()).isTrue();
assertThat(type.isAggregateRoot()).isFalse();
}
@Test
void considersEntityAnAggregateRootIfTheresARepositoryForIt() {
Map<Class<?>, Boolean> parameters = new HashMap<Class<?>, Boolean>();
parameters.put(SampleEntity.class, true);
parameters.put(OtherEntity.class, false);
parameters.put(NoEntity.class, false);
parameters.entrySet().stream().forEach(it -> {
JavaClass entity = classes.getRequiredClass(it.getKey());
assertThat(new SpringDataAwareArchitecturallyEvidentType(entity, classes).isAggregateRoot())
.isEqualTo(it.getValue());
});
}
@TestFactory
Stream<DynamicTest> considersJMoleculesEntity() {
return DynamicTest.stream(getTypesFor(JMoleculesAnnotatedEntity.class, JMoleculesImplementingEntity.class), //
it -> String.format("%s is considered an entity", it.getType().getSimpleName()), //
it -> {
assertThat(it.isEntity()).isTrue();
assertThat(it.isAggregateRoot()).isFalse();
assertThat(it.isRepository()).isFalse();
});
}
@TestFactory
Stream<DynamicTest> considersJMoleculesAggregateRoot() {
return DynamicTest.stream(
getTypesFor(JMoleculesAnnotatedAggregateRoot.class, JMoleculesImplementingAggregateRoot.class), //
it -> String.format("%s is considered an entity, aggregate root but not a repository",
it.getType().getSimpleName()), //
it -> {
assertThat(it.isEntity()).isTrue();
assertThat(it.isAggregateRoot()).isTrue();
assertThat(it.isRepository()).isFalse();
});
}
@TestFactory
Stream<DynamicTest> considersJMoleculesRepository() {
return DynamicTest.stream(getTypesFor(JMoleculesAnnotatedRepository.class), //
it -> String.format("%s is considered a repository", it.getType().getSimpleName()), //
it -> {
assertThat(it.isEntity()).isFalse();
assertThat(it.isAggregateRoot()).isFalse();
assertThat(it.isRepository()).isTrue();
});
}
@Test
void discoversEventsListenedToForEventListener() {
JavaClass listenerType = classes.getRequiredClass(SomeEventListener.class);
assertThat(ArchitecturallyEvidentType.of(listenerType, classes).getReferenceTypes()) //
.extracting(JavaClass::getFullName) //
.containsExactly(Object.class.getName(), String.class.getName());
}
@Test
void discoversImplementingEventListener() {
JavaClass listenerType = classes.getRequiredClass(ImplementingEventListener.class);
assertThat(ArchitecturallyEvidentType.of(listenerType, classes).getReferenceTypes()) //
.extracting(JavaClass::getFullName) //
.containsExactly(ApplicationReadyEvent.class.getName());
}
@Test
void discoversJMoleculesEventHandler() {
JavaClass type = classes.getRequiredClass(JMoleculesEventListener.class);
assertThat(ArchitecturallyEvidentType.of(type, classes).isEventListener()).isTrue();
}
@Test
void discoversJMoleculesRepository() {
JavaClass type = classes.getRequiredClass(JMoleculesImplementingRepository.class);
assertThat(ArchitecturallyEvidentType.of(type, classes).isRepository()).isTrue();
}
private Iterator<ArchitecturallyEvidentType> getTypesFor(Class<?>... types) {
return Stream.of(types) //
.map(classes::getRequiredClass) //
.map(it -> ArchitecturallyEvidentType.of(it, classes)) //
.iterator();
}
// Spring
@Repository
interface SpringRepository {}
@Entity
class SampleEntity {}
// Spring Data
interface SampleRepository extends CrudRepository<SampleEntity, UUID> {}
@Entity
class OtherEntity {}
class NoEntity {}
// jMolecules
@org.jmolecules.ddd.annotation.Entity
class JMoleculesAnnotatedEntity {}
@org.jmolecules.ddd.annotation.AggregateRoot
class JMoleculesAnnotatedAggregateRoot {}
class JMoleculesImplementingIdentifier implements org.jmolecules.ddd.types.Identifier {}
abstract class JMoleculesImplementingEntity
implements
org.jmolecules.ddd.types.Entity<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
abstract class JMoleculesImplementingAggregateRoot
implements
org.jmolecules.ddd.types.AggregateRoot<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
@org.jmolecules.ddd.annotation.Repository
class JMoleculesAnnotatedRepository {}
interface JMoleculesEventListener {
@DomainEventHandler
void on(Object event);
}
interface JMoleculesImplementingRepository extends
org.jmolecules.ddd.types.Repository<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
// Spring
class SomeEventListener {
@EventListener
void on(Object event) {}
@EventListener
void on(String event) {}
@EventListener
void onOther(Object event) {}
}
class ImplementingEventListener implements ApplicationListener<ApplicationReadyEvent> {
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
*/
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2019 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
*
* http://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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.moduliths.model.Module.ModuleDependency;
import org.springframework.beans.factory.annotation.Autowired;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ClassFileImporter;
/**
* Unit tests for {@link ModuleDependency}.
*
* @author Oliver Drotbohm
*/
class ModuleDependencyUnitTest {
ClassFileImporter importer = new ClassFileImporter();
@Test
public void detectsInjectionDependencies() {
assertThat(findDependencies(SubType.class)) //
.containsExactlyInAnyOrder(A.class, B.class, C.class, D.class, E.class, F.class);
}
@Test
public void detectsDependencyFromAnnotatedConstructor() {
assertThat(findDependencies(MultipleConstructors.class)) //
.containsExactlyInAnyOrder(B.class);
}
@Test
public void detectsDependencyFromSingleUnannotatedConstructor() {
assertThat(findDependencies(SingleConstructor.class)) //
.containsExactlyInAnyOrder(B.class);
}
private Stream<Class<?>> findDependencies(Class<?> type) {
return ModuleDependency.fromType(importer.importClass(type)) //
.map(ModuleDependency::getTarget) //
.map(JavaClass::reflect);
}
static class A {}
static class B {}
static class C {}
static class D {}
static class E {}
static class F {}
static class SomeComponent {
@Autowired A a;
@Autowired
void setD(D d) {}
}
static class SubType extends SomeComponent {
@Autowired E e;
SubType(B b, C c) {}
@Autowired
void setF(F f) {}
}
static class MultipleConstructors {
MultipleConstructors(A a) {}
@Autowired
MultipleConstructors(B b) {}
}
static class SingleConstructor {
SingleConstructor(B b) {}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020-2021 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.moduliths.model;
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 ModuleDetectionStrategy}.
*
* @author Oliver Drotbohm
*/
class ModuleDetectionStrategyUnitTest {
@Test
void usesExplicitlyAnnotatedConstant() {
assertThat(ModuleDetectionStrategy.explictlyAnnotated())
.isEqualTo(ModuleDetectionStrategies.EXPLICITLY_ANNOTATED);
}
@Test
void usesDirectSubPackages() {
assertThat(ModuleDetectionStrategy.directSubPackage())
.isEqualTo(ModuleDetectionStrategies.DIRECT_SUB_PACKAGES);
}
@Test
void detectsJMoleculesAnnotatedModule() {
JavaClasses classes = new ClassFileImporter() //
.withImportOption(new ImportOption.OnlyIncludeTests()) //
.importPackages("jmolecules");
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
assertThat(ModuleDetectionStrategy.explictlyAnnotated().getModuleBasePackages(javaPackage))
.containsExactly(javaPackage);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019-2020 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
*
* http://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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import com.acme.withatbean.TestEvents.JMoleculesAnnotated;
import com.acme.withatbean.TestEvents.JMoleculesImplementing;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
/**
* Unit tests for {@link Module}.
*
* @author Oliver Drotbohm
*/
@TestInstance(Lifecycle.PER_CLASS)
class ModuleUnitTest {
ClassFileImporter importer = new ClassFileImporter();
JavaClasses classes = importer.importPackages("com.acme.withatbean"); //
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "");
Module module = new Module(javaPackage, false);
@Test
public void considersExternalSpringBeans() {
assertThat(module.getSpringBeans()) //
.flatExtracting(SpringBean::getFullyQualifiedTypeName) //
.contains(DataSource.class.getName());
}
@Test
void discoversPublishedEvents() {
JavaClass jMoleculesAnnotated = classes.get(JMoleculesAnnotated.class);
JavaClass jMoleculesImplementing = classes.get(JMoleculesImplementing.class);
List<EventType> events = module.getPublishedEvents();
assertThat(events.stream().map(EventType::getType)) //
.containsExactlyInAnyOrder(jMoleculesAnnotated, jMoleculesImplementing);
assertThat(events.stream().filter(it -> it.getType().equals(jMoleculesAnnotated))) //
.element(0) //
.satisfies(it -> {
assertThat(it.getSources()).isNotEmpty();
});
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2019 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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.moduliths.Modulith;
import org.moduliths.Modulithic;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Unit tests for {@link ModulithMetadata}.
*
* @author Oliver Drotbohm
*/
class ModulithMetadataUnitTest {
@Test
public void inspectsModulithAnnotation() throws Exception {
Stream.of(ModulithAnnotated.class, ModuliticAnnotated.class) //
.map(ModulithMetadata::of) //
.forEach(it -> {
assertThat(it.getAdditionalPackages()).containsExactly("com.acme.foo");
assertThat(it.getSharedModuleNames()).containsExactly("shared.module");
assertThat(it.getSystemName()).hasValue("systemName");
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
});
}
@Test
public void usesDefaultsIfModulithAnnotationsAreMissing() {
ModulithMetadata metadata = ModulithMetadata.of(SpringBootApplicationAnnotated.class);
assertThat(metadata.getAdditionalPackages()).isEmpty();
assertThat(metadata.getSharedModuleNames()).isEmpty();
assertThat(metadata.getSystemName()).isEmpty();
assertThat(metadata.useFullyQualifiedModuleNames()).isFalse();
}
@Test
public void rejectsTypeNotAnnotatedWithEitherModulithAnnotationOrSpringBootApplication() {
assertThatExceptionOfType(IllegalArgumentException.class) //
.isThrownBy(() -> ModulithMetadata.of(Unannotated.class)) //
.withMessageContaining(Modulith.class.getSimpleName()) //
.withMessageContaining(Modulithic.class.getSimpleName()) //
.withMessageContaining(SpringBootApplication.class.getSimpleName());
}
@Modulith(additionalPackages = "com.acme.foo", //
sharedModules = "shared.module", //
systemName = "systemName", //
useFullyQualifiedModuleNames = true)
static class ModulithAnnotated {}
@Modulithic(additionalPackages = "com.acme.foo", //
sharedModules = "shared.module", //
systemName = "systemName", //
useFullyQualifiedModuleNames = true)
static class ModuliticAnnotated {}
@SpringBootApplication
static class SpringBootApplicationAnnotated {}
static class Unannotated {}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2020 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.moduliths.model;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import org.jmolecules.ddd.annotation.AggregateRoot;
import org.springframework.data.repository.Repository;
import org.springframework.util.Assert;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
/**
* Utilities for testing.
*
* @author Oliver Drotbohm
*/
class TestUtils {
private static Supplier<JavaClasses> imported = Suppliers.memoize(() -> new ClassFileImporter() //
.importPackagesOf(Modules.class, Repository.class, AggregateRoot.class));
private static DescribedPredicate<JavaClass> IS_MODULE_TYPE = JavaClass.Predicates
.resideInAPackage(Modules.class.getPackage().getName());
private static Supplier<Classes> classes = Suppliers.memoize(() -> Classes.of(imported.get()).that(IS_MODULE_TYPE));
/**
* Returns all {@link Classes} of this module.
*
* @return
*/
public static Classes getClasses() {
return classes.get();
}
public static JavaClasses getJavaClasses() {
return imported.get().that(IS_MODULE_TYPE);
}
/**
* Returns all {@link Classes} in the package of the given type.
*
* @param packageType must not be {@literal null}.
* @return
*/
public static Classes getClasses(Class<?> packageType) {
Assert.notNull(packageType, "Package type must not be null!");
return getClasses().that(resideInAPackage(packageType.getPackage().getName()));
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2020 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.moduliths.model;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link Violations}.
*
* @author Oliver Drotbohm
*/
class ViolationsUnitTests {
@Test
void combinesExceptionMessages() {
Violations violations = Violations.NONE //
.and(new IllegalArgumentException("First")) //
.and(new IllegalArgumentException("Second"));
assertThat(violations.getMessage()) //
.isEqualTo("- First\n- Second");
}
}

View File

@@ -0,0 +1 @@
spring.main.banner-mode=OFF

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<root level="OFF">
<appender-ref ref="console" />
</root>
</configuration>