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:
83
moduliths-core/pom.xml
Normal file
83
moduliths-core/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
moduliths-core/src/main/java/org/moduliths/model/Classes.java
Normal file
237
moduliths-core/src/main/java/org/moduliths/model/Classes.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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("."));
|
||||
}
|
||||
}
|
||||
@@ -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 ? "" : "…");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
833
moduliths-core/src/main/java/org/moduliths/model/Module.java
Normal file
833
moduliths-core/src/main/java/org/moduliths/model/Module.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
451
moduliths-core/src/main/java/org/moduliths/model/Modules.java
Normal file
451
moduliths-core/src/main/java/org/moduliths/model/Modules.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
33
moduliths-core/src/main/java/org/moduliths/model/Source.java
Normal file
33
moduliths-core/src/main/java/org/moduliths/model/Source.java
Normal 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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
158
moduliths-core/src/main/java/org/moduliths/model/Types.java
Normal file
158
moduliths-core/src/main/java/org/moduliths/model/Types.java
Normal 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));
|
||||
}
|
||||
}
|
||||
122
moduliths-core/src/main/java/org/moduliths/model/Violations.java
Normal file
122
moduliths-core/src/main/java/org/moduliths/model/Violations.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.model;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.jmolecules.ddd.annotation.Module
|
||||
package jmolecules;
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
1
moduliths-core/src/test/resources/application.properties
Normal file
1
moduliths-core/src/test/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.main.banner-mode=OFF
|
||||
14
moduliths-core/src/test/resources/logback.xml
Normal file
14
moduliths-core/src/test/resources/logback.xml
Normal 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>
|
||||
Reference in New Issue
Block a user