diff --git a/CHANGELOG.md b/CHANGELOG.md index 202c865d3..6ff6d81d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Add Contract Termination Observer +- MDS only + - Log contract termination events in the logging house + #### Patch Changes ### Deployment Migration Notes diff --git a/build.gradle.kts b/build.gradle.kts index 9ee6fa69e..d8cb980a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -129,7 +129,7 @@ subprojects { tasks.register("printClasspath") { group = libs.versions.edcGroup.get() description = "The EdcRuntimeExtension JUnit Extension requires the gradle task 'printClasspath'" - println(sourceSets.main.get().runtimeClasspath.asPath); + println(sourceSets.main.get().runtimeClasspath.asPath) } java { diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java index 36a151b5b..a8c8f7bf7 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractAgreementTerminationService.java @@ -16,15 +16,20 @@ import de.sovity.edc.extension.contacttermination.query.ContractAgreementTerminationDetailsQuery; import de.sovity.edc.extension.contacttermination.query.TerminateContractQuery; +import de.sovity.edc.extension.messenger.SovityMessage; import de.sovity.edc.extension.messenger.SovityMessenger; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.val; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.observe.Observable; +import org.eclipse.edc.spi.observe.ObservableImpl; import org.jetbrains.annotations.Nullable; import org.jooq.DSLContext; import java.time.OffsetDateTime; +import java.util.function.Consumer; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.COUNTERPARTY; import static de.sovity.edc.ext.db.jooq.enums.ContractTerminatedBy.SELF; @@ -37,15 +42,20 @@ public class ContractAgreementTerminationService { private final TerminateContractQuery terminateContractQuery; private final Monitor monitor; private final String thisParticipantId; + @Getter + private final Observable contractTerminationObservable = new ObservableImpl<>(); /** * This is to terminate an EDC's own contract. * If the termination comes from an external system, use - * {@link #terminateCounterpartyAgreement(DSLContext, String, ContractTerminationParam)} + * {@link #terminateAgreementAsCounterparty(DSLContext, String, ContractTerminationParam)} * to validate the counter-party's identity. */ public OffsetDateTime terminateAgreementOrThrow(DSLContext dsl, ContractTerminationParam termination) { + val starterEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), thisParticipantId); + notifyObservers(it -> it.contractTerminationStartedFromThisInstance(starterEvent)); + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); if (details == null) { @@ -58,16 +68,22 @@ public OffsetDateTime terminateAgreementOrThrow(DSLContext dsl, ContractTerminat val terminatedAt = terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, SELF); + val endEvent = ContractTerminationEvent.from(termination, terminatedAt, thisParticipantId); + notifyObservers(it -> it.contractTerminationCompletedOnThisInstance(endEvent)); + notifyTerminationToProvider(details.counterpartyAddress(), termination); return terminatedAt; } - public OffsetDateTime terminateCounterpartyAgreement( + public OffsetDateTime terminateAgreementAsCounterparty( DSLContext dsl, @Nullable String identity, ContractTerminationParam termination ) { + val starterEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), null); + notifyObservers(it -> it.contractTerminatedByCounterpartyStarted(starterEvent)); + val details = contractAgreementTerminationDetailsQuery.fetchAgreementDetailsOrThrow(dsl, termination.contractAgreementId()); if (details == null) { @@ -90,15 +106,35 @@ public OffsetDateTime terminateCounterpartyAgreement( val agent = thisParticipantId.equals(details.counterpartyId()) ? SELF : COUNTERPARTY; - return terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, agent); + val result = terminateContractQuery.terminateConsumerAgreementOrThrow(dsl, termination, agent); + + val endEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), details.counterpartyId()); + notifyObservers(it -> it.contractTerminatedByCounterparty(endEvent)); + + return result; } public void notifyTerminationToProvider(String counterPartyAddress, ContractTerminationParam termination) { + + val notificationEvent = ContractTerminationEvent.from(termination, OffsetDateTime.now(), null); + notifyObservers(it -> it.contractTerminationOnCounterpartyStarted(notificationEvent)); + sovityMessenger.send( + SovityMessage.class, counterPartyAddress, new ContractTerminationMessage( termination.contractAgreementId(), termination.detail(), termination.reason())); } + + private void notifyObservers(Consumer call) { + for (val listener : contractTerminationObservable.getListeners()) { + try { + call.accept(listener); + } catch (Exception e) { + monitor.warning("Failure when notifying the contract termination listener.", e); + } + } + } } diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java new file mode 100644 index 000000000..4def044dc --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +import java.time.OffsetDateTime; + +public record ContractTerminationEvent( + String contractAgreementId, + String detail, + String reason, + OffsetDateTime timestamp, + String origin +) { + public static ContractTerminationEvent from(ContractTerminationParam contractTerminationParam, OffsetDateTime dateTime, String origin) { + return new ContractTerminationEvent( + contractTerminationParam.contractAgreementId(), + contractTerminationParam.detail(), + contractTerminationParam.reason(), + dateTime, + origin + ); + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java index cad805be9..84bb39b0f 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationExtension.java @@ -91,7 +91,7 @@ private void setupMessenger(ContractAgreementTerminationService terminationServi ContractTerminationMessage.class, (claims, termination) -> dslContextFactory.transactionResult(dsl -> - terminationService.terminateCounterpartyAgreement( + terminationService.terminateAgreementAsCounterparty( dsl, participantAgentService.createFor(claims).getIdentity(), buildTerminationRequest(termination)))); diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java new file mode 100644 index 000000000..ef9cee608 --- /dev/null +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/ContractTerminationObserver.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.contacttermination; + +public interface ContractTerminationObserver { + + /** + * Indicates that a contract termination was started by this EDC. + */ + default void contractTerminationStartedFromThisInstance(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that the first step to terminate a contract, terminating a contract on this EDC instance itself, was successful. + * The contract is now marked as terminated on this EDC's side. + */ + default void contractTerminationCompletedOnThisInstance(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination on the counterparty EDC was started. + */ + default void contractTerminationOnCounterpartyStarted(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination was started by a counterparty EDC terminated successfully + */ + default void contractTerminatedByCounterpartyStarted(ContractTerminationEvent contractTerminationEvent) { + } + + /** + * Indicates that a contract termination initiated by a counterparty EDC terminated successfully + * The contract is now marked as terminated on this EDC. + */ + default void contractTerminatedByCounterparty(ContractTerminationEvent contractTerminationEvent) { + } +} diff --git a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java index f404eb92c..5873784b8 100644 --- a/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java +++ b/extensions/contract-termination/src/main/java/de/sovity/edc/extension/contacttermination/query/TerminateContractQuery.java @@ -31,8 +31,8 @@ public class TerminateContractQuery { public OffsetDateTime terminateConsumerAgreementOrThrow( DSLContext dsl, ContractTerminationParam termination, - ContractTerminatedBy terminatedBy) { - + ContractTerminatedBy terminatedBy + ) { val tooAccurate = OffsetDateTime.now(); val now = tooAccurate.truncatedTo(ChronoUnit.MICROS); diff --git a/extensions/mds-logginghouse-binder/README.md b/extensions/mds-logginghouse-binder/README.md new file mode 100644 index 000000000..6330791e8 --- /dev/null +++ b/extensions/mds-logginghouse-binder/README.md @@ -0,0 +1,44 @@ + +
+
+ + Logo + + +

