GH-301 - Add Neo4j event publication repository.
This commit is contained in:
committed by
Oliver Drotbohm
parent
bf404a571c
commit
4e069b98ea
@@ -124,6 +124,11 @@
|
||||
<artifactId>spring-modulith-starter-mongodb</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starter-neo4j</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starter-test</artifactId>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<module>spring-modulith-events-jpa</module>
|
||||
<module>spring-modulith-events-kafka</module>
|
||||
<module>spring-modulith-events-mongodb</module>
|
||||
<module>spring-modulith-events-neo4j</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
||||
58
spring-modulith-events/spring-modulith-events-neo4j/pom.xml
Normal file
58
spring-modulith-events/spring-modulith-events-neo4j/pom.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<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.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Spring Modulith - Events - Neo4j-based repository</name>
|
||||
<artifactId>spring-modulith-events-neo4j</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springframework.modulith.events.neo4j</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-modulith-events-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-neo4j</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<version>1.19.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>1.19.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* The event publication entity definition.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jEventPublication {
|
||||
|
||||
public final UUID identifier;
|
||||
public final Instant publicationDate;
|
||||
public final String listenerId;
|
||||
public final Object event;
|
||||
public final String eventHash;
|
||||
|
||||
public Instant completionDate;
|
||||
|
||||
public Neo4jEventPublication(UUID identifier, Instant publicationDate, String listenerId, Object event, String eventHash) {
|
||||
this.identifier = identifier;
|
||||
this.publicationDate = publicationDate;
|
||||
this.listenerId = listenerId;
|
||||
this.event = event;
|
||||
this.eventHash = eventHash;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import org.neo4j.cypherdsl.core.renderer.Configuration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.modulith.events.config.EventPublicationAutoConfiguration;
|
||||
import org.springframework.modulith.events.config.EventPublicationConfigurationExtension;
|
||||
import org.springframework.modulith.events.core.EventSerializer;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@AutoConfigureBefore(EventPublicationAutoConfiguration.class)
|
||||
public class Neo4jEventPublicationAutoConfiguration implements EventPublicationConfigurationExtension {
|
||||
|
||||
@Bean
|
||||
Neo4jEventPublicationRepository neo4jEventPublicationRepository(Neo4jClient neo4jClient, Configuration cypherDslConfiguration, EventSerializer eventSerializer) {
|
||||
return new Neo4jEventPublicationRepository(neo4jClient, cypherDslConfiguration, eventSerializer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Configuration.class)
|
||||
Configuration cypherDslConfiguration() {
|
||||
return Configuration.defaultConfig();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "spring.modulith.events.neo4j.event-index.enabled", havingValue = "true")
|
||||
Neo4jIndexInitializer neo4jIndexInitializer(Neo4jClient neo4jClient) {
|
||||
return new Neo4jIndexInitializer(neo4jClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.renderer.Configuration;
|
||||
import org.neo4j.cypherdsl.core.renderer.Renderer;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.modulith.events.core.EventPublicationRepository;
|
||||
import org.springframework.modulith.events.core.EventSerializer;
|
||||
import org.springframework.modulith.events.core.PublicationTargetIdentifier;
|
||||
import org.springframework.modulith.events.core.TargetEventPublication;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Transactional
|
||||
class Neo4jEventPublicationRepository implements EventPublicationRepository {
|
||||
|
||||
private static final String ID = "identifier";
|
||||
private static final String EVENT_SERIALIZED = "eventSerialized";
|
||||
private static final String EVENT_HASH = "eventHash";
|
||||
private static final String EVENT_TYPE = "eventType";
|
||||
private static final String LISTENER_ID = "listenerId";
|
||||
private static final String PUBLICATION_DATE = "publicationDate";
|
||||
private static final String COMPLETION_DATE = "completionDate";
|
||||
|
||||
private static final Node eventPublicationNode = Cypher.node("Neo4jEventPublication").named("neo4jEventPublication");
|
||||
|
||||
private final Neo4jClient neo4jClient;
|
||||
private final Configuration cypherDslConfiguration;
|
||||
private final EventSerializer eventSerializer;
|
||||
|
||||
Neo4jEventPublicationRepository(Neo4jClient neo4jClient, Configuration cypherDslConfiguration, EventSerializer eventSerializer) {
|
||||
|
||||
Assert.notNull(neo4jClient, "Neo4jClient must not be null!");
|
||||
Assert.notNull(cypherDslConfiguration, "CypherDSL configuration must not be null!");
|
||||
Assert.notNull(eventSerializer, "EventSerializer must not be null!");
|
||||
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.cypherDslConfiguration = cypherDslConfiguration;
|
||||
this.eventSerializer = eventSerializer;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.EventPublicationRepository#create(org.springframework.modulith.events.EventPublication)
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public TargetEventPublication create(TargetEventPublication publication) {
|
||||
|
||||
var identifier = publication.getIdentifier();
|
||||
var publicationDate = publication.getPublicationDate();
|
||||
var listenerId = publication.getTargetIdentifier().getValue();
|
||||
var event = publication.getEvent();
|
||||
var eventType = event.getClass().getName();
|
||||
|
||||
var eventSerialized = (String) eventSerializer.serialize(event);
|
||||
var eventHash = DigestUtils.md5DigestAsHex(eventSerialized.getBytes());
|
||||
|
||||
var createStatement = Cypher.create(eventPublicationNode)
|
||||
.set(eventPublicationNode.property(ID).to(Cypher.parameter(ID)))
|
||||
.set(eventPublicationNode.property(EVENT_SERIALIZED).to(Cypher.parameter(EVENT_SERIALIZED)))
|
||||
.set(eventPublicationNode.property(EVENT_HASH).to(Cypher.parameter(EVENT_HASH)))
|
||||
.set(eventPublicationNode.property(EVENT_TYPE).to(Cypher.parameter(EVENT_TYPE)))
|
||||
.set(eventPublicationNode.property(LISTENER_ID).to(Cypher.parameter(LISTENER_ID)))
|
||||
.set(eventPublicationNode.property(PUBLICATION_DATE).to(Cypher.parameter(PUBLICATION_DATE)))
|
||||
.build();
|
||||
|
||||
neo4jClient.query(renderStatement(createStatement))
|
||||
.bindAll(Map.of(
|
||||
ID, Values.value(identifier.toString()),
|
||||
EVENT_SERIALIZED, eventSerialized,
|
||||
EVENT_HASH, eventHash,
|
||||
EVENT_TYPE, eventType,
|
||||
LISTENER_ID, listenerId,
|
||||
PUBLICATION_DATE, Values.value(publicationDate.atOffset(ZoneOffset.UTC))
|
||||
))
|
||||
.run();
|
||||
|
||||
return publication;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.EventPublicationRepository#markCompleted(java.lang.Object, org.springframework.modulith.events.PublicationTargetIdentifier, java.time.Instant)
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void markCompleted(Object event, PublicationTargetIdentifier identifier, Instant completionDate) {
|
||||
var eventHash = DigestUtils.md5DigestAsHex(((String) eventSerializer.serialize(event)).getBytes());
|
||||
|
||||
var completeStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(EVENT_HASH).eq(Cypher.parameter(EVENT_HASH)))
|
||||
.and(eventPublicationNode.property(LISTENER_ID).eq(Cypher.parameter(LISTENER_ID)))
|
||||
.set(eventPublicationNode.property(COMPLETION_DATE).to(Cypher.parameter(COMPLETION_DATE)))
|
||||
.build();
|
||||
|
||||
neo4jClient.query(renderStatement(completeStatement))
|
||||
.bind(eventHash).to(EVENT_HASH)
|
||||
.bind(identifier.getValue()).to(LISTENER_ID)
|
||||
.bind(Values.value(completionDate.atOffset(ZoneOffset.UTC))).to(COMPLETION_DATE)
|
||||
.run();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.core.EventPublicationRepository#findIncompletePublicationsByEventAndTargetIdentifier(java.lang.Object, org.springframework.modulith.events.core.PublicationTargetIdentifier)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<TargetEventPublication> findIncompletePublications() {
|
||||
|
||||
var findIncompleteStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(COMPLETION_DATE).isNull())
|
||||
.returning(eventPublicationNode)
|
||||
.orderBy(eventPublicationNode.property(PUBLICATION_DATE))
|
||||
.build();
|
||||
|
||||
return List.copyOf(neo4jClient.query(renderStatement(findIncompleteStatement))
|
||||
.fetchAs(TargetEventPublication.class)
|
||||
.mappedBy(this::mapRecordToPublication)
|
||||
.all());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<TargetEventPublication> findIncompletePublicationsPublishedBefore(Instant instant) {
|
||||
var findIncompleteStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(PUBLICATION_DATE).lt(Cypher.parameter(PUBLICATION_DATE)))
|
||||
.and(eventPublicationNode.property(COMPLETION_DATE).isNull())
|
||||
.returning(eventPublicationNode)
|
||||
.orderBy(eventPublicationNode.property(PUBLICATION_DATE))
|
||||
.build();
|
||||
|
||||
return List.copyOf(neo4jClient.query(renderStatement(findIncompleteStatement))
|
||||
.bind(Values.value(instant.atOffset(ZoneOffset.UTC))).to(PUBLICATION_DATE)
|
||||
.fetchAs(TargetEventPublication.class)
|
||||
.mappedBy(this::mapRecordToPublication)
|
||||
.all());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<TargetEventPublication> findIncompletePublicationsByEventAndTargetIdentifier(Object event, PublicationTargetIdentifier targetIdentifier) {
|
||||
|
||||
var eventHash = DigestUtils.md5DigestAsHex(((String) eventSerializer.serialize(event)).getBytes());
|
||||
var listenerId = targetIdentifier.getValue();
|
||||
|
||||
var statement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(EVENT_HASH).eq(Cypher.parameter(EVENT_HASH)))
|
||||
.and(eventPublicationNode.property(LISTENER_ID).eq(Cypher.parameter(LISTENER_ID)))
|
||||
.and(eventPublicationNode.property(COMPLETION_DATE).isNull())
|
||||
.returning(eventPublicationNode)
|
||||
.build();
|
||||
|
||||
return neo4jClient.query(renderStatement(statement))
|
||||
.bindAll(Map.of(EVENT_HASH, eventHash, LISTENER_ID, listenerId))
|
||||
.fetchAs(TargetEventPublication.class)
|
||||
.mappedBy(this::mapRecordToPublication)
|
||||
.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deletePublications(List<UUID> identifiers) {
|
||||
var deleteStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(ID).in(Cypher.parameter(ID)))
|
||||
.delete(eventPublicationNode)
|
||||
.build();
|
||||
|
||||
neo4jClient.query(renderStatement(deleteStatement))
|
||||
.bind(identifiers.stream().map(UUID::toString).toList()).to(ID)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteCompletedPublications() {
|
||||
var deleteStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(COMPLETION_DATE).isNotNull())
|
||||
.delete(eventPublicationNode)
|
||||
.build();
|
||||
|
||||
neo4jClient.query(renderStatement(deleteStatement))
|
||||
.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteCompletedPublicationsBefore(Instant instant) {
|
||||
var deleteStatement = Cypher.match(eventPublicationNode)
|
||||
.where(eventPublicationNode.property(PUBLICATION_DATE).lt(Cypher.parameter(PUBLICATION_DATE)))
|
||||
.and(eventPublicationNode.property(COMPLETION_DATE).isNotNull())
|
||||
.delete(eventPublicationNode)
|
||||
.build();
|
||||
|
||||
neo4jClient.query(renderStatement(deleteStatement))
|
||||
.bind(Values.value(instant.atOffset(ZoneOffset.UTC))).to(PUBLICATION_DATE)
|
||||
.run();
|
||||
}
|
||||
|
||||
private Neo4jEventPublicationAdapter mapRecordToPublication(TypeSystem typeSystem, org.neo4j.driver.Record record) {
|
||||
var publicationNode = record.get(eventPublicationNode.getRequiredSymbolicName().getValue()).asNode();
|
||||
var identifier = UUID.fromString(publicationNode.get(ID).asString());
|
||||
var publicationDate = publicationNode.get(PUBLICATION_DATE).asZonedDateTime().toInstant();
|
||||
var listenerId = publicationNode.get(LISTENER_ID).asString();
|
||||
var eventSerialized = publicationNode.get(EVENT_SERIALIZED).asString();
|
||||
var eventHash = publicationNode.get(EVENT_HASH).asString();
|
||||
var eventType = publicationNode.get(EVENT_TYPE).asString();
|
||||
|
||||
try {
|
||||
Object event = eventSerializer.deserialize(eventSerialized, Class.forName(eventType));
|
||||
Neo4jEventPublication publication = new Neo4jEventPublication(identifier, publicationDate, listenerId, event, eventHash);
|
||||
|
||||
return new Neo4jEventPublicationAdapter(publication);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String renderStatement(Statement statement) {
|
||||
return Renderer.getRenderer(cypherDslConfiguration).render(statement);
|
||||
}
|
||||
|
||||
|
||||
public static class Neo4jEventPublicationAdapter implements TargetEventPublication {
|
||||
|
||||
private final Neo4jEventPublication delegate;
|
||||
|
||||
public Neo4jEventPublicationAdapter(Neo4jEventPublication delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getIdentifier() {
|
||||
return delegate.identifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getEvent() {
|
||||
return delegate.event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getPublicationDate() {
|
||||
return delegate.publicationDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> getCompletionDate() {
|
||||
return Optional.ofNullable(delegate.completionDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markCompleted(Instant instant) {
|
||||
delegate.completionDate = instant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationTargetIdentifier getTargetIdentifier() {
|
||||
return PublicationTargetIdentifier.of(delegate.listenerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublicationCompleted() {
|
||||
return delegate.completionDate != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof Neo4jEventPublicationAdapter that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(delegate, that.delegate);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(delegate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jIndexInitializer implements InitializingBean {
|
||||
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
public Neo4jIndexInitializer(Neo4jClient neo4jClient) {
|
||||
this.neo4jClient = neo4jClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
neo4jClient
|
||||
.query("CREATE INDEX eventHashIndex IF NOT EXISTS FOR (n:`Neo4jEventPublication`) ON (n.eventHash)")
|
||||
.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.modulith.events.neo4j.event-index.enabled",
|
||||
"type": "java.lang.boolean",
|
||||
"description": "Whether to initialize the index on the Neo4j event publication event hash property.",
|
||||
"defaultValue": "false"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.modulith.events.neo4j.Neo4jEventPublicationAutoConfiguration
|
||||
@@ -0,0 +1,230 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import lombok.Value;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.cypherdsl.core.renderer.Dialect;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.neo4j.driver.types.Node;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.modulith.events.core.EventSerializer;
|
||||
import org.springframework.modulith.events.core.PublicationTargetIdentifier;
|
||||
import org.springframework.modulith.events.core.TargetEventPublication;
|
||||
import org.springframework.modulith.testapp.TestApplication;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.testcontainers.containers.Neo4jContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@SpringJUnitConfig(Neo4jEventPublicationRepositoryTest.Config.class)
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class Neo4jEventPublicationRepositoryTest {
|
||||
|
||||
@Container
|
||||
private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5"))
|
||||
.withRandomPassword();
|
||||
|
||||
static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener");
|
||||
|
||||
@Autowired
|
||||
private Neo4jEventPublicationRepository repository;
|
||||
|
||||
@Autowired
|
||||
private Driver driver;
|
||||
|
||||
@MockBean
|
||||
private EventSerializer eventSerializer;
|
||||
|
||||
@BeforeEach
|
||||
void clearDb() {
|
||||
try (var session = driver.session()) {
|
||||
session.run("MATCH (n) detach delete n").consume();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void createEventPublication() {
|
||||
var testEvent = new TestEvent("id");
|
||||
var eventSerialized = "{\"eventId\":\"id\"}";
|
||||
var eventHash = DigestUtils.md5DigestAsHex(eventSerialized.getBytes());
|
||||
|
||||
when(eventSerializer.serialize(testEvent)).thenReturn(eventSerialized);
|
||||
var publication = repository.create(TargetEventPublication.of(testEvent, TARGET_IDENTIFIER));
|
||||
|
||||
try (var session = driver.session()) {
|
||||
var result = session.run("MATCH (p:Neo4jEventPublication) return p")
|
||||
.single();
|
||||
|
||||
Node neo4jEventPublicationNode = result.get("p").asNode();
|
||||
assertThat(UUID.fromString(neo4jEventPublicationNode.get("identifier").asString())).isEqualTo(publication.getIdentifier());
|
||||
assertThat(neo4jEventPublicationNode.get("publicationDate").asZonedDateTime().toInstant()).isEqualTo(publication.getPublicationDate());
|
||||
assertThat(neo4jEventPublicationNode.get("listenerId").asString()).isEqualTo(publication.getTargetIdentifier().getValue());
|
||||
assertThat(neo4jEventPublicationNode.get("completionDate").isNull()).isTrue();
|
||||
assertThat(neo4jEventPublicationNode.get("eventSerialized").asString()).isEqualTo(eventSerialized);
|
||||
assertThat(neo4jEventPublicationNode.get("eventHash").asString()).isEqualTo(eventHash);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateEventPublication() {
|
||||
var testEvent1 = new TestEvent("id1");
|
||||
var event1Serialized = "{\"eventId\":\"id1\"}";
|
||||
var testEvent2 = new TestEvent("id2");
|
||||
var event2Serialized = "{\"eventId\":\"id2\"}";
|
||||
|
||||
when(eventSerializer.serialize(testEvent1)).thenReturn(event1Serialized);
|
||||
when(eventSerializer.serialize(testEvent2)).thenReturn(event2Serialized);
|
||||
when(eventSerializer.deserialize(event2Serialized, TestEvent.class)).thenReturn(testEvent2);
|
||||
|
||||
var event1 = repository.create(TargetEventPublication.of(testEvent1, TARGET_IDENTIFIER));
|
||||
var event2 = repository.create(TargetEventPublication.of(testEvent2, TARGET_IDENTIFIER));
|
||||
|
||||
var now = Instant.now();
|
||||
repository.markCompleted(event1, now);
|
||||
|
||||
assertThat(repository.findIncompletePublications()).hasSize(1)
|
||||
.element(0)
|
||||
.extracting(TargetEventPublication::getEvent).isEqualTo(event2.getEvent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findInCompletePastPublications() {
|
||||
var testEvent = new TestEvent("id");
|
||||
var eventSerialized = "{\"eventId\":\"id\"}";
|
||||
|
||||
when(eventSerializer.serialize(testEvent)).thenReturn(eventSerialized);
|
||||
when(eventSerializer.deserialize(eventSerialized, TestEvent.class)).thenReturn(testEvent);
|
||||
|
||||
var event = repository.create(TargetEventPublication.of(testEvent, TARGET_IDENTIFIER));
|
||||
|
||||
var newer = Instant.now().plus(1L, ChronoUnit.MINUTES);
|
||||
var older = Instant.now().minus(1L, ChronoUnit.MINUTES);
|
||||
|
||||
assertThat(repository.findIncompletePublicationsPublishedBefore(newer)).hasSize(1)
|
||||
.element(0)
|
||||
.extracting(TargetEventPublication::getEvent).isEqualTo(event.getEvent());
|
||||
|
||||
assertThat(repository.findIncompletePublicationsPublishedBefore(older)).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findIncompleteByEventAndTargetIdentifier() {
|
||||
var testEvent = new TestEvent("id");
|
||||
var eventSerialized = "{\"eventId\":\"id\"}";
|
||||
|
||||
when(eventSerializer.serialize(testEvent)).thenReturn(eventSerialized);
|
||||
when(eventSerializer.deserialize(eventSerialized, TestEvent.class)).thenReturn(testEvent);
|
||||
|
||||
var event = repository.create(TargetEventPublication.of(testEvent, TARGET_IDENTIFIER));
|
||||
|
||||
assertThat(repository.findIncompletePublicationsByEventAndTargetIdentifier(testEvent, event.getTargetIdentifier()))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletePublicationById() {
|
||||
TestEvent testEvent = new TestEvent("id");
|
||||
var eventSerialized = "{\"eventId\":\"id\"}";
|
||||
when(eventSerializer.serialize(testEvent)).thenReturn(eventSerialized);
|
||||
|
||||
var event = repository.create(TargetEventPublication.of(testEvent, TARGET_IDENTIFIER));
|
||||
assertThat(repository.findIncompletePublications()).hasSize(1);
|
||||
|
||||
repository.deletePublications(List.of(event.getIdentifier()));
|
||||
assertThat(repository.findIncompletePublications()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteCompletedPublications() {
|
||||
TestEvent testEvent1 = new TestEvent("id1");
|
||||
var event1Serialized = "{\"eventId\":\"id1\"}";
|
||||
TestEvent testEvent2 = new TestEvent("id2");
|
||||
var event2Serialized = "{\"eventId\":\"id2\"}";
|
||||
|
||||
when(eventSerializer.serialize(testEvent1)).thenReturn(event1Serialized);
|
||||
when(eventSerializer.serialize(testEvent2)).thenReturn(event2Serialized);
|
||||
when(eventSerializer.deserialize(event1Serialized, TestEvent.class)).thenReturn(testEvent1);
|
||||
when(eventSerializer.deserialize(event2Serialized, TestEvent.class)).thenReturn(testEvent2);
|
||||
|
||||
var event1 = repository.create(TargetEventPublication.of(testEvent1, TARGET_IDENTIFIER));
|
||||
|
||||
repository.markCompleted(event1, Instant.now());
|
||||
|
||||
repository.deleteCompletedPublications();
|
||||
|
||||
try (var session = driver.session()) {
|
||||
var count = session.run("MATCH (n) WHERE n.completionDate is not null return count(n)").single().get("count(n)").asLong();
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteCompletedPublicationsBefore() throws Exception {
|
||||
TestEvent testEvent1 = new TestEvent("id1");
|
||||
var event1Serialized = "{\"eventId\":\"id1\"}";
|
||||
TestEvent testEvent2 = new TestEvent("id2");
|
||||
var event2Serialized = "{\"eventId\":\"id2\"}";
|
||||
|
||||
when(eventSerializer.serialize(testEvent1)).thenReturn(event1Serialized);
|
||||
when(eventSerializer.serialize(testEvent2)).thenReturn(event2Serialized);
|
||||
when(eventSerializer.deserialize(event1Serialized, TestEvent.class)).thenReturn(testEvent1);
|
||||
when(eventSerializer.deserialize(event2Serialized, TestEvent.class)).thenReturn(testEvent2);
|
||||
|
||||
var event1 = repository.create(TargetEventPublication.of(testEvent1, TARGET_IDENTIFIER));
|
||||
|
||||
Instant old = Instant.now();
|
||||
repository.markCompleted(event1, old);
|
||||
Thread.sleep(100);
|
||||
var event2 = repository.create(TargetEventPublication.of(testEvent2, TARGET_IDENTIFIER));
|
||||
repository.markCompleted(event2, Instant.now());
|
||||
|
||||
repository.deleteCompletedPublicationsBefore(old.plus(10, ChronoUnit.MILLIS));
|
||||
// defensive check just to be sure
|
||||
assertThat(repository.findIncompletePublications()).hasSize(0);
|
||||
|
||||
try (var session = driver.session()) {
|
||||
var records = session.run("MATCH (n) WHERE n.completionDate is not null return n").list();
|
||||
assertThat(records.size()).isEqualTo(1);
|
||||
assertThat(records.get(0).get("n").asNode().get("eventSerialized").asString()).contains("id2");
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
private static final class TestEvent {
|
||||
String eventId;
|
||||
}
|
||||
|
||||
@Import({TestApplication.class})
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration() {
|
||||
return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_5).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.springframework.modulith.events.neo4j;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.modulith.events.core.EventSerializer;
|
||||
import org.springframework.modulith.testapp.TestApplication;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.testcontainers.containers.Neo4jContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jIndexInitializerTest {
|
||||
|
||||
@ImportAutoConfiguration({Neo4jEventPublicationAutoConfiguration.class, TestBase.Config.class})
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@ContextConfiguration(classes = TestApplication.class)
|
||||
static class TestBase {
|
||||
|
||||
@Container
|
||||
private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5"))
|
||||
.withRandomPassword();
|
||||
|
||||
@MockBean
|
||||
EventSerializer eventSerializer;
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
Driver driver() {
|
||||
return GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DataNeo4jTest(properties = "spring.modulith.events.neo4j.event-index.enabled=true")
|
||||
class WithIndexEnabled extends TestBase {
|
||||
@Autowired
|
||||
Neo4jClient neo4jClient;
|
||||
|
||||
@Autowired
|
||||
Optional<Neo4jIndexInitializer> neo4jIndexInitializer;
|
||||
|
||||
@Test
|
||||
void indexInitializerBeanIsPresent() {
|
||||
assertThat(neo4jIndexInitializer).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexWasCreated() {
|
||||
assertThat(neo4jClient.query("SHOW INDEX YIELD name")
|
||||
.fetchAs(String.class)
|
||||
.all()).contains("eventHashIndex");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DataNeo4jTest(properties = "spring.modulith.events.neo4j.event-index.enabled=false")
|
||||
class WithoutIndexEnabled extends TestBase {
|
||||
@Autowired
|
||||
Neo4jClient neo4jClient;
|
||||
|
||||
@Autowired
|
||||
Optional<Neo4jIndexInitializer> neo4jIndexInitializer;
|
||||
|
||||
@Test
|
||||
void indexInitializerBeanIsNotPresent() {
|
||||
assertThat(neo4jIndexInitializer).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexWasNotCreated() {
|
||||
assertThat(neo4jClient.query("SHOW INDEX YIELD name")
|
||||
.fetchAs(String.class)
|
||||
.all()).doesNotContain("eventHashIndex");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.springframework.modulith.testapp;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class TestApplication {
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
<modules>
|
||||
<module>spring-modulith-example-epr-jdbc</module>
|
||||
<module>spring-modulith-example-epr-mongodb</module>
|
||||
<module>spring-modulith-example-epr-neo4j</module>
|
||||
<module>spring-modulith-example-full</module>
|
||||
<module>spring-modulith-example-kafka</module>
|
||||
</modules>
|
||||
|
||||
BIN
spring-modulith-examples/spring-modulith-example-epr-neo4j/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
spring-modulith-examples/spring-modulith-example-epr-neo4j/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
18
spring-modulith-examples/spring-modulith-example-epr-neo4j/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
18
spring-modulith-examples/spring-modulith-example-epr-neo4j/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
||||
287
spring-modulith-examples/spring-modulith-example-epr-neo4j/mvnw
vendored
Executable file
287
spring-modulith-examples/spring-modulith-example-epr-neo4j/mvnw
vendored
Executable file
@@ -0,0 +1,287 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.1.1
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir"; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname $0)")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $wrapperUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
QUIET="--quiet"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
QUIET=""
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath"
|
||||
fi
|
||||
[ $? -eq 0 ] || rm -f "$wrapperJarPath"
|
||||
elif command -v curl > /dev/null; then
|
||||
QUIET="--silent"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
QUIET=""
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L
|
||||
fi
|
||||
[ $? -eq 0 ] || rm -f "$wrapperJarPath"
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=`cygpath --path --windows "$javaSource"`
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
187
spring-modulith-examples/spring-modulith-example-epr-neo4j/mvnw.cmd
vendored
Normal file
187
spring-modulith-examples/spring-modulith-example-epr-neo4j/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.1.1
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
@@ -0,0 +1,56 @@
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-examples</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-modulith-example-epr-neo4j</artifactId>
|
||||
<name>Spring Modulith - Examples - EPR Neo4j Example</name>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starter-neo4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- jMolecules -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules</groupId>
|
||||
<artifactId>jmolecules-events</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Persistence -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-neo4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
= Spring Modulith Example Documentation
|
||||
:modulith-docs: ../../../target/spring-modulith-docs
|
||||
|
||||
== Overview
|
||||
|
||||
plantuml::{modulith-docs}/components.puml[format="svg"]
|
||||
|
||||
== Inventory
|
||||
|
||||
plantuml::{modulith-docs}/module-inventory.puml[format="svg"]
|
||||
include::{modulith-docs}/module-inventory.adoc[]
|
||||
|
||||
== Orders
|
||||
|
||||
plantuml::{modulith-docs}/module-order.puml[format="svg"]
|
||||
include::{modulith-docs}/module-order.adoc[]
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Spring Modulith example application
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String... args) {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.inventory;
|
||||
|
||||
import example.order.OrderCompleted;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.modulith.ApplicationModuleListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* A Spring {@link Service} exposed by the inventory module.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
class InventoryManagement {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InventoryManagement.class);
|
||||
|
||||
private final ApplicationEventPublisher events;
|
||||
|
||||
@ApplicationModuleListener
|
||||
void on(OrderCompleted event) throws InterruptedException {
|
||||
|
||||
var orderId = event.orderId();
|
||||
|
||||
LOG.info("Received order completion for {}.", orderId);
|
||||
|
||||
// Simulate busy work
|
||||
Thread.sleep(1000);
|
||||
events.publishEvent(new InventoryUpdated(orderId));
|
||||
|
||||
LOG.info("Finished order completion for {}.", orderId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.inventory;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public record InventoryUpdated(UUID orderId) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* The logical application module inventory implemented as a single-package module. Allows to hide application
|
||||
* components inside the module by using package scoped types.
|
||||
*
|
||||
* @see example.inventory.InventoryInternal
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
package example.inventory;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.order;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jmolecules.event.types.DomainEvent;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public record OrderCompleted(UUID orderId) implements DomainEvent {}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.order;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrderManagement {
|
||||
|
||||
private final @NonNull ApplicationEventPublisher events;
|
||||
|
||||
@Transactional
|
||||
public void complete() {
|
||||
events.publishEvent(new OrderCompleted(UUID.randomUUID()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* The logical application module order implemented as a multi-package module. Internal components located in nested
|
||||
* packages are prevented from being accessed by the {@link org.springframework.modulith.core.ApplicationModules} type.
|
||||
*
|
||||
* @see example.ModularityTests
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
package example.order;
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example;
|
||||
|
||||
import example.inventory.InventoryUpdated;
|
||||
import example.order.OrderManagement;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.cypherdsl.core.renderer.Configuration;
|
||||
import org.neo4j.cypherdsl.core.renderer.Dialect;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.modulith.events.core.EventPublicationRegistry;
|
||||
import org.springframework.modulith.test.EnableScenarios;
|
||||
import org.springframework.modulith.test.Scenario;
|
||||
import org.testcontainers.containers.Neo4jContainer;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@SpringBootTest
|
||||
@EnableScenarios
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class ApplicationIntegrationTests {
|
||||
|
||||
@TestConfiguration
|
||||
static class MongoDbInfrastructureConfiguration {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
Neo4jContainer<?> neo4jContainer() {
|
||||
return new Neo4jContainer<>("neo4j:5").withRandomPassword();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Driver driver(Neo4jContainer<?> container) {
|
||||
return GraphDatabase.driver(container.getBoltUrl(), AuthTokens.basic("neo4j", container.getAdminPassword()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Configuration cypherDslConfiguration() {
|
||||
return Configuration.newConfig().withDialect(Dialect.NEO4J_5).build();
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired OrderManagement orders;
|
||||
@Autowired EventPublicationRegistry registry;
|
||||
|
||||
@Test
|
||||
void bootstrapsApplication(Scenario scenario) throws Exception {
|
||||
|
||||
scenario.stimulate(() -> orders.complete())
|
||||
.andWaitForStateChange(() -> registry.findIncompletePublications(), Collection::isEmpty)
|
||||
.andExpect(InventoryUpdated.class)
|
||||
.toArrive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.modulith.core.ApplicationModules;
|
||||
import org.springframework.modulith.docs.Documenter;
|
||||
|
||||
/**
|
||||
* Tests to verify the modular structure and generate documentation for the modules.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ModularityTests {
|
||||
|
||||
ApplicationModules modules = ApplicationModules.of(Application.class);
|
||||
|
||||
@Test
|
||||
void verifiesModularStructure() {
|
||||
modules.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createModuleDocumentation() {
|
||||
new Documenter(modules).writeDocumentation();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<module>spring-modulith-starter-jdbc</module>
|
||||
<module>spring-modulith-starter-jpa</module>
|
||||
<module>spring-modulith-starter-mongodb</module>
|
||||
<module>spring-modulith-starter-neo4j</module>
|
||||
<module>spring-modulith-starter-test</module>
|
||||
</modules>
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starters</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-modulith-starter-neo4j</artifactId>
|
||||
<name>Spring Modulith - Starters - Starter Neo4j</name>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springframework.modulith.starter.neo4j</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-starter-core</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Events -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events-api</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events-core</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events-jackson</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.modulith</groupId>
|
||||
<artifactId>spring-modulith-events-neo4j</artifactId>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
Reference in New Issue
Block a user