diff --git a/.editorconfig b/.editorconfig index 4476e7487..5d2208c46 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_size = 4 +ij_continuation_indent_size = 4 [*.ts] quote_type = single diff --git a/CHANGELOG.md b/CHANGELOG.md index 47082e289..24fd6e084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Add the SovityMessenger extension + #### Patch Changes - Unified database migration histories diff --git a/docs/dev/checkstyle/checkstyle-config.xml b/docs/dev/checkstyle/checkstyle-config.xml index dc848298a..df0eaafa4 100644 --- a/docs/dev/checkstyle/checkstyle-config.xml +++ b/docs/dev/checkstyle/checkstyle-config.xml @@ -267,7 +267,7 @@ - + diff --git a/extensions/sovity-messenger/README.md b/extensions/sovity-messenger/README.md new file mode 100644 index 000000000..4d8a46a25 --- /dev/null +++ b/extensions/sovity-messenger/README.md @@ -0,0 +1,72 @@ + +
+
+ + Logo + + +

EDC-Connector Extension:
Sovity Messenger

+ +

+ Report Bug + ยท + Request Feature +

+
+ + +## About this Extension + +To provide a simpler way to exchange messages between EDCs while re-using the Dataspace's Connector-to-Connector authentication mechanisms, we created our own extension with a much simpler API surface omitting JSON-LD. + +## Why does this extension exist? + +Adding custom DSP messages to a vanilla EDC is verbose and requires the handling of JSON-LD and implementing your own Transformers. Since we do not care about JSON-LD we wanted a simpler API surface. + +## Architecture + +The sovity Messenger is implemented on top of the DSP messaging protocol and re-uses its exchange and authentication. + +It is abstracted from the internals of the DSP protocol such that changing the underlying implementation remains an option. + + +```mermaid +--- +title: Registering a handler +--- +sequenceDiagram + Caller ->> SovityMessengerRegistry: register(inputClass, intputType, handler) +``` + +```mermaid +--- +title: Sending a message +--- +sequenceDiagram + Caller ->>+SovityMessenger: send(resultClass, counterPartyAddress, payload) + SovityMessenger -->> RemoteMessageDispatcherRegistry: dispatch(genericMessage) + SovityMessenger -->> -Caller: Future + RemoteMessageDispatcherRegistry ->> +CustomMessageReceiverController: <> + CustomMessageReceiverController ->> CustomMessageReceiverController: processMessage(handler, payload) + CustomMessageReceiverController -->> -RemoteMessageDispatcherRegistry: <> + RemoteMessageDispatcherRegistry -->> SovityMessenger: <> + SovityMessenger ->> +Caller: Future + Caller ->> -Caller: future.get() +``` + +## Demo + +You can find a demo in the [demo](src/test/java/de/sovity/edc/extension/messenger/demo). + +The 2 key entry points are: + +- Register your message receiving by talking to the SovityMessageRegistry as demonstrated [here](src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java). +- Send messages by calling the SovityMessenger as shown [here](src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java). + +## License + +Apache License 2.0 - see [LICENSE](../../LICENSE) + +## Contact + +sovity GmbH - contact@sovity.de diff --git a/extensions/sovity-messenger/build.gradle.kts b/extensions/sovity-messenger/build.gradle.kts new file mode 100644 index 000000000..815865804 --- /dev/null +++ b/extensions/sovity-messenger/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + `java-library` + `maven-publish` +} + +dependencies { + annotationProcessor(libs.lombok) + + compileOnly(libs.lombok) + + implementation(project(":utils:json-and-jsonld-utils")) + + implementation(libs.edc.controlPlaneCore) + implementation(libs.edc.dspApiConfiguration) + implementation(libs.edc.dspHttpSpi) + implementation(libs.edc.httpSpi) + implementation(libs.edc.managementApiConfiguration) + implementation(libs.edc.transformCore) + + + testAnnotationProcessor(libs.lombok) + + testCompileOnly(libs.lombok) + + testImplementation(project(":utils:test-connector-remote")) + testImplementation(project(":utils:test-utils")) + + testImplementation(libs.edc.junit) + testImplementation(libs.edc.dataPlaneSelectorCore) + testImplementation(libs.edc.dspApiConfiguration) + testImplementation(libs.edc.dspHttpCore) + testImplementation(libs.edc.iamMock) + testImplementation(libs.edc.jsonLd) + + 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.junit.api) + testImplementation(libs.jsonAssert) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockserver.netty) + testImplementation(libs.restAssured.restAssured) + testImplementation(libs.testcontainers.testcontainers) + testImplementation(libs.testcontainers.junitJupiter) + testImplementation(libs.testcontainers.postgresql) + + testRuntimeOnly(libs.junit.engine) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java new file mode 100644 index 000000000..04d07f0a1 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessage.java @@ -0,0 +1,38 @@ +/* + * 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.messenger; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The interface to implement when sending a message via the {@link SovityMessenger}. + *
+ * The classes extending this interface must annotate the private fields to be sent with Jackson's + * {@link com.fasterxml.jackson.annotation.JsonProperty}. + * {@code public} fields are serialized automatically. + *
+ * It is recommended to have a no-args constructor. + *
+ * See this doc + * for more detailed info about Jackson's serialization. + */ +public interface SovityMessage { + /** + * To avoid conflicts, it is recommended to use Java package-like naming convention. + * + * @return A unique string across all possible messages to identify the type of message. + */ + @JsonIgnore + String getType(); +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java new file mode 100644 index 000000000..82dbfe54d --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessenger.java @@ -0,0 +1,117 @@ +/* + * 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.messenger; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.SovityMessengerStatus; +import de.sovity.edc.utils.JsonUtils; +import jakarta.json.Json; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.response.StatusResult; +import org.jetbrains.annotations.NotNull; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * The service to send {@link SovityMessage}s. + */ +@RequiredArgsConstructor +public class SovityMessenger { + + private final RemoteMessageDispatcherRegistry registry; + + private final ObjectMapper serializer; + + /** + * Sends a message to the counterparty address and returns a future result. + * + * @param resultType The result's class. + * @param counterPartyAddress The base DSP URL where to send the message. e.g. https://server:port/api/dsp + * @param payload The message to send. + * @param The outgoing message type. + * @param The incoming message type. + * @return A future result. + * @throws SovityMessengerException If a problem related to the message processing happened. + */ + public CompletableFuture> send( + Class resultType, String counterPartyAddress, T payload) { + try { + val message = buildMessage(counterPartyAddress, payload); + val future = registry.dispatch(SovityMessageRequest.class, message); + return future.thenApply(processResponse(resultType, payload)); + } catch (URISyntaxException | MalformedURLException | JsonProcessingException e) { + throw new EdcException("Failed to build a custom sovity message", e); + } + } + + static class Discarded implements SovityMessage { + @Override + public String getType() { + return "de.sovity.edc.extension.messenger.impl.SovityMessengerImpl.Discarded"; + } + } + + /** + * Fire-and-forget messaging where you don't care about the response. + */ + public void send(String counterPartyAddress, T payload) { + send(Discarded.class, counterPartyAddress, payload); + } + + @NotNull + private Function, StatusResult> processResponse( + Class resultType, T payload) { + return statusResult -> statusResult.map(content -> { + try { + val headerStr = content.header(); + val header = JsonUtils.parseJsonObj(headerStr); + if (header.getString("status").equals(SovityMessengerStatus.OK.getCode())) { + val resultBody = content.body(); + return serializer.readValue(resultBody, resultType); + } else if (header.getString("status").equals(SovityMessengerStatus.HANDLER_EXCEPTION.getCode())) { + throw new SovityMessengerException( + header.getString("message"), + header.getString(SovityMessengerStatus.HANDLER_EXCEPTION.getCode(), "No outgoing body."), + payload); + } else { + throw new SovityMessengerException(header.getString("message")); + } + } catch (JsonProcessingException e) { + throw new EdcException(e); + } + }); + } + + @NotNull + private SovityMessageRequest buildMessage(String counterPartyAddress, T payload) + throws MalformedURLException, URISyntaxException, JsonProcessingException { + val url = new URI(counterPartyAddress).toURL(); + val header1 = Json.createObjectBuilder() + .add("type", payload.getType()) + .build(); + val header = JsonUtils.toJson(header1); + val serialized = serializer.writeValueAsString(payload); + return new SovityMessageRequest(url, header, serialized); + } + +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java new file mode 100644 index 000000000..2de888e30 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerException.java @@ -0,0 +1,32 @@ +/* + * 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.messenger; + +import lombok.Getter; + +public class SovityMessengerException extends RuntimeException { + + @Getter + private String body; + private Object payload; + + public SovityMessengerException(String message) { + super(message); + } + + public SovityMessengerException(String message, String body, Object payload) { + super(message); + this.body = body; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java new file mode 100644 index 000000000..5a01dc7c0 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerExtension.java @@ -0,0 +1,111 @@ +/* + * 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.messenger; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.controller.SovityMessageController; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.MessageEmitter; +import de.sovity.edc.extension.messenger.impl.MessageReceiver; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import lombok.val; +import org.eclipse.edc.protocol.dsp.api.configuration.DspApiConfiguration; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpRemoteMessageDispatcher; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.web.spi.WebService; + +@Provides({SovityMessenger.class, SovityMessengerRegistry.class}) +public class SovityMessengerExtension implements ServiceExtension { + + public static final String NAME = "SovityMessenger"; + + @Inject + private DspApiConfiguration dspApiConfiguration; + + @Inject + private DspHttpRemoteMessageDispatcher dspHttpRemoteMessageDispatcher; + + @Inject + private IdentityService identityService; + + @Inject + private JsonLdRemoteMessageSerializer jsonLdRemoteMessageSerializer; + + @Inject + private Monitor monitor; + + @Inject + private RemoteMessageDispatcherRegistry registry; + + @Inject + private TypeManager typeManager; + + @Inject + private TypeTransformerRegistry typeTransformerRegistry; + + @Inject + private WebService webService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + val objectMapper = new ObjectMapperFactory().createObjectMapper(); + val handlers = new SovityMessengerRegistry(); + setupSovityMessengerEmitter(context, objectMapper); + setupSovityMessengerReceiver(context, objectMapper, handlers); + } + + private void setupSovityMessengerEmitter(ServiceExtensionContext context, ObjectMapper objectMapper) { + val factory = new MessageEmitter(jsonLdRemoteMessageSerializer); + val delegate = new MessageReceiver(objectMapper); + + dspHttpRemoteMessageDispatcher.registerMessage(SovityMessageRequest.class, factory, delegate); + + typeTransformerRegistry.register(new JsonObjectFromSovityMessageRequest()); + + val sovityMessenger = new SovityMessenger(registry, objectMapper); + context.registerService(SovityMessenger.class, sovityMessenger); + } + + private void setupSovityMessengerReceiver(ServiceExtensionContext context, ObjectMapper objectMapper, SovityMessengerRegistry handlers) { + val receiver = new SovityMessageController( + identityService, + dspApiConfiguration.getDspCallbackAddress(), + typeTransformerRegistry, + monitor, + objectMapper, + handlers); + + webService.registerResource(dspApiConfiguration.getContextAlias(), receiver); + + context.registerService(SovityMessengerRegistry.class, handlers); + + typeTransformerRegistry.register(new JsonObjectFromSovityMessageResponse()); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java new file mode 100644 index 000000000..933dc6a1f --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/SovityMessengerRegistry.java @@ -0,0 +1,102 @@ +/* + * 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.messenger; + +import de.sovity.edc.extension.messenger.impl.Handler; +import lombok.SneakyThrows; +import lombok.val; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * The component where to register message handlers when using the {@link SovityMessenger}. + */ +public class SovityMessengerRegistry { + + private final Map> handlers = new HashMap<>(); + + /** + * Register a handler to process a sovity message. + * + * @param incomingMessage The incoming message type to register (what was sent). Must have a no-arg constructor. + * @param handler The function to process this type of message. + * @param Incoming message type. + * @param Outgoing message type you send with {@link SovityMessenger#send(Class, String, SovityMessage)}. + */ + @SneakyThrows + public void register(Class incomingMessage, Function handler) { + val type = getTypeViaIntrospection(incomingMessage); + register(incomingMessage, type, handler); + } + + /** + * Registers a signal. This is a simplified version of a message where no answer is expected. + * + * @param incomingSignal The signal to send. + * @param handler Signal processing + */ + @SneakyThrows + public void registerSignal(Class incomingSignal, Consumer handler) { + val type = getTypeViaIntrospection(incomingSignal); + registerSignal(incomingSignal, type, handler); + } + + /** + * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using + * {@link #register(Class, Function)} for type safety. + */ + public void register(Class clazz, String type, Function handler) { + if (handlers.containsKey(type)) { + throw new IllegalStateException("A handler is already registered for " + type); + } + + handlers.put(type, new Handler<>(clazz, handler)); + } + + /** + * Use this constructor only if your message can't have a default constructor. Otherwise, prefer using + * {@link #registerSignal(Class, Consumer)} for type safety. + */ + public void registerSignal(Class clazz, String type, Consumer handler) { + if (handlers.containsKey(type)) { + throw new IllegalStateException("A handler is already registered for " + type); + } + register(clazz, type, in -> { + handler.accept(in); + return null; + }); + } + + /** + * Retrieve a handler for the specified message type. + * + * @param type A unique ID to identify a message type. + * @return The function to process this message type. + */ + @SuppressWarnings("unchecked") + public Handler getHandler(String type) { + return (Handler) handlers.get(type); + } + + private static String getTypeViaIntrospection(Class incomingMessage) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + val defaultConstructor = incomingMessage.getConstructor(); + defaultConstructor.setAccessible(true); + val type = defaultConstructor.newInstance().getType(); + return type; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java new file mode 100644 index 000000000..5acc77c21 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/controller/SovityMessageController.java @@ -0,0 +1,178 @@ +/* + * 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.messenger.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.SovityMessage; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.impl.Handler; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.SovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.SovityMessengerStatus; +import de.sovity.edc.utils.JsonUtils; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.eclipse.edc.protocol.dsp.api.configuration.error.DspErrorResponse; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.UUID; + +import static de.sovity.edc.extension.messenger.controller.SovityMessageController.PATH; + +@RequiredArgsConstructor +@Path(PATH) +public class SovityMessageController { + + public static final String PATH = "/sovity/message/generic"; + + private final IdentityService identityService; + private final String callbackAddress; + private final TypeTransformerRegistry typeTransformerRegistry; + private final Monitor monitor; + private final ObjectMapper mapper; + + @Getter + private final SovityMessengerRegistry handlers; + + @POST + public Response post( + @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, + SovityMessageRequest request) { + + val validation = validateToken(authorization); + if (validation.failed()) { + return Response.status( + Response.Status.UNAUTHORIZED.getStatusCode(), + String.join(", ", validation.getFailureMessages()) + ).build(); + } + + val handler = getHandler(request); + if (handler == null) { + val errorAnswer = buildErrorNoHandlerHeader(request); + return Response.ok() + .type(MediaType.APPLICATION_JSON) + .entity(errorAnswer).build(); + } + + try { + val response = processMessage(request, handler); + + return typeTransformerRegistry.transform(response, JsonObject.class) + .map(it -> Response.ok().type(MediaType.APPLICATION_JSON).entity(it).build()) + .orElse(failure -> { + var errorCode = UUID.randomUUID(); + monitor.warning(String.format("Error transforming " + response.getClass().getSimpleName() + ", error id %s: %s", errorCode, failure.getFailureDetail())); + return DspErrorResponse + .type(Prop.SovityMessageExt.REQUEST) + .internalServerError(); + }); + } catch (Exception e) { + monitor.warning("Failed to process message with type " + getMessageType(request), e); + val errorAnswer = buildErrorHandlerExceptionHeader(request); + return Response.ok() + .type(MediaType.APPLICATION_JSON) + .entity(errorAnswer) + .build(); + } + } + + private SovityMessageResponse processMessage(SovityMessageRequest compacted, Handler handler) throws JsonProcessingException { + val bodyStr = compacted.body(); + val parsed = mapper.readValue(bodyStr, handler.clazz()); + val result = handler.handler().apply(parsed); + val resultBody = mapper.writeValueAsString(result); + + val response = new SovityMessageResponse( + buildOkHeader(handler.clazz()), + resultBody); + + return response; + } + + private String buildOkHeader(Class clazz) { + try { + Constructor constructor = clazz.getConstructor(); + constructor.setAccessible(true); + String type = ((SovityMessage) constructor.newInstance()).getType(); + JsonObject header = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.OK.getCode()) + .add("type", type) + .build(); + return JsonUtils.toJson(header); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new EdcException(e); + } + } + + private Result validateToken(String authorization) { + val token = TokenRepresentation.Builder.newInstance().token(authorization).build(); + return identityService.verifyJwtToken(token, callbackAddress); + } + + private SovityMessageResponse buildErrorNoHandlerHeader(SovityMessageRequest request) { + val messageType = getMessageType(request); + val json = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.NO_HANDLER.getCode()) + .add("message", "No handler for message type " + messageType) + .build(); + val headerStr = JsonUtils.toJson(json); + + return new SovityMessageResponse(headerStr, ""); + } + + private SovityMessageResponse buildErrorHandlerExceptionHeader(SovityMessageRequest request) { + val messageType = getMessageType(request); + val body = request.body(); + val json = Json.createObjectBuilder() + .add("status", SovityMessengerStatus.HANDLER_EXCEPTION.getCode()) + .add("message", "Error when processing a message with type " + messageType) + .add(SovityMessengerStatus.HANDLER_EXCEPTION.getCode(), body) + .build(); + val headerStr = JsonUtils.toJson(json); + + return new SovityMessageResponse(headerStr, ""); + } + + private Handler getHandler(SovityMessageRequest request) { + final var messageType = getMessageType(request); + return handlers.getHandler(messageType); + } + + private static String getMessageType(SovityMessageRequest request) { + val headerStr = request.header(); + val header = Json.createReader(new StringReader(headerStr)).readObject(); + return header.getString("type"); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java new file mode 100644 index 000000000..4e7ef993b --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/Handler.java @@ -0,0 +1,19 @@ +/* + * 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.messenger.impl; + +import java.util.function.Function; + +public record Handler(Class clazz, Function handler) { +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java new file mode 100644 index 000000000..baf15f102 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageRequest.java @@ -0,0 +1,44 @@ +/* + * 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.messenger.impl; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +public class JsonObjectFromSovityMessageRequest extends AbstractJsonLdTransformer { + + public JsonObjectFromSovityMessageRequest() { + super(SovityMessageRequest.class, JsonObject.class); + } + + @Override + public @Nullable JsonObject transform( + @NotNull SovityMessageRequest message, + @NotNull TransformerContext context) { + + var builder = Json.createObjectBuilder(); + builder.add(TYPE, Prop.SovityMessageExt.REQUEST) + .add(Prop.SovityMessageExt.HEADER, message.header()) + .add(Prop.SovityMessageExt.BODY, message.body()); + + return builder.build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java new file mode 100644 index 000000000..9ae6a42e8 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/JsonObjectFromSovityMessageResponse.java @@ -0,0 +1,44 @@ +/* + * 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.messenger.impl; + +import de.sovity.edc.utils.jsonld.vocab.Prop; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +public class JsonObjectFromSovityMessageResponse extends AbstractJsonLdTransformer { + + public JsonObjectFromSovityMessageResponse() { + super(SovityMessageResponse.class, JsonObject.class); + } + + @Override + public @Nullable JsonObject transform( + @NotNull SovityMessageResponse message, + @NotNull TransformerContext context) { + + var builder = Json.createObjectBuilder(); + builder.add(TYPE, Prop.SovityMessageExt.RESPONSE) + .add(Prop.SovityMessageExt.HEADER, message.header()) + .add(Prop.SovityMessageExt.BODY, message.body()); + + return builder.build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java new file mode 100644 index 000000000..1ba6bfb46 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageEmitter.java @@ -0,0 +1,40 @@ +/* + * 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.messenger.impl; + +import de.sovity.edc.extension.messenger.controller.SovityMessageController; +import lombok.RequiredArgsConstructor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpRequestFactory; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +@RequiredArgsConstructor +public class MessageEmitter implements DspHttpRequestFactory { + + private final JsonLdRemoteMessageSerializer serializer; + + @Override + public Request createRequest(SovityMessageRequest message) { + String serialized = serializer.serialize(message); + return new Request.Builder() + .url(message.counterPartyAddress() + SovityMessageController.PATH) + .post(RequestBody.create( + serialized, + MediaType.get(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + )) + .build(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java new file mode 100644 index 000000000..c997b9e4b --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/MessageReceiver.java @@ -0,0 +1,46 @@ +/* + * 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.messenger.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.val; +import okhttp3.Response; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.util.function.Function; + +@RequiredArgsConstructor +public class MessageReceiver extends DspHttpDispatcherDelegate { + + private final ObjectMapper mapper; + + @Override + protected Function parseResponse() { + return res -> { + try { + val body = res.body(); + if (body == null) { + return null; + } + String content = body.string(); + return mapper.readValue(content, SovityMessageRequest.class); + } catch (IOException e) { + throw new EdcException(e); + } + }; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.java new file mode 100644 index 000000000..4c794e5ba --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/ObjectMapperFactory.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.messenger.impl; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +public class ObjectMapperFactory { + public ObjectMapper createObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + return objectMapper; + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java new file mode 100644 index 000000000..4a057b76f --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageRequest.java @@ -0,0 +1,46 @@ +/* + * 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.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.utils.jsonld.vocab.Prop; +import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.net.URL; + +public record SovityMessageRequest( + @JsonIgnore + URL counterPartyAddress, + + @JsonProperty(Prop.SovityMessageExt.HEADER) + String header, + + @JsonProperty(Prop.SovityMessageExt.BODY) + String body +) implements RemoteMessage { + + @JsonIgnore + @Override + public String getProtocol() { + return HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; + } + + @JsonIgnore + @Override + public String getCounterPartyAddress() { + return counterPartyAddress.toString(); + } +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java new file mode 100644 index 000000000..e6c79d291 --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessageResponse.java @@ -0,0 +1,28 @@ +/* + * 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.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.utils.jsonld.vocab.Prop; + +public record SovityMessageResponse( + @JsonProperty(Prop.SovityMessageExt.HEADER) + String header, + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonProperty(Prop.SovityMessageExt.BODY) + String body +) { +} diff --git a/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java new file mode 100644 index 000000000..516c16dce --- /dev/null +++ b/extensions/sovity-messenger/src/main/java/de/sovity/edc/extension/messenger/impl/SovityMessengerStatus.java @@ -0,0 +1,28 @@ +/* + * 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.messenger.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SovityMessengerStatus { + + NO_HANDLER("no_handler"), + HANDLER_EXCEPTION("handler_exception"), + OK("ok"); + + private final String code; +} diff --git a/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..7fde2514c --- /dev/null +++ b/extensions/sovity-messenger/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +de.sovity.edc.extension.messenger.SovityMessengerExtension diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java new file mode 100644 index 000000000..dce553b83 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/SovityMessengerExtensionE2eTest.java @@ -0,0 +1,109 @@ +/* + * 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.messenger; + +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.messenger.dto.Addition; +import de.sovity.edc.extension.messenger.dto.Answer; +import de.sovity.edc.extension.messenger.dto.Multiplication; +import de.sovity.edc.extension.messenger.dto.UnsupportedMessage; +import lombok.val; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +public class SovityMessengerExtensionE2eTest { + private static final String EMITTER_PARTICIPANT_ID = "emitter"; + private static final String RECEIVER_PARTICIPANT_ID = "receiver"; + + @RegisterExtension + static EdcExtension emitterEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension receiverEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase EMITTER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase RECEIVER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorConfig providerConfig; + private ConnectorConfig consumerConfig; + + private String counterPartyAddress; + + @BeforeEach + void setup() { + providerConfig = forTestDatabase(EMITTER_PARTICIPANT_ID, EMITTER_DATABASE); + emitterEdcContext.setConfiguration(providerConfig.getProperties()); + emitterEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + consumerConfig = forTestDatabase(RECEIVER_PARTICIPANT_ID, RECEIVER_DATABASE); + receiverEdcContext.setConfiguration(consumerConfig.getProperties()); + receiverEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + counterPartyAddress = consumerConfig.getProtocolEndpoint().getUri().toString(); + } + + @Test + void e2eTest() throws ExecutionException, InterruptedException, TimeoutException { + val sovityMessenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + val handlers = receiverEdcContext.getContext().getService(SovityMessengerRegistry.class); + handlers.register(Addition.class, in -> new Answer(in.getOp1() + in.getOp2())); + handlers.register(Multiplication.class, in -> new Answer(in.getOp1() * in.getOp2())); + + val added = sovityMessenger.send(Answer.class, counterPartyAddress, new Addition(20, 30)); + val multiplied = sovityMessenger.send(Answer.class, counterPartyAddress, new Multiplication(20, 30)); + + // assert + added.get(30, SECONDS) + .onFailure(it -> fail(it.getFailureDetail())) + .onSuccess(it -> { + assertThat(it).isInstanceOf(Answer.class); + assertThat(it.getAnswer()).isEqualTo(50); + }); + + multiplied.get(30, SECONDS) + .onFailure(it -> fail(it.getFailureDetail())) + .onSuccess(it -> { + assertThat(it).isInstanceOf(Answer.class); + assertThat(it.getAnswer()).isEqualTo(600); + }); + } + + @Test + void e2eNoHandlerTest() { + val sovityMessenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + + val added = sovityMessenger.send(Answer.class, counterPartyAddress, new UnsupportedMessage()); + + // assert + val exception = assertThrows(ExecutionException.class, () -> added.get(30, SECONDS)); + assertThat(exception.getCause()).isInstanceOf(SovityMessengerException.class); + assertThat(exception.getCause().getMessage()).isEqualTo("No handler for message type unsupported"); + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java new file mode 100644 index 000000000..e21839a13 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Answer.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +class Answer implements SovityMessage { + @JsonProperty + private String string; + + @Override + public String getType() { + return "answer"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java new file mode 100644 index 000000000..6d791d5bf --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/Payload.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +class Payload implements SovityMessage { + @JsonProperty + private Integer integer; + + @Override + public String getType() { + return "payload"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java new file mode 100644 index 000000000..c297e442a --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/controller/SovityMessageControllerTest.java @@ -0,0 +1,125 @@ +/* + * 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.messenger.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageRequest; +import de.sovity.edc.extension.messenger.impl.JsonObjectFromSovityMessageResponse; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import jakarta.ws.rs.core.Response; +import lombok.val; +import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.result.Result; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +class SovityMessageControllerTest { + + private TypeTransformerRegistryImpl transformers = new TypeTransformerRegistryImpl(); + private ConsoleMonitor monitor = new ConsoleMonitor(); + private ObjectMapperFactory omf = new ObjectMapperFactory(); + private ObjectMapper objectMapper = omf.createObjectMapper(); + private IdentityService identityService = mock(IdentityService.class); + private SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + + @BeforeEach + public void beforeEach() { + transformers = new TypeTransformerRegistryImpl(); + transformers.register(new JsonObjectFromSovityMessageRequest()); + transformers.register(new JsonObjectFromSovityMessageResponse()); + + monitor = new ConsoleMonitor(); + + omf = new ObjectMapperFactory(); + objectMapper = omf.createObjectMapper(); + + handlers = new SovityMessengerRegistry(); + + reset(identityService); + when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.success(ClaimToken.Builder.newInstance().build())); + } + + @Test + void canAnswerRequest() throws JsonProcessingException, MalformedURLException { + // arrange + + val handlers = new SovityMessengerRegistry(); + + val controller = new SovityMessageController( + identityService, + "http://example.com/callback", + transformers, + monitor, + objectMapper, + handlers + ); + + Function handler = payload -> new Answer(String.valueOf(payload.getInteger())); + handlers.register(Payload.class, "foo", handler); + + val message = new SovityMessageRequest( + new URL("https://example.com/api"), + """ + { "type" : "foo" } + """, + objectMapper.writeValueAsString(new Payload(1))); + + // act + + try (val response = controller.post("", message)) { + // assert + assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + + } + + @Test + void post_whenNonAuthorized_shouldReturnHttp401() throws MalformedURLException, JsonProcessingException { + // arrange + val identityService = mock(IdentityService.class); + when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.failure("Invalid token")); + + val controller = new SovityMessageController(identityService, "http://example.com/callback", transformers, monitor, objectMapper, handlers); + + val message = new SovityMessageRequest( + new URL("https://example.com/api"), + """ + { "type" : "foo" } + """, + objectMapper.writeValueAsString(new Payload(1))); + + // act + try (val response = controller.post("", message)) { + // assert + assertThat(response.getStatus()).isEqualTo(Response.Status.UNAUTHORIZED.getStatusCode()); + } + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java new file mode 100644 index 000000000..1052df2b9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemo.java @@ -0,0 +1,73 @@ +/* + * 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.messenger.demo; + +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import de.sovity.edc.extension.messenger.demo.message.Addition; +import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Failing; +import de.sovity.edc.extension.messenger.demo.message.Signal; +import de.sovity.edc.extension.messenger.demo.message.Sqrt; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static java.lang.Math.sqrt; + + +public class SovityMessengerDemo implements ServiceExtension { + + public static final String NAME = "sovityMessengerDemo"; + + @Override + public String name() { + return NAME; + } + + /* + * 3 parts are needed: + * - the messenger + * - the handler registry + * - your handlers + */ + + @Inject + private SovityMessenger sovityMessenger; + + @Inject + private SovityMessengerRegistry registry; + + @Override + public void initialize(ServiceExtensionContext context) { + // Register the various messages that you would like to process. + // By class, safer. + registry.register(Sqrt.class, single -> new Answer(sqrt(single.getValue()))); + // By String, could be unsafe during refactorings. + registry.register(Addition.class, Addition.TYPE, add -> new Answer(add.op1 + add.op2)); + + registry.registerSignal(Signal.class, signal -> System.out.println("Received signal.")); + registry.register(Failing.class, failing -> { + throw new RuntimeException("Failed!"); + }); + + /* + * In the counterpart connector, messages can be sent with the code below. + * Check out the de.sovity.edc.extension.sovitymessenger.demo.SovityMessengerDemoTest#demo() + * for a detailed usage. + */ + + // val answer = sovityMessenger.send(Answer.class, "http://localhost/api/dsp", new Sqrt(9.0)); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java new file mode 100644 index 000000000..afc357de9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/SovityMessengerDemoTest.java @@ -0,0 +1,133 @@ +/* + * 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.messenger.demo; + +import de.sovity.edc.extension.e2e.connector.config.ConnectorConfig; +import de.sovity.edc.extension.e2e.db.TestDatabase; +import de.sovity.edc.extension.e2e.db.TestDatabaseViaTestcontainers; +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.SovityMessengerException; +import de.sovity.edc.extension.messenger.demo.message.Addition; +import de.sovity.edc.extension.messenger.demo.message.Answer; +import de.sovity.edc.extension.messenger.demo.message.Failing; +import de.sovity.edc.extension.messenger.demo.message.Signal; +import de.sovity.edc.extension.messenger.demo.message.Sqrt; +import de.sovity.edc.extension.messenger.demo.message.UnregisteredMessage; +import de.sovity.edc.extension.utils.junit.DisabledOnGithub; +import lombok.val; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; + +class SovityMessengerDemoTest { + + // Setup, you may skip this part + + private static final String EMITTER_PARTICIPANT_ID = "emitter"; + private static final String RECEIVER_PARTICIPANT_ID = "receiver"; + + @RegisterExtension + static EdcExtension emitterEdcContext = new EdcExtension(); + @RegisterExtension + static EdcExtension receiverEdcContext = new EdcExtension(); + + @RegisterExtension + static final TestDatabase EMITTER_DATABASE = new TestDatabaseViaTestcontainers(); + @RegisterExtension + static final TestDatabase RECEIVER_DATABASE = new TestDatabaseViaTestcontainers(); + + private ConnectorConfig providerConfig; + private ConnectorConfig consumerConfig; + + private String receiverAddress; + + // still setup, skip + + @BeforeEach + void setup() { + providerConfig = forTestDatabase(EMITTER_PARTICIPANT_ID, EMITTER_DATABASE); + emitterEdcContext.setConfiguration(providerConfig.getProperties()); + emitterEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + consumerConfig = forTestDatabase(RECEIVER_PARTICIPANT_ID, RECEIVER_DATABASE); + receiverEdcContext.setConfiguration(consumerConfig.getProperties()); + receiverEdcContext.registerServiceMock(TokenDecorator.class, (td) -> td); + + receiverAddress = "http://localhost:" + consumerConfig.getProtocolEndpoint().port() + consumerConfig.getProtocolEndpoint().path(); + } + + /** + * Actual usage of the Sovity Messenger. + */ + @DisabledOnGithub + @Test + void demo() throws ExecutionException, InterruptedException, TimeoutException { + /* + * Get a reference to the SovityMessenger. This is equivalent to + * + * @Inject SovityMessenger messenger; + * + * in an extension. + * + * This messenger is already configured to accept messages in de.sovity.edc.extension.messenger.demo.SovityMessengerDemo#initialize + */ + val messenger = emitterEdcContext.getContext().getService(SovityMessenger.class); + + System.out.println("START MARKER"); + + // Send messages + val added = messenger.send(Answer.class, receiverAddress, new Addition(20, 30)); + val rooted = messenger.send(Answer.class, receiverAddress, new Sqrt(9.0)); + val unregistered = messenger.send(Answer.class, receiverAddress, new UnregisteredMessage()); + messenger.send(receiverAddress, new Signal()); + + try { + // Wait for the answers + added.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + rooted.get(2, TimeUnit.SECONDS).onSuccess(it -> System.out.println(it.getAnswer())); + unregistered.get(2, TimeUnit.SECONDS); + } catch (ExecutionException e) { + /* + * When a problem happens, a SovityMessengerException is thrown and encapsulated in an ExecutionException. + */ + System.out.println(e.getCause().getMessage()); + } + + try { + val failing1 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 1")); + val failing2 = messenger.send(Answer.class, receiverAddress, new Failing("Some content 2")); + failing1.get(2, TimeUnit.SECONDS); + failing2.get(2, TimeUnit.SECONDS); + } catch (ExecutionException e) { + val cause = e.getCause(); + if (cause instanceof SovityMessengerException messengerException) { + // Error when processing a message with type demo-failing + System.out.println(messengerException.getMessage()); + // {"message":"Some content 1/2"} + System.out.println(messengerException.getBody()); + } + } + + System.out.println("END MARKER"); + } + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java new file mode 100644 index 000000000..370a5c7bf --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Addition.java @@ -0,0 +1,40 @@ +/* + * 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.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Addition implements SovityMessage { + + public static final String TYPE = "demo-add"; + + @Override + public String getType() { + return TYPE; + } + + @JsonProperty + public int op1; + + @JsonProperty + public int op2; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java new file mode 100644 index 000000000..8c79d1487 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Answer.java @@ -0,0 +1,34 @@ +/* + * 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.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Answer implements SovityMessage { + + @Override + public String getType() { + return "demo-answer"; + } + + @JsonProperty + private double answer; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java new file mode 100644 index 000000000..17dfa78be --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Failing.java @@ -0,0 +1,32 @@ +/* + * 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.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Failing implements SovityMessage { + + private String message; + + @Override + public String getType() { + return "demo-failing"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java new file mode 100644 index 000000000..f82c27dc3 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Multiplication.java @@ -0,0 +1,38 @@ +/* + * 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.messenger.demo.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Multiplication implements SovityMessage { + + @Override + public String getType() { + return "demo-multiply"; + } + + @JsonProperty + public int op1; + + @JsonProperty + public int op2; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java new file mode 100644 index 000000000..5cf54edff --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Signal.java @@ -0,0 +1,23 @@ +/* + * 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.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class Signal implements SovityMessage { + @Override + public String getType() { + return "demo-signal"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java new file mode 100644 index 000000000..b2011d507 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/Sqrt.java @@ -0,0 +1,38 @@ +/* + * 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.messenger.demo.message; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Sqrt implements SovityMessage { + + private static final String TYPE = "demo-sqrt"; + + @Override + public String getType() { + return TYPE; + } + + @JsonProperty + private double value; + +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java new file mode 100644 index 000000000..5f1be9879 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/demo/message/UnregisteredMessage.java @@ -0,0 +1,24 @@ +/* + * 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.messenger.demo.message; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class UnregisteredMessage implements SovityMessage { + + @Override + public String getType() { + return "demo-unregistered"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java new file mode 100644 index 000000000..a5f270e70 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Addition.java @@ -0,0 +1,37 @@ +/* + * 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.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Addition implements SovityMessage { + @Override + public String getType() { + return "add"; + } + + @JsonProperty + private int op1; + @JsonProperty + private int op2; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java new file mode 100644 index 000000000..29d3fa236 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Answer.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Answer implements SovityMessage { + @Override + public String getType() { + return getClass().getCanonicalName(); + } + + @JsonProperty + private int answer; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.java new file mode 100644 index 000000000..e25689bb6 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/Multiplication.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.messenger.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class Multiplication implements SovityMessage { + @Override + public String getType() { + return "mul"; + } + + @JsonProperty + private int op1; + @JsonProperty + private int op2; +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java new file mode 100644 index 000000000..9ccad0e52 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/dto/UnsupportedMessage.java @@ -0,0 +1,23 @@ +/* + * 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.messenger.dto; + +import de.sovity.edc.extension.messenger.SovityMessage; + +public class UnsupportedMessage implements SovityMessage { + @Override + public String getType() { + return "unsupported"; + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java new file mode 100644 index 000000000..c8df906ee --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/echo/SovityMessageRequestTest.java @@ -0,0 +1,57 @@ +/* + * 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.messenger.echo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.sovity.edc.extension.messenger.impl.ObjectMapperFactory; +import de.sovity.edc.extension.messenger.impl.SovityMessageRequest; +import lombok.val; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.net.MalformedURLException; +import java.net.URL; + +class SovityMessageRequestTest { + + + @Test + void canSerialize() throws MalformedURLException, JsonProcessingException, JSONException { + // arrange + val message = new SovityMessageRequest( + new URL("https://example.com"), + "{\"type\":\"foo\"}", + "body content" + ); + + val mapper = new ObjectMapperFactory().createObjectMapper(); + + // act + val serialized = mapper.writeValueAsString(message); + + // assert + JSONAssert.assertEquals( + """ + { + "https://semantic.sovity.io/message/generic/header": "{\\"type\\":\\"foo\\"}", + "https://semantic.sovity.io/message/generic/body": "body content" + } + """, + serialized, + JSONCompareMode.STRICT + ); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java new file mode 100644 index 000000000..8369c7595 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/MessageEmitterTest.java @@ -0,0 +1,57 @@ +/* + * 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.messenger.impl; + +import lombok.val; +import org.eclipse.edc.core.transform.TypeTransformerRegistryImpl; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.protocol.dsp.serialization.JsonLdRemoteMessageSerializerImpl; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class MessageEmitterTest { + + private final ObjectMapperFactory mapperFactory = new ObjectMapperFactory(); + + @Test + void emitValidMessage_whenEmpty_shouldSucceed() throws IOException { + // arrange + TypeTransformerRegistry registry = new TypeTransformerRegistryImpl(); + registry.register(new JsonObjectFromSovityMessageRequest()); + JsonLdRemoteMessageSerializer serializer = new JsonLdRemoteMessageSerializerImpl( + registry, + mapperFactory.createObjectMapper(), + new TitaniumJsonLd(mock(Monitor.class)) + ); + val emitter = new MessageEmitter(serializer); + + // act + val request = emitter.createRequest(new SovityMessageRequest( + new URL("https://example.com/api"), + "header", + "body" + )); + + // assert + assertThat(request.url().toString()).isEqualTo("https://example.com/api/sovity/message/generic"); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java new file mode 100644 index 000000000..b2c44cd72 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerRegistryImplTest.java @@ -0,0 +1,72 @@ +/* + * 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.messenger.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.sovity.edc.extension.messenger.SovityMessage; +import de.sovity.edc.extension.messenger.SovityMessengerRegistry; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.val; +import org.junit.jupiter.api.Test; + +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SovityMessengerRegistryImplTest { + + @AllArgsConstructor + @NoArgsConstructor + @Getter + static class MyInt implements SovityMessage { + + @Override + public String getType() { + return "message"; + } + + @JsonProperty + private int value; + } + + @Test + void canRegisterAndRetrieveHandler() { + // arrange + SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + Function handler = myInt -> String.valueOf(myInt.getValue()); + + // act + handlers.register(MyInt.class, "itoa", handler); + val back = handlers.getHandler("itoa"); + + // assert + assertThat(back.handler().apply(new MyInt(1))).isEqualTo("1"); + } + + @Test + void register_whenRegisteringDuplicatedName_shouldThrowIllegalStateException() { + // arrange + SovityMessengerRegistry handlers = new SovityMessengerRegistry(); + Function handler = myInt -> String.valueOf(myInt.getValue()); + + // act + handlers.register(MyInt.class, "foo", handler); + + // assert + assertThrows(IllegalStateException.class, () -> handlers.register(MyInt.class, "foo", handler)); + } +} diff --git a/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java new file mode 100644 index 000000000..6c2334d85 --- /dev/null +++ b/extensions/sovity-messenger/src/test/java/de/sovity/edc/extension/messenger/impl/SovityMessengerTest.java @@ -0,0 +1,62 @@ +/* + * 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.messenger.impl; + +import de.sovity.edc.extension.messenger.SovityMessenger; +import de.sovity.edc.extension.messenger.dto.Answer; +import de.sovity.edc.extension.messenger.dto.UnsupportedMessage; +import lombok.val; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.response.StatusResult; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SovityMessengerTest { + @Test + void send_whenNoHandler_shouldThrowSovityMessengerException() throws MalformedURLException { + // arrange + val registry = mock(RemoteMessageDispatcherRegistry.class); + CompletableFuture> future = CompletableFuture.completedFuture( + StatusResult.success( + new SovityMessageRequest( + new URL("https://example.com/api/dsp"), + """ + { + "status": "no_handler", + "message": "No handler for foo" + } + """, + null))); + + when(registry.dispatch(any(), any())).thenReturn(future); + val messenger = new SovityMessenger(registry, new ObjectMapperFactory().createObjectMapper()); + val answer = messenger.send(Answer.class, "https://example.com/api/dsp", new UnsupportedMessage()); + + // act + val exception = assertThrows(ExecutionException.class, answer::get); + + // assert + assertThat(exception.getCause().getMessage()).isEqualTo("No handler for foo"); + } +} diff --git a/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..afe23dcb9 --- /dev/null +++ b/extensions/sovity-messenger/src/test/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,2 @@ +de.sovity.edc.extension.messenger.SovityMessengerExtension +de.sovity.edc.extension.messenger.demo.SovityMessengerDemo diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46c3d69aa..b7c15321a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ openapiJackson = "0.2.6" postgres = "42.4.5" quarkus = "2.16.6.Final" quartz = "2.3.2" -restAssured = "4.5.0" +restAssured = "5.4.0" retry = "1.5.7" shadow = "7.1.2" swagger = "1.6.12" @@ -99,7 +99,9 @@ edc-dataPlaneUtil = { module = "org.eclipse.edc:data-plane-util", version.ref = edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-dspApiConfiguration = { module = "org.eclipse.edc:dsp-api-configuration", version.ref = "edc" } edc-dspHttpSpi = { module = "org.eclipse.edc:dsp-http-spi", version.ref = "edc" } +edc-dspHttpCore = { module = "org.eclipse.edc:dsp-http-core", version.ref = "edc" } edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } +edc-httpSpi = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } edc-iamMock = { module = "org.eclipse.edc:iam-mock", version.ref = "edc" } edc-jsonLd = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } edc-jsonLdSpi = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 176f5942d..a3ac33a58 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ include(":extensions:policy-always-true") include(":extensions:policy-referring-connector") include(":extensions:policy-time-interval") include(":extensions:postgres-flyway") +include(":extensions:sovity-messenger") include(":extensions:sovity-edc-extensions-package") include(":extensions:test-backend-controller") include(":extensions:transfer-process-status-checker") diff --git a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java index 1fbdce1c0..21d03fbeb 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/DataSourceParameterizationTest.java @@ -110,7 +110,6 @@ class DataSourceParameterizationTest { private final String destinationPath = "/destination/some/path/"; private final String sourceUrl = "http://localhost:" + port + sourcePath; private final String destinationUrl = "http://localhost:" + port + destinationPath; - // TODO: remove the test backend dependency? private ClientAndServer mockServer; private static final AtomicInteger DATA_OFFER_INDEX = new AtomicInteger(0); @@ -502,7 +501,7 @@ private String initiateTransferWithParameters( Map dataSinkProperties = new HashMap<>(); dataSinkProperties.put(EDC_NAMESPACE + "baseUrl", destinationUrl); dataSinkProperties.put(EDC_NAMESPACE + "method", HttpMethod.PUT); - dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); // TODO: http proxy + dataSinkProperties.put(EDC_NAMESPACE + "type", "HttpData"); transferProcessProperties.put(rootKey + METHOD, testCase.method); if (testCase.body != null) { diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index 3606ddde9..3d35377a0 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -136,6 +136,16 @@ public class HttpDatasourceHints { } } + @UtilityClass + public class SovityMessageExt { + public final String CTX = "https://semantic.sovity.io/message/generic/"; + public final String REQUEST = CTX + "request"; + public final String RESPONSE = CTX + "response"; + public final String ERROR_MESSAGE = CTX + "errorMessage"; + public final String HEADER = CTX + "header"; + public final String BODY = CTX + "body"; + } + /** * FOAF Vocabulary */ diff --git a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java index 5cc358f9a..6c7a046f8 100644 --- a/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java +++ b/utils/test-connector-remote/src/main/java/de/sovity/edc/extension/e2e/connector/config/ConnectorConfigFactory.java @@ -16,22 +16,74 @@ import de.sovity.edc.extension.e2e.db.TestDatabase; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.val; +import java.io.IOException; +import java.net.ServerSocket; import java.util.HashMap; +import java.util.Random; import java.util.UUID; import static de.sovity.edc.extension.e2e.connector.config.DatasourceConfigUtils.configureDatasources; import static de.sovity.edc.extension.e2e.connector.config.api.EdcApiConfigFactory.configureApi; +import static org.eclipse.edc.junit.testfixtures.TestUtils.MAX_TCP_PORT; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ConnectorConfigFactory { + private static final Random RANDOM = new Random(); + + /** + * Creates the default configuration to start an EDC with the given test database. + * + * @deprecated Use {@link ConnectorConfigFactory#forTestDatabase(String, TestDatabase)} + * with automatic ports allocation to prevent port allocation conflicts. + */ + @Deprecated public static ConnectorConfig forTestDatabase(String participantId, int firstPort, TestDatabase testDatabase) { var config = basicEdcConfig(participantId, firstPort); config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); return config; } + public static ConnectorConfig forTestDatabase(String participantId, TestDatabase testDatabase) { + val firstPort = getFreePortRange(5); + var config = basicEdcConfig(participantId, firstPort); + config.setProperties(configureDatasources(testDatabase.getJdbcCredentials())); + return config; + } + + private static synchronized int getFreePortRange(int size) { + // pick a random in a reasonable range + int firstPort = getFreePort(RANDOM.nextInt(10_000, 50_000)); + + int currentPort = firstPort; + do { + if (canUsePort(currentPort + 1)) { + currentPort++; + } else { + firstPort = getFreePort(currentPort++); + } + } while (currentPort < firstPort + size); + + return firstPort; + } + + private static boolean canUsePort(int port) { + + if (port <= 0 || port >= MAX_TCP_PORT) { + throw new IllegalArgumentException("Lower bound must be > 0 and < " + MAX_TCP_PORT + " and be < upperBound"); + } + + try (ServerSocket serverSocket = new ServerSocket(port)) { + serverSocket.setReuseAddress(true); + return true; + } catch (IOException e) { + return false; + } + } + public static ConnectorConfig basicEdcConfig(String participantId, int firstPort) { var apiKey = UUID.randomUUID().toString(); var apiConfig = configureApi(firstPort, apiKey); @@ -55,11 +107,11 @@ public static ConnectorConfig basicEdcConfig(String participantId, int firstPort properties.put("my.edc.maintainer.name", "Maintainer Name %s".formatted(participantId)); return new ConnectorConfig( - participantId, - apiConfig.getDefaultApiGroup(), - apiConfig.getManagementApiGroup(), - apiConfig.getProtocolApiGroup(), - properties + participantId, + apiConfig.getDefaultApiGroup(), + apiConfig.getManagementApiGroup(), + apiConfig.getProtocolApiGroup(), + properties ); } }