EDC-Connector Extension:
MDS Contract Termination - LoggingHouse binder

+ +

+ Report Bug + ยท + Request Feature +

+
+ + +## About this Extension + +It links the Contract Termination events with the LoggingHouse. + +## Why does this extension exist? + +MDS needs to log the events generated when terminating a contract with their Logging House extension. +The Logging House is an external dependency and the linkage must only happen for the MDS variant. + +This extension implements this specific task. + +## Architecture + +```mermaid +flowchart TD + Binder(MDS LoggingHouse Binder) --> LoggingHouse(Logging House Extension) + Binder(MDS LoggingHouse Binder) --> ContractTermination(Contract Termination Extension) + MDS(MDS CE) --> Binder +``` + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/mds-logginghouse-binder/build.gradle.kts b/extensions/mds-logginghouse-binder/build.gradle.kts new file mode 100644 index 000000000..bd378ea58 --- /dev/null +++ b/extensions/mds-logginghouse-binder/build.gradle.kts @@ -0,0 +1,62 @@ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + + implementation(project(":extensions:contract-termination")) + + implementation(libs.edc.coreSpi) + implementation(libs.edc.dspNegotiationTransform) + implementation(libs.edc.transferSpi) + + implementation(libs.loggingHouse.client) + implementation(libs.jakarta.rsApi) + + + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + + testImplementation(project(":extensions:postgres-flyway")) + testImplementation(project(":utils:test-utils")) + testImplementation(project(":utils:versions")) + + testImplementation(libs.edc.monitorJdkLogger) + testImplementation(libs.edc.http) { + exclude(group = "org.eclipse.jetty", module = "jetty-client") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-util") + exclude(group = "org.eclipse.jetty", module = "jetty-webapp") + } + + // Updated jetty versions for e.g. CVE-2023-26048 + testImplementation(libs.bundles.jetty.cve2023) + + testImplementation(libs.assertj.core) + testImplementation(libs.flyway.core) + testImplementation(libs.junit.api) + testImplementation(libs.hibernate.validation) + testImplementation(libs.jakarta.el) + testImplementation(libs.mockito.core) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.postgresql) + + testRuntimeOnly(libs.junit.engine) +} + +group = libs.versions.sovityEdcExtensionGroup.get() + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java new file mode 100644 index 000000000..0ec722beb --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LogEntry.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@AllArgsConstructor +@Builder +@Data +@NoArgsConstructor +public class LogEntry { + + @JsonProperty("contractAgreementId") + private String contractAgreementId; + + @JsonProperty("event") + private String event; + + @JsonProperty("detail") + private String detail; + + @JsonProperty("reason") + private String reason; + + @JsonProperty("timestamp") + private OffsetDateTime timestamp; + + public static LogEntry from(String event, ContractTerminationEvent contractTerminationEvent) { + return LogEntry.builder() + .event(event) + .contractAgreementId(contractTerminationEvent.contractAgreementId()) + .detail(contractTerminationEvent.detail()) + .reason(contractTerminationEvent.reason()) + .timestamp(contractTerminationEvent.timestamp()) + .build(); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java new file mode 100644 index 000000000..6ca7c24a1 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/LoggingHouseException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +public class LoggingHouseException extends RuntimeException { + public LoggingHouseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java new file mode 100644 index 000000000..0237f5fa2 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.truzzt.extension.logginghouse.client.events.CustomLoggingHouseEvent; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MdsContractTerminationEvent extends CustomLoggingHouseEvent { + private final String eventId; + private final String processId; + private final String messageBody; +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java new file mode 100644 index 000000000..3ba115efb --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsContractTerminationObserver.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import de.sovity.edc.extension.contacttermination.ContractTerminationObserver; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.uuid.UuidGenerator; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class MdsContractTerminationObserver implements ContractTerminationObserver { + + private final EventRouter eventRouter; + + private final Monitor monitor; + + private final ObjectMapper objectMapper; + + @Override + public void contractTerminationCompletedOnThisInstance(ContractTerminationEvent contractTerminationEvent) { + sendMessage("contractTerminatedByThisInstance", contractTerminationEvent); + } + + @Override + public void contractTerminatedByCounterparty(ContractTerminationEvent contractTerminationEvent) { + sendMessage("contractTerminatedByCounterparty", contractTerminationEvent); + } + + private void sendMessage(String logEvent, ContractTerminationEvent contractTerminationEvent) { + val logEntry = LogEntry.from(logEvent, contractTerminationEvent); + try { + val event = buildLogEvent(contractTerminationEvent.contractAgreementId(), logEntry); + publishEvent(event); + monitor.debug("Published event for " + logEntry); + } catch (JsonProcessingException e) { + val message = "Failed to serialize the event for the LoggingHouse " + logEntry; + throw new LoggingHouseException(message, e); + } + } + + private @NotNull MdsContractTerminationEvent buildLogEvent(String contractAgreementId, LogEntry logEntry) + throws JsonProcessingException { + val message = objectMapper.writeValueAsString(logEntry); + return new MdsContractTerminationEvent( + UuidGenerator.INSTANCE.generate().toString(), + contractAgreementId, + message + ); + } + + private void publishEvent(MdsContractTerminationEvent event) { + @SuppressWarnings("unchecked") + EventEnvelope.Builder builder = EventEnvelope.Builder.newInstance(); + + val eventEnvelope = builder + .at(System.currentTimeMillis()) + .payload(event) + .build(); + + eventRouter.publish(eventEnvelope); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java new file mode 100644 index 000000000..8fd10a763 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/java/de/sovity/edc/extension/mdslogginhousebinder/MdsLoggingHouseBinder.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.extension.mdslogginhousebinder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import lombok.val; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +public class MdsLoggingHouseBinder implements ServiceExtension { + + @Inject + private EventRouter eventRouter; + + @Inject + private Monitor monitor; + + @Inject + private ContractAgreementTerminationService contractAgreementTerminationService; + + @Override + public void initialize(ServiceExtensionContext context) { + setupLoggingHouseTerminationEventsLogging(); + } + + private void setupLoggingHouseTerminationEventsLogging() { + val objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + contractAgreementTerminationService.getContractTerminationObservable() + .registerListener(new MdsContractTerminationObserver(eventRouter, monitor, objectMapper)); + } +} diff --git a/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..6ac768693 --- /dev/null +++ b/extensions/mds-logginghouse-binder/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.mdslogginhousebinder.MdsLoggingHouseBinder diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5a766691..b33b3facf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ json = "20220924" jsonAssert = "1.5.1" jsonUnit = "3.2.7" junit = "5.10.0" -loggingHouse = "v1.1.0" +loggingHouse = "v1.2.0-alpha.1" lombok = "1.18.30" mockito = "5.12.0" mockserver = "5.15.0" diff --git a/launchers/common/base-mds/build.gradle.kts b/launchers/common/base-mds/build.gradle.kts index 0faceaa8d..89d1a8bd8 100644 --- a/launchers/common/base-mds/build.gradle.kts +++ b/launchers/common/base-mds/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { implementation(libs.loggingHouse.client) + implementation(project(":extensions:mds-logginghouse-binder")) } group = libs.versions.sovityEdcGroup.get() diff --git a/settings.gradle.kts b/settings.gradle.kts index 78a0903b2..003fd30ef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ include(":extensions:contract-termination") include(":extensions:database-direct-access") include(":extensions:edc-ui-config") include(":extensions:last-commit-info") +include(":extensions:mds-logginghouse-binder") include(":extensions:policy-always-true") include(":extensions:policy-referring-connector") include(":extensions:policy-time-interval") diff --git a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java index 525e5669d..e9347079d 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/AlwaysTrueMigrationReversedTest.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; diff --git a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java index bc4d8bb10..81f06a375 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/ContractTerminationTest.java @@ -22,6 +22,9 @@ import de.sovity.edc.client.gen.model.ContractTerminationRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.TransferHistoryEntry; +import de.sovity.edc.extension.contacttermination.ContractAgreementTerminationService; +import de.sovity.edc.extension.contacttermination.ContractTerminationEvent; +import de.sovity.edc.extension.contacttermination.ContractTerminationObserver; import de.sovity.edc.extension.e2e.extension.Consumer; import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; @@ -34,10 +37,13 @@ import org.awaitility.Awaitility; import org.eclipse.edc.connector.contract.spi.ContractId; import org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates; +import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @@ -59,6 +65,8 @@ import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; public class ContractTerminationTest { @@ -70,8 +78,8 @@ public class ContractTerminationTest { void canGetAgreementPageForNonTerminatedContract( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assets = IntStream.range(0, 3).mapToObj((it) -> scenario.createAsset()); val agreements = assets @@ -118,8 +126,8 @@ void canGetAgreementPageForNonTerminatedContract( void canGetAgreementPageForTerminatedContract( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); scenario.negotiateAssetAndAwait(assetId); @@ -151,7 +159,6 @@ void canTerminateFromConsumer( @Consumer EdcClient consumerClient, @Provider EdcClient providerClient ) { - val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -180,8 +187,8 @@ void canTerminateFromConsumer( void limitTheReasonSizeAt100Chars( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -221,8 +228,8 @@ void limitTheReasonSizeAt100Chars( void limitTheDetailSizeAt1000Chars( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -264,8 +271,7 @@ void limitTheDetailSizeAt1000Chars( @TestFactory List theDetailsAreMandatory( E2eScenario scenario, - @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient + @Consumer EdcClient consumerClient ) { val invalidDetails = List.of( "", @@ -305,8 +311,8 @@ List theDetailsAreMandatory( void canTerminateFromProvider( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -337,7 +343,8 @@ void canTerminateFromProvider( @Test void doesntCrashWhenAgreementDoesntExist( - @Consumer EdcClient consumerClient) { + @Consumer EdcClient consumerClient + ) { // act assertThrows( ApiException.class, @@ -353,8 +360,8 @@ void cantTransferDataAfterTerminated( E2eScenario scenario, ClientAndServer mockServer, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = "asset-1"; val mockedAsset = scenario.createAssetWithMockResource(assetId); scenario.createContractDefinition(assetId); @@ -417,8 +424,8 @@ void cantTransferDataAfterTerminated( void canTerminateOnlyOnce( E2eScenario scenario, @Consumer EdcClient consumerClient, - @Provider EdcClient providerClient) { - + @Provider EdcClient providerClient + ) { val assetId = scenario.createAsset(); scenario.createContractDefinition(assetId); val negotiation = scenario.negotiateAssetAndAwait(assetId); @@ -439,12 +446,92 @@ void canTerminateOnlyOnce( assertThat(alreadyExists.getLastUpdatedDate()).isEqualTo(firstTermination.getLastUpdatedDate()); } + @SneakyThrows + @Test + void canListenToTerminationEvents( + E2eScenario scenario, + @Consumer EdcClient consumerClient, + @Consumer EdcExtension consumerExtension, + @Provider EdcClient providerClient, + @Provider EdcExtension providerExtension + ) { + // arrange + val assetId = scenario.createAsset(); + scenario.createContractDefinition(assetId); + val negotiation = scenario.negotiateAssetAndAwait(assetId); + + val detail = "Some detail"; + val reason = "Some reason"; + val contractTerminationRequest = ContractTerminationRequest.builder().detail(detail).reason(reason).build(); + val contractAgreementId = negotiation.getContractAgreementId(); + + val consumerService = consumerExtension.getContext().getService(ContractAgreementTerminationService.class); + val providerService = providerExtension.getContext().getService(ContractAgreementTerminationService.class); + + val consumerObserver = Mockito.spy(new ContractTerminationObserver() { + }); + val providerObserver = Mockito.spy(new ContractTerminationObserver() { + }); + + consumerService.getContractTerminationObservable().registerListener(consumerObserver); + providerService.getContractTerminationObservable().registerListener(providerObserver); + + // act + + consumerClient.uiApi().terminateContractAgreement(contractAgreementId, contractTerminationRequest); + + awaitTerminationCount(consumerClient, 1); + awaitTerminationCount(providerClient, 1); + + Thread.sleep(2000); + + // assert + + assertThat(consumerService.getContractTerminationObservable().getListeners()).hasSize(1); + assertThat(providerService.getContractTerminationObservable().getListeners()).hasSize(1); + + ArgumentCaptor argument = ArgumentCaptor.forClass(ContractTerminationEvent.class); + + verify(consumerObserver).contractTerminationStartedFromThisInstance(argument.capture()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(consumerObserver).contractTerminationCompletedOnThisInstance(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(consumerObserver).contractTerminationOnCounterpartyStarted(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(providerObserver).contractTerminatedByCounterpartyStarted(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + verify(providerObserver).contractTerminatedByCounterparty(any()); + assertTerminationEvent(argument, contractAgreementId, contractTerminationRequest); + + // act + + consumerService.getContractTerminationObservable().unregisterListener(consumerObserver); + providerService.getContractTerminationObservable().unregisterListener(providerObserver); + + // assert + + assertThat(consumerService.getContractTerminationObservable().getListeners()).hasSize(0); + assertThat(providerService.getContractTerminationObservable().getListeners()).hasSize(0); + } + + private static void assertTerminationEvent(ArgumentCaptor argument, String contractAgreementId, + ContractTerminationRequest contractTerminationRequest) { + assertThat(argument.getValue().contractAgreementId()).isEqualTo(contractAgreementId); + assertThat(argument.getValue().detail()).isEqualTo(contractTerminationRequest.getDetail()); + assertThat(argument.getValue().reason()).isEqualTo(contractTerminationRequest.getReason()); + assertThat(argument.getValue().timestamp()).isNotNull(); + } + private static void assertTermination( ContractAgreementPage consumerSideAgreements, String detail, String reason, - ContractTerminatedBy terminatedBy) { - + ContractTerminatedBy terminatedBy + ) { val contractAgreements = consumerSideAgreements.getContractAgreements(); assertThat(contractAgreements).hasSize(1); assertThat(contractAgreements.get(0).getTerminationStatus()).isEqualTo(TERMINATED);