diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/build.gradle.kts b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/build.gradle.kts index 867c7d1587a..00c9973370f 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/build.gradle.kts +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/build.gradle.kts @@ -25,4 +25,6 @@ dependencies { api(project(":spi:common:catalog-spi")) api(libs.jakartaJson) + + testImplementation(testFixtures(project(":data-protocols:dsp:dsp-http-spi"))) } \ No newline at end of file diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/DspCatalogHttpDispatcherExtension.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/DspCatalogHttpDispatcherExtension.java index 5f0913572b3..54f592fd270 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/DspCatalogHttpDispatcherExtension.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/DspCatalogHttpDispatcherExtension.java @@ -17,6 +17,7 @@ import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; import org.eclipse.edc.protocol.dsp.catalog.dispatcher.delegate.CatalogRequestHttpDelegate; 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.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; @@ -37,6 +38,8 @@ public class DspCatalogHttpDispatcherExtension implements ServiceExtension { @Inject private DspHttpRemoteMessageDispatcher messageDispatcher; @Inject + private JsonLdRemoteMessageSerializer remoteMessageSerializer; + @Inject private TypeManager typeManager; @Inject private JsonLdTransformerRegistry transformerRegistry; @@ -48,7 +51,7 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - messageDispatcher.registerDelegate(new CatalogRequestHttpDelegate(typeManager.getMapper(TYPE_MANAGER_CONTEXT_JSON_LD), transformerRegistry)); + messageDispatcher.registerDelegate(new CatalogRequestHttpDelegate(remoteMessageSerializer, typeManager.getMapper(TYPE_MANAGER_CONTEXT_JSON_LD), transformerRegistry)); } } diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestHttpDelegate.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestHttpDelegate.java index fb3e2dc4e16..0fb1405d12e 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestHttpDelegate.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestHttpDelegate.java @@ -14,18 +14,16 @@ package org.eclipse.edc.protocol.dsp.catalog.dispatcher.delegate; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; import jakarta.json.JsonObject; -import okhttp3.MediaType; import okhttp3.Request; -import okhttp3.RequestBody; import okhttp3.Response; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.CatalogRequestMessage; import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; import org.eclipse.edc.spi.EdcException; import java.io.IOException; @@ -33,7 +31,6 @@ import static java.lang.String.format; import static java.lang.String.join; -import static org.eclipse.edc.jsonld.util.JsonLdUtil.compact; import static org.eclipse.edc.jsonld.util.JsonLdUtil.expand; import static org.eclipse.edc.protocol.dsp.catalog.spi.CatalogApiPaths.BASE_PATH; import static org.eclipse.edc.protocol.dsp.catalog.spi.CatalogApiPaths.CATALOG_REQUEST; @@ -43,14 +40,13 @@ /** * Delegate for dispatching catalog requests as defined in the dataspace protocol specification. */ -public class CatalogRequestHttpDelegate implements DspHttpDispatcherDelegate { - - private static final String APPLICATION_JSON = "application/json"; +public class CatalogRequestHttpDelegate extends DspHttpDispatcherDelegate { private final ObjectMapper mapper; private final JsonLdTransformerRegistry transformerRegistry; - public CatalogRequestHttpDelegate(ObjectMapper mapper, JsonLdTransformerRegistry transformerRegistry) { + public CatalogRequestHttpDelegate(JsonLdRemoteMessageSerializer serializer, ObjectMapper mapper, JsonLdTransformerRegistry transformerRegistry) { + super(serializer); this.mapper = mapper; this.transformerRegistry = transformerRegistry; } @@ -70,12 +66,7 @@ public Class getMessageType() { */ @Override public Request buildRequest(CatalogRequestMessage message) { - var requestBody = RequestBody.create(toJson(message), MediaType.get(APPLICATION_JSON)); - - return new Request.Builder() - .url(message.getCallbackAddress() + BASE_PATH + CATALOG_REQUEST) - .post(requestBody) - .build(); + return buildRequest(message, BASE_PATH + CATALOG_REQUEST, jsonLdContext()); } /** @@ -104,19 +95,6 @@ public Function parseResponse() { } }; } - - private String toJson(CatalogRequestMessage message) { - try { - var transformResult = transformerRegistry.transform(message, JsonObject.class); - if (transformResult.succeeded()) { - var compacted = compact(transformResult.getContent(), jsonLdContext()); - return mapper.writeValueAsString(compacted); - } - throw new EdcException(format("Failed to write request: %s", join(", ", transformResult.getFailureMessages()))); - } catch (JsonProcessingException e) { - throw new EdcException("Failed to serialize catalog request", e); - } - } private JsonObject jsonLdContext() { return Json.createObjectBuilder() diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestMessageHttpDelegateTest.java index c7fa73abc03..abe56823bcd 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestMessageHttpDelegateTest.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/catalog/dispatcher/delegate/CatalogRequestMessageHttpDelegateTest.java @@ -14,16 +14,14 @@ package org.eclipse.edc.protocol.dsp.catalog.dispatcher.delegate; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; import jakarta.json.JsonObject; -import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import okio.Buffer; import org.eclipse.edc.catalog.spi.Catalog; import org.eclipse.edc.catalog.spi.CatalogRequestMessage; -import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.Result; @@ -37,10 +35,7 @@ import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; import static org.eclipse.edc.protocol.dsp.catalog.spi.CatalogApiPaths.BASE_PATH; import static org.eclipse.edc.protocol.dsp.catalog.spi.CatalogApiPaths.CATALOG_REQUEST; -import static org.eclipse.edc.protocol.dsp.catalog.transform.DspCatalogPropertyAndTypeNames.DSPACE_PREFIX; -import static org.eclipse.edc.protocol.dsp.catalog.transform.DspCatalogPropertyAndTypeNames.DSPACE_SCHEMA; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; @@ -48,16 +43,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class CatalogRequestMessageHttpDelegateTest { - - private final ObjectMapper mapper = mock(ObjectMapper.class); - private final JsonLdTransformerRegistry registry = mock(JsonLdTransformerRegistry.class); +class CatalogRequestMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { private CatalogRequestHttpDelegate delegate; @BeforeEach void setUp() { - delegate = new CatalogRequestHttpDelegate(mapper, registry); + delegate = new CatalogRequestHttpDelegate(serializer, mapper, registry); } @Test @@ -67,34 +59,12 @@ void getMessageType_returnCatalogRequest() { @Test void buildRequest_returnRequest() throws IOException { - var jsonObject = Json.createObjectBuilder() - .add(DSPACE_SCHEMA + "key", "value") - .build(); - var serializedBody = "catalog request"; - - when(registry.transform(isA(CatalogRequestMessage.class), eq(JsonObject.class))) - .thenReturn(Result.success(jsonObject)); - when(mapper.writeValueAsString(any(JsonObject.class))).thenReturn(serializedBody); - - var message = getCatalogRequest(); - var httpRequest = delegate.buildRequest(message); - - assertThat(httpRequest.url().url()).hasToString(message.getCallbackAddress() + BASE_PATH + CATALOG_REQUEST); - assertThat(readRequestBody(httpRequest)).isEqualTo(serializedBody); - - verify(registry, times(1)) - .transform(argThat(requestMessage -> ((CatalogRequestMessage) requestMessage).getQuerySpec().equals(message.getQuerySpec())), eq(JsonObject.class)); - verify(mapper, times(1)) - .writeValueAsString(argThat(json -> ((JsonObject) json).get(CONTEXT) != null && ((JsonObject) json).get(DSPACE_PREFIX + ":key") != null)); + testBuildRequest_shouldReturnRequest(message(), BASE_PATH + CATALOG_REQUEST); } @Test - void buildRequest_transformationFails_throwException() { - when(registry.transform(isA(CatalogRequestMessage.class), eq(JsonObject.class))) - .thenReturn(Result.failure("error")); - - var message = getCatalogRequest(); - assertThatThrownBy(() -> delegate.buildRequest(message)).isInstanceOf(EdcException.class); + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); } @Test @@ -133,23 +103,12 @@ void parseResponse_transformationFails_throwException() throws IOException { @Test void parseResponse_readingResponseBodyFails_throwException() throws IOException { - var response = mock(Response.class); - var responseBody = mock(ResponseBody.class); - - when(response.body()).thenReturn(responseBody); - when(responseBody.bytes()).thenReturn("test".getBytes()); - when(mapper.readValue(any(byte[].class), eq(JsonObject.class))).thenThrow(IOException.class); - - assertThatThrownBy(() -> delegate.parseResponse().apply(response)).isInstanceOf(EdcException.class); + testParseResponse_shouldThrowException_whenReadingResponseBodyFails(); } @Test - void parseResponse_responseBodyNull_throwException() throws IOException { - var response = mock(Response.class); - - when(response.body()).thenReturn(null); - - assertThatThrownBy(() -> delegate.parseResponse().apply(response)).isInstanceOf(EdcException.class); + void parseResponse_responseBodyNull_throwException() { + testParseResponse_shouldThrowException_whenResponseBodyNull(); } @Test @@ -175,7 +134,7 @@ private JsonObject getJsonObject() { .build(); } - private CatalogRequestMessage getCatalogRequest() { + private CatalogRequestMessage message() { return CatalogRequestMessage.Builder.newInstance() .callbackAddress("http://connector") .connectorId("connector-id") @@ -183,11 +142,9 @@ private CatalogRequestMessage getCatalogRequest() { .querySpec(QuerySpec.max()) .build(); } - - private String readRequestBody(Request request) throws IOException { - var buffer = new Buffer(); - request.body().writeTo(buffer); - return buffer.readUtf8(); + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; } - } diff --git a/data-protocols/dsp/dsp-http-core/build.gradle.kts b/data-protocols/dsp/dsp-http-core/build.gradle.kts index 207885221a7..355284a41f7 100644 --- a/data-protocols/dsp/dsp-http-core/build.gradle.kts +++ b/data-protocols/dsp/dsp-http-core/build.gradle.kts @@ -18,5 +18,7 @@ plugins { dependencies { api(project(":spi:common:http-spi")) + api(project(":spi:common:json-ld-spi")) + api(project(":extensions:common:json-ld")) api(project(":data-protocols:dsp:dsp-http-spi")) } \ No newline at end of file diff --git a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/DspHttpCoreExtension.java b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/DspHttpCoreExtension.java index f647cfcd690..d5467c277ba 100644 --- a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/DspHttpCoreExtension.java +++ b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/DspHttpCoreExtension.java @@ -14,8 +14,11 @@ package org.eclipse.edc.protocol.dsp; +import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; import org.eclipse.edc.protocol.dsp.dispatcher.DspHttpRemoteMessageDispatcherImpl; +import org.eclipse.edc.protocol.dsp.serialization.JsonLdRemoteMessageSerializerImpl; 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.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; @@ -23,6 +26,9 @@ import org.eclipse.edc.spi.iam.IdentityService; import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.types.TypeManager; + +import static org.eclipse.edc.jsonld.JsonLdExtension.TYPE_MANAGER_CONTEXT_JSON_LD; /** * Provides an implementation of {@link DspHttpRemoteMessageDispatcher} to support sending dataspace @@ -40,6 +46,10 @@ public class DspHttpCoreExtension implements ServiceExtension { private EdcHttpClient httpClient; @Inject private IdentityService identityService; + @Inject + private JsonLdTransformerRegistry transformerRegistry; + @Inject + private TypeManager typeManager; @Override public String name() { @@ -52,5 +62,10 @@ public DspHttpRemoteMessageDispatcher dspHttpRemoteMessageDispatcher() { dispatcherRegistry.register(dispatcher); return dispatcher; } + + @Provider + public JsonLdRemoteMessageSerializer jsonLdRemoteMessageSerializer() { + return new JsonLdRemoteMessageSerializerImpl(transformerRegistry, typeManager.getMapper(TYPE_MANAGER_CONTEXT_JSON_LD)); + } } diff --git a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImpl.java b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImpl.java new file mode 100644 index 00000000000..756e14cd93a --- /dev/null +++ b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.serialization; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import static java.lang.String.format; +import static java.lang.String.join; +import static org.eclipse.edc.jsonld.util.JsonLdUtil.compact; + +/** + * Serializes {@link RemoteMessage}s to JSON-LD. + */ +public class JsonLdRemoteMessageSerializerImpl implements JsonLdRemoteMessageSerializer { + + private JsonLdTransformerRegistry registry; + private ObjectMapper mapper; + + public JsonLdRemoteMessageSerializerImpl(JsonLdTransformerRegistry registry, ObjectMapper mapper) { + this.registry = registry; + this.mapper = mapper; + } + + /** + * Serializes a {@link RemoteMessage} to JSON-LD. The message is first transformed using the + * {@link JsonLdTransformerRegistry}, then the resulting JSON-LD structure is compacted using + * the given JSON-LD context before returning it as a string. + * + * @param message the message to serialize + * @param jsonLdContext the JSON-LD context + * @return the serialized message + */ + @Override + public String serialize(RemoteMessage message, JsonObject jsonLdContext) { + try { + var transformResult = registry.transform(message, JsonObject.class); + if (transformResult.succeeded()) { + var compacted = compact(transformResult.getContent(), jsonLdContext); + return mapper.writeValueAsString(compacted); + } + throw new EdcException(format("Failed to transform %s: %s", message.getClass().getSimpleName(), join(", ", transformResult.getFailureMessages()))); + } catch (JsonProcessingException e) { + throw new EdcException(format("Failed to serialize %s", message.getClass().getSimpleName()), e); + } + } +} diff --git a/data-protocols/dsp/dsp-http-core/src/test/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImplTest.java b/data-protocols/dsp/dsp-http-core/src/test/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImplTest.java new file mode 100644 index 00000000000..38b0e5aa73b --- /dev/null +++ b/data-protocols/dsp/dsp-http-core/src/test/java/org/eclipse/edc/protocol/dsp/serialization/JsonLdRemoteMessageSerializerImplTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.serialization; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class JsonLdRemoteMessageSerializerImplTest { + + private JsonLdTransformerRegistry registry = mock(JsonLdTransformerRegistry.class); + private ObjectMapper mapper = mock(ObjectMapper.class); + + private JsonLdRemoteMessageSerializerImpl serializer; + private RemoteMessage message = mock(RemoteMessage.class); + + @BeforeEach + void setUp() { + serializer = new JsonLdRemoteMessageSerializerImpl(registry, mapper); + } + + @Test + void serialize_shouldReturnString_whenValidMessage() throws JsonProcessingException { + var json = messageJson(); + var serialized = "serialized"; + + when(registry.transform(message, JsonObject.class)) + .thenReturn(Result.success(json)); + when(mapper.writeValueAsString(any(JsonObject.class))).thenReturn(serialized); + + var result = serializer.serialize(message, jsonLdContext()); + + assertThat(result).isEqualTo(serialized); + + verify(registry, times(1)).transform(message, JsonObject.class); + verify(mapper, times(1)) + .writeValueAsString(argThat(obj -> ((JsonObject) obj).get("schema:key") != null)); + } + + @Test + void serialize_shouldThrowException_whenTransformationFails() { + when(registry.transform(message, JsonObject.class)) + .thenReturn(Result.failure("error")); + + assertThatThrownBy(() -> serializer.serialize(message, jsonLdContext())) + .isInstanceOf(EdcException.class); + } + + @Test + void serialize_shouldThrowException_whenSerializationFails() throws JsonProcessingException { + var json = messageJson(); + + when(registry.transform(message, JsonObject.class)) + .thenReturn(Result.success(json)); + when(mapper.writeValueAsString(any(JsonObject.class))).thenThrow(JsonProcessingException.class); + + assertThatThrownBy(() -> serializer.serialize(message, jsonLdContext())) + .isInstanceOf(EdcException.class); + } + + private JsonObject messageJson() { + return Json.createObjectBuilder() + .add("http://schema/key", "value") + .build(); + } + + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add("schema", "http://schema/") + .build(); + } +} diff --git a/data-protocols/dsp/dsp-http-spi/build.gradle.kts b/data-protocols/dsp/dsp-http-spi/build.gradle.kts index 5d1371313cb..a7c589b21dd 100644 --- a/data-protocols/dsp/dsp-http-spi/build.gradle.kts +++ b/data-protocols/dsp/dsp-http-spi/build.gradle.kts @@ -14,9 +14,16 @@ plugins { `java-library` + `java-test-fixtures` } dependencies { api(project(":spi:common:core-spi")) api(libs.okhttp) + api(libs.jakartaJson) + + testFixturesApi(project(":core:common:junit")) + testFixturesApi(project(":spi:common:json-ld-spi")) + testFixturesImplementation(libs.mockito.core) + testFixturesImplementation(libs.assertj) } diff --git a/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/dispatcher/DspHttpDispatcherDelegate.java b/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/dispatcher/DspHttpDispatcherDelegate.java index 6d39d745883..0fe33e42209 100644 --- a/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/dispatcher/DspHttpDispatcherDelegate.java +++ b/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/dispatcher/DspHttpDispatcherDelegate.java @@ -14,8 +14,12 @@ package org.eclipse.edc.protocol.dsp.spi.dispatcher; +import jakarta.json.JsonObject; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; import org.eclipse.edc.spi.types.domain.message.RemoteMessage; import java.util.function.Function; @@ -26,14 +30,22 @@ * @param the type of message * @param the response type */ -public interface DspHttpDispatcherDelegate { +public abstract class DspHttpDispatcherDelegate { + + private static final String APPLICATION_JSON = "application/json"; + + private JsonLdRemoteMessageSerializer serializer; + + protected DspHttpDispatcherDelegate(JsonLdRemoteMessageSerializer serializer) { + this.serializer = serializer; + } /** * Returns the type of {@link RemoteMessage} this delegate can handle. * * @return the message type */ - Class getMessageType(); + public abstract Class getMessageType(); /** * Builds the HTTP request for the message including method, URL, body and headers. The @@ -42,13 +54,24 @@ public interface DspHttpDispatcherDelegate { * @param message the message * @return the request builder */ - Request buildRequest(M message); + public abstract Request buildRequest(M message); + + protected Request buildRequest(M message, String path, JsonObject jsonLdContext) { + var body = serializer.serialize(message, jsonLdContext); + var requestBody = RequestBody.create(body, MediaType.get(APPLICATION_JSON)); + + return new Request.Builder() + .url(message.getCallbackAddress() + path) + .header("Content-Type", APPLICATION_JSON) + .post(requestBody) + .build(); + } /** * Parses the response to return an instance of the expected response type. * * @return the parsed response */ - Function parseResponse(); + public abstract Function parseResponse(); } diff --git a/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/serialization/JsonLdRemoteMessageSerializer.java b/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/serialization/JsonLdRemoteMessageSerializer.java new file mode 100644 index 00000000000..b007223208b --- /dev/null +++ b/data-protocols/dsp/dsp-http-spi/src/main/java/org/eclipse/edc/protocol/dsp/spi/serialization/JsonLdRemoteMessageSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.spi.serialization; + +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +/** + * Serializes {@link RemoteMessage}s to JSON-LD. + */ +public interface JsonLdRemoteMessageSerializer { + + /** + * Serializes a {@link RemoteMessage} to JSON-LD using the given JSON-LD context. + * + * @param message the message to serialize + * @param jsonLdContext the JSON-LD context + * @return the serialized message + */ + String serialize(RemoteMessage message, JsonObject jsonLdContext); + +} diff --git a/data-protocols/dsp/dsp-http-spi/src/testFixtures/java/org/eclipse/edc/protocol/dsp/spi/testfixtures/dispatcher/DspHttpDispatcherDelegateTestBase.java b/data-protocols/dsp/dsp-http-spi/src/testFixtures/java/org/eclipse/edc/protocol/dsp/spi/testfixtures/dispatcher/DspHttpDispatcherDelegateTestBase.java new file mode 100644 index 00000000000..9dc1c9ee8d8 --- /dev/null +++ b/data-protocols/dsp/dsp-http-spi/src/testFixtures/java/org/eclipse/edc/protocol/dsp/spi/testfixtures/dispatcher/DspHttpDispatcherDelegateTestBase.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import org.eclipse.edc.jsonld.spi.transformer.JsonLdTransformerRegistry; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test base for implementations of {@link DspHttpDispatcherDelegate}. Provides methods that test + * common behaviour of delegate methods. Each sub-class of this test base should choose the + * respective methods for the delegate implementation it's testing. + */ +public abstract class DspHttpDispatcherDelegateTestBase { + + protected JsonLdRemoteMessageSerializer serializer = mock(JsonLdRemoteMessageSerializer.class); + protected ObjectMapper mapper = mock(ObjectMapper.class); + protected JsonLdTransformerRegistry registry = mock(JsonLdTransformerRegistry.class); + + /** + * Returns the delegate to test. + * + * @return the delegate + */ + protected abstract DspHttpDispatcherDelegate delegate(); + + /** + * Checks that a delegate, given a message, builds the expected HTTP request. The message should + * be serialized and added as the request body. Validates that the delegate sets the expected + * request path. Relevant for all delegates. + * + * @param message the message + * @param path the expected path + */ + protected void testBuildRequest_shouldReturnRequest(M message, String path) throws IOException { + var serializedBody = "serialized"; + + when(serializer.serialize(eq(message), any(JsonObject.class))).thenReturn(serializedBody); + + var httpRequest = delegate().buildRequest(message); + + assertThat(httpRequest.url().url()).hasToString(message.getCallbackAddress() + path); + assertThat(readRequestBody(httpRequest)).isEqualTo(serializedBody); + + verify(serializer, times(1)).serialize(eq(message), any(JsonObject.class)); + } + + /** + * Checks that a delegate throws an exception if serialization of the message to send fails. + * Relevant for all delegates. + * + * @param message the message + */ + protected void testBuildRequest_shouldThrowException_whenSerializationFails(M message) { + when(serializer.serialize(eq(message), any(JsonObject.class))).thenThrow(EdcException.class); + + assertThatThrownBy(() -> delegate().buildRequest(message)).isInstanceOf(EdcException.class); + } + + /** + * Checks that a delegate throws an exception when the response body is missing. Only relevant + * for delegates that process the response body. + */ + protected void testParseResponse_shouldThrowException_whenResponseBodyNull() { + var response = mock(Response.class); + + when(response.body()).thenReturn(null); + + assertThatThrownBy(() -> delegate().parseResponse().apply(response)).isInstanceOf(EdcException.class); + } + + /** + * Checks that a delegate throws an exception when the response body cannot be read. Only + * relevant for delegates that process the response body. + */ + protected void testParseResponse_shouldThrowException_whenReadingResponseBodyFails() throws IOException { + var response = mock(Response.class); + var responseBody = mock(ResponseBody.class); + + when(response.body()).thenReturn(responseBody); + when(responseBody.bytes()).thenReturn("test".getBytes()); + when(mapper.readValue(any(byte[].class), eq(JsonObject.class))).thenThrow(IOException.class); + + assertThatThrownBy(() -> delegate().parseResponse().apply(response)).isInstanceOf(EdcException.class); + } + + /** + * Checks that a delegate returns a null function for parsing the response body. Only relevant + * for delegates that do not process the response body. + */ + protected void testParseResponse_shouldReturnNullFunction_whenResponseBodyNotProcessed() { + var response = mock(Response.class); + + assertThat(delegate().parseResponse().apply(response)).isNull(); + } + + protected String readRequestBody(Request request) throws IOException { + var buffer = new Buffer(); + request.body().writeTo(buffer); + return buffer.readUtf8(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/build.gradle.kts b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/build.gradle.kts new file mode 100644 index 00000000000..5d368e6d5e6 --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-spi")) + api(project(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-transform")) + api(project(":data-protocols:dsp:dsp-http-core")) + api(project(":data-protocols:dsp:dsp-http-spi")) + api(project(":extensions:common:json-ld")) + api(project(":spi:control-plane:contract-spi")) + + api(libs.jakartaJson) + + testImplementation(testFixtures(project(":data-protocols:dsp:dsp-http-spi"))) +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/DspNegotiationHttpDispatcherExtension.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/DspNegotiationHttpDispatcherExtension.java new file mode 100644 index 00000000000..9d75deba80f --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/DspNegotiationHttpDispatcherExtension.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher; + +import org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate.ContractAgreementMessageHttpDelegate; +import org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate.ContractAgreementVerificationMessageHttpDelegate; +import org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate.ContractNegotiationEventMessageHttpDelegate; +import org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate.ContractNegotiationTerminationMessageHttpDelegate; +import org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate.ContractRequestMessageHttpDelegate; +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.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; + +import static org.eclipse.edc.jsonld.JsonLdExtension.TYPE_MANAGER_CONTEXT_JSON_LD; + +@Extension(value = DspNegotiationHttpDispatcherExtension.NAME) +public class DspNegotiationHttpDispatcherExtension implements ServiceExtension { + + public static final String NAME = "Dataspace Protocol Negotiation HTTP Dispatcher Extension"; + + @Inject + private DspHttpRemoteMessageDispatcher messageDispatcher; + @Inject + private TypeManager typeManager; + @Inject + private JsonLdRemoteMessageSerializer remoteMessageSerializer; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + messageDispatcher.registerDelegate(new ContractAgreementMessageHttpDelegate(remoteMessageSerializer)); + messageDispatcher.registerDelegate(new ContractAgreementVerificationMessageHttpDelegate(remoteMessageSerializer)); + messageDispatcher.registerDelegate(new ContractNegotiationEventMessageHttpDelegate(remoteMessageSerializer)); + messageDispatcher.registerDelegate(new ContractNegotiationTerminationMessageHttpDelegate(remoteMessageSerializer)); + messageDispatcher.registerDelegate(new ContractRequestMessageHttpDelegate(remoteMessageSerializer, typeManager.getMapper(TYPE_MANAGER_CONTEXT_JSON_LD))); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegate.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegate.java new file mode 100644 index 00000000000..5aa1deb88eb --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegate.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +import java.util.function.Function; + +import static org.eclipse.edc.jsonld.spi.Namespaces.ODRL_PREFIX; +import static org.eclipse.edc.jsonld.spi.Namespaces.ODRL_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_PREFIX; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; + +/** + * Delegate for dispatching contract agreement message as defined in the dataspace protocol specification. + */ +public class ContractAgreementMessageHttpDelegate extends DspHttpDispatcherDelegate { + + public ContractAgreementMessageHttpDelegate(JsonLdRemoteMessageSerializer serializer) { + super(serializer); + } + + @Override + public Class getMessageType() { + return ContractAgreementMessage.class; + } + + /** + * Sends a contract agreement message. The request body is constructed as defined in the dataspace + * protocol. The request is sent to the remote component using the path from the http binding. + * + * @param message the message. + * @return the built okhttp request. + */ + @Override + public Request buildRequest(ContractAgreementMessage message) { + return buildRequest(message, BASE_PATH + message.getProcessId() + AGREEMENT, jsonLdContext()); + } + + /** + * Parses the response to an agreement message. The JSON-LD structure from the response body is + * expanded and returned. + * + * @return a function that contains the response body or null. + */ + @Override + public Function parseResponse() { + return response -> null; + } + + // TODO refactor according to https://github.com/eclipse-edc/Connector/issues/2763 + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add(DSPACE_PREFIX, DSPACE_SCHEMA) + .add(ODRL_PREFIX, ODRL_SCHEMA) + .build(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegate.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegate.java new file mode 100644 index 00000000000..be791b57e7a --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegate.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementVerificationMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +import java.util.function.Function; + +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_PREFIX; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.VERIFICATION; + +/** + * Delegate for dispatching contract agreement verification message as defined in the dataspace protocol specification. + */ +public class ContractAgreementVerificationMessageHttpDelegate extends DspHttpDispatcherDelegate { + + public ContractAgreementVerificationMessageHttpDelegate(JsonLdRemoteMessageSerializer serializer) { + super(serializer); + } + + @Override + public Class getMessageType() { + return ContractAgreementVerificationMessage.class; + } + + /** + * Sends a contract agreement verification message. The request body is constructed as defined in + * the dataspace protocol. The request is sent to the remote component using the path from the http + * binding. + * + * @param message the message. + * @return the built okhttp request. + */ + @Override + public Request buildRequest(ContractAgreementVerificationMessage message) { + return buildRequest(message, BASE_PATH + message.getProcessId() + AGREEMENT + VERIFICATION, jsonLdContext()); + } + + /** + * Parses the response to an agreement verification message. The JSON-LD structure from the response + * body is expanded and returned. + * + * @return a function that contains the response body or null. + */ + @Override + public Function parseResponse() { + return response -> null; + } + + // TODO refactor according to https://github.com/eclipse-edc/Connector/issues/2763 + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add(DSPACE_PREFIX, DSPACE_SCHEMA) + .build(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegate.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegate.java new file mode 100644 index 00000000000..acdc2535d1e --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegate.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractNegotiationEventMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +import java.util.function.Function; + +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_PREFIX; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.EVENT; + +/** + * Delegate for dispatching contract negotiation event message as defined in the dataspace protocol specification. + */ +public class ContractNegotiationEventMessageHttpDelegate extends DspHttpDispatcherDelegate { + + public ContractNegotiationEventMessageHttpDelegate(JsonLdRemoteMessageSerializer serializer) { + super(serializer); + } + + @Override + public Class getMessageType() { + return ContractNegotiationEventMessage.class; + } + + /** + * Sends a contract negotiation event message. The request body is constructed as defined in the + * dataspace protocol. The request is sent to the remote component using the path from the http + * binding. + * + * @param message the message. + * @return the built okhttp request. + */ + @Override + public Request buildRequest(ContractNegotiationEventMessage message) { + return buildRequest(message, BASE_PATH + message.getProcessId() + EVENT, jsonLdContext()); + } + + /** + * Parses the response to a contract negotiation event message. The JSON-LD structure from the response + * body is expanded and returned. + * + * @return a function that contains the response body or null. + */ + @Override + public Function parseResponse() { + return response -> null; + } + + // TODO refactor according to https://github.com/eclipse-edc/Connector/issues/2763 + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add(DSPACE_PREFIX, DSPACE_SCHEMA) + .build(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegate.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegate.java new file mode 100644 index 00000000000..7240d990a5a --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegate.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; + +import java.util.function.Function; + +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_PREFIX; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.TERMINATION; + +/** + * Delegate for dispatching contract negotiation termination message as defined in the dataspace protocol specification. + */ +public class ContractNegotiationTerminationMessageHttpDelegate extends DspHttpDispatcherDelegate { + + public ContractNegotiationTerminationMessageHttpDelegate(JsonLdRemoteMessageSerializer serializer) { + super(serializer); + } + + @Override + public Class getMessageType() { + return ContractNegotiationTerminationMessage.class; + } + + /** + * Sends a contract negotiation termination message. The request body is constructed as defined + * in the dataspace protocol. The request is sent to the remote component using the path from the + * http binding. + * + * @param message the message. + * @return the built okhttp request. + */ + @Override + public Request buildRequest(ContractNegotiationTerminationMessage message) { + return buildRequest(message, BASE_PATH + message.getProcessId() + TERMINATION, jsonLdContext()); + } + + /** + * Parses the response to a contract negotiation event message. The JSON-LD structure from the response + * body is expanded and returned. + * + * @return a function that contains the response body or null. + */ + @Override + public Function parseResponse() { + return response -> null; + } + + // TODO refactor according to https://github.com/eclipse-edc/Connector/issues/2763 + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add(DSPACE_PREFIX, DSPACE_SCHEMA) + .build(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegate.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegate.java new file mode 100644 index 00000000000..6e3a3c3e2cd --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegate.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.serialization.JsonLdRemoteMessageSerializer; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.util.function.Function; + +import static org.eclipse.edc.jsonld.spi.Namespaces.ODRL_PREFIX; +import static org.eclipse.edc.jsonld.spi.Namespaces.ODRL_SCHEMA; +import static org.eclipse.edc.jsonld.util.JsonLdUtil.expand; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_PREFIX; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_SCHEMA; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; + +/** + * Delegate for dispatching contract request message as defined in the dataspace protocol specification. + */ +public class ContractRequestMessageHttpDelegate extends DspHttpDispatcherDelegate { + + private ObjectMapper mapper; + + public ContractRequestMessageHttpDelegate(JsonLdRemoteMessageSerializer serializer, ObjectMapper mapper) { + super(serializer); + this.mapper = mapper; + } + + @Override + public Class getMessageType() { + return ContractRequestMessage.class; + } + + /** + * Sends a contract request message. The request body is constructed as defined in the dataspace + * protocol. The request is sent to the remote component using the path from the http binding. + * + * @param message the message. + * @return the built okhttp request. + */ + @Override + public Request buildRequest(ContractRequestMessage message) { + if (message.getType() == ContractRequestMessage.Type.INITIAL) { + return buildRequest(message, BASE_PATH + INITIAL_CONTRACT_REQUEST, jsonLdContext()); + } else { + return buildRequest(message, BASE_PATH + message.getProcessId() + CONTRACT_REQUEST, jsonLdContext()); + } + } + + @Override + public Function parseResponse() { + return response -> { + try { + var jsonObject = mapper.readValue(response.body().bytes(), JsonObject.class); + return expand(jsonObject).get(0); + } catch (NullPointerException e) { + throw new EdcException("Failed to read response body, as body was null."); + } catch (IndexOutOfBoundsException e) { + throw new EdcException("Failed to expand JSON-LD in response body.", e); + } catch (IOException e) { + throw new EdcException("Failed to read response body.", e); + } + }; + } + + // TODO refactor according to https://github.com/eclipse-edc/Connector/issues/2763 + private JsonObject jsonLdContext() { + return Json.createObjectBuilder() + .add(DSPACE_PREFIX, DSPACE_SCHEMA) + .add(ODRL_PREFIX, ODRL_SCHEMA) + .build(); + } +} diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000000..e0121021b66 --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering +# +# 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: +# Fraunhofer Institute for Software and Systems Engineering - Initial API and Implementation +# +# + +org.eclipse.edc.protocol.dsp.negotiation.dispatcher.DspNegotiationHttpDispatcherExtension \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegateTest.java new file mode 100644 index 00000000000..f7b7c1a0bdc --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementMessageHttpDelegateTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementMessage; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; + +class ContractAgreementMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { + + private ContractAgreementMessageHttpDelegate delegate; + + @BeforeEach + void setUp() { + delegate = new ContractAgreementMessageHttpDelegate(serializer); + } + + @Test + void getMessageType() { + assertThat(delegate.getMessageType()).isEqualTo(ContractAgreementMessage.class); + } + + @Test + void buildRequest() throws IOException { + var message = message(); + testBuildRequest_shouldReturnRequest(message, BASE_PATH + message.getProcessId() + AGREEMENT); + } + + @Test + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); + } + + @Test + void parseResponse_returnNull() { + testParseResponse_shouldReturnNullFunction_whenResponseBodyNotProcessed(); + } + + private ContractAgreementMessage message() { + var value = "example"; + return ContractAgreementMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .contractAgreement(contractAgreement()) + .build(); + } + + private ContractAgreement contractAgreement() { + return ContractAgreement.Builder.newInstance() + .id(String.valueOf(UUID.randomUUID())) + .providerAgentId("agentId") + .consumerAgentId("agentId") + .assetId("assetId") + .policy(policy()).build(); + } + + private Policy policy() { + var action = Action.Builder.newInstance().type("USE").build(); + var permission = Permission.Builder.newInstance().action(action).build(); + var prohibition = Prohibition.Builder.newInstance().action(action).build(); + var duty = Duty.Builder.newInstance().action(action).build(); + return Policy.Builder.newInstance() + .permission(permission) + .prohibition(prohibition) + .duty(duty) + .build(); + } + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; + } +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegateTest.java new file mode 100644 index 00000000000..2c72f7e095e --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractAgreementVerificationMessageHttpDelegateTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementVerificationMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.VERIFICATION; + +class ContractAgreementVerificationMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { + + private ContractAgreementVerificationMessageHttpDelegate delegate; + + @BeforeEach + void setUp() { + delegate = new ContractAgreementVerificationMessageHttpDelegate(serializer); + } + + @Test + void getMessageType() { + assertThat(delegate.getMessageType()).isEqualTo(ContractAgreementVerificationMessage.class); + } + + @Test + void buildRequest() throws IOException { + var message = message(); + testBuildRequest_shouldReturnRequest(message, BASE_PATH + message.getProcessId() + AGREEMENT + VERIFICATION); + } + + @Test + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); + } + + @Test + void parseResponse_returnNull() { + testParseResponse_shouldReturnNullFunction_whenResponseBodyNotProcessed(); + } + + private ContractAgreementVerificationMessage message() { + var value = "example"; + return ContractAgreementVerificationMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .build(); + } + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; + } +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegateTest.java new file mode 100644 index 00000000000..18350d62f00 --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationEventMessageHttpDelegateTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractNegotiationEventMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.EVENT; + +class ContractNegotiationEventMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { + + private ContractNegotiationEventMessageHttpDelegate delegate; + + @BeforeEach + void setUp() { + delegate = new ContractNegotiationEventMessageHttpDelegate(serializer); + } + + @Test + void getMessageType() { + assertThat(delegate.getMessageType()).isEqualTo(ContractNegotiationEventMessage.class); + } + + @Test + void buildRequest() throws IOException { + var message = message(); + testBuildRequest_shouldReturnRequest(message, BASE_PATH + message.getProcessId() + EVENT); + } + + @Test + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); + } + + @Test + void parseResponse_returnNull() { + testParseResponse_shouldReturnNullFunction_whenResponseBodyNotProcessed(); + } + + private ContractNegotiationEventMessage message() { + var value = "example"; + return ContractNegotiationEventMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .type(ContractNegotiationEventMessage.Type.FINALIZED) + .build(); + } + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; + } +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegateTest.java new file mode 100644 index 00000000000..3d5c99ff344 --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractNegotiationTerminationMessageHttpDelegateTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.TERMINATION; + +class ContractNegotiationTerminationMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { + + private ContractNegotiationTerminationMessageHttpDelegate delegate; + + @BeforeEach + void setUp() { + delegate = new ContractNegotiationTerminationMessageHttpDelegate(serializer); + } + + @Test + void getMessageType() { + assertThat(delegate.getMessageType()).isEqualTo(ContractNegotiationTerminationMessage.class); + } + + @Test + void buildRequest() throws IOException { + var message = message(); + testBuildRequest_shouldReturnRequest(message, BASE_PATH + message.getProcessId() + TERMINATION); + } + + @Test + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); + } + + @Test + void parseResponse_returnNull() { + testParseResponse_shouldReturnNullFunction_whenResponseBodyNotProcessed(); + } + + private ContractNegotiationTerminationMessage message() { + var value = "example"; + return ContractNegotiationTerminationMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .rejectionReason(value) + .build(); + } + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; + } +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegateTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegateTest.java new file mode 100644 index 00000000000..e04980d65a6 --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-dispatcher/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/dispatcher/delegate/ContractRequestMessageHttpDelegateTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering + * + * 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: + * Fraunhofer Institute for Software and Systems Engineering - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.dispatcher.delegate; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.protocol.dsp.spi.dispatcher.DspHttpDispatcherDelegate; +import org.eclipse.edc.protocol.dsp.spi.testfixtures.dispatcher.DspHttpDispatcherDelegateTestBase; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.util.JsonLdUtil.expand; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_CONTRACT_NEGOTIATION; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_NEGOTIATION_PROPERTY_CHECKSUM; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_NEGOTIATION_PROPERTY_PROCESS_ID; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_NEGOTIATION_PROPERTY_STATE; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.DspNegotiationPropertyAndTypeNames.DSPACE_NEGOTIATION_STATE_REQUESTED; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.BASE_PATH; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.spi.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ContractRequestMessageHttpDelegateTest extends DspHttpDispatcherDelegateTestBase { + + private ContractRequestMessageHttpDelegate delegate; + + @BeforeEach + void setUp() { + delegate = new ContractRequestMessageHttpDelegate(serializer, mapper); + } + + @Test + void getMessageType() { + assertThat(delegate.getMessageType()).isEqualTo(ContractRequestMessage.class); + } + + @Test + void buildRequest_initial() throws IOException { + testBuildRequest_shouldReturnRequest(message_initial(), BASE_PATH + INITIAL_CONTRACT_REQUEST); + } + + @Test + void buildRequest() throws IOException { + var message = message(); + testBuildRequest_shouldReturnRequest(message, BASE_PATH + message.getProcessId() + CONTRACT_REQUEST); + } + + @Test + void buildRequest_serializationFails_throwException() { + testBuildRequest_shouldThrowException_whenSerializationFails(message()); + } + + @Test + void parseResponse_returnNegotiation() throws IOException { + var response = mock(Response.class); + var responseBody = mock(ResponseBody.class); + var bytes = "test".getBytes(); + + var expanded = expand(negotiation()).get(0); + + when(response.body()).thenReturn(responseBody); + when(responseBody.bytes()).thenReturn(bytes); + when(mapper.readValue(bytes, JsonObject.class)).thenReturn(negotiation()); + + var result = delegate.parseResponse().apply(response); + + assertThat(result).isEqualTo(expanded); + verify(mapper, times(1)).readValue(bytes, JsonObject.class); + } + + @Test + void parseResponse_responseBodyNull_throwException() { + testParseResponse_shouldThrowException_whenResponseBodyNull(); + } + + @Test + void parseResponse_readingResponseBodyFails_throwException() throws IOException { + testParseResponse_shouldThrowException_whenReadingResponseBodyFails(); + } + + private ContractRequestMessage message() { + var value = "example"; + return ContractRequestMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .dataSet(value) + .contractOffer(contractOffer()) + .type(ContractRequestMessage.Type.COUNTER_OFFER) + .build(); + } + + private ContractRequestMessage message_initial() { + var value = "example"; + return ContractRequestMessage.Builder.newInstance() + .protocol(value) + .processId(value) + .callbackAddress("http://connector") + .dataSet(value) + .contractOffer(contractOffer()) + .type(ContractRequestMessage.Type.INITIAL) + .build(); + } + + private ContractOffer contractOffer() { + return ContractOffer.Builder.newInstance() + .id(String.valueOf(UUID.randomUUID())) + .asset(Asset.Builder.newInstance().id("assetId").build()) + .contractStart(ZonedDateTime.now()) + .contractEnd(ZonedDateTime.now()) + .policy(policy()).build(); + } + + private Policy policy() { + var action = Action.Builder.newInstance().type("USE").build(); + var permission = Permission.Builder.newInstance().action(action).build(); + var prohibition = Prohibition.Builder.newInstance().action(action).build(); + var duty = Duty.Builder.newInstance().action(action).build(); + return Policy.Builder.newInstance() + .permission(permission) + .prohibition(prohibition) + .duty(duty) + .build(); + } + + private JsonObject negotiation() { + var value = "example"; + var builder = Json.createObjectBuilder(); + builder.add(JsonLdKeywords.ID, value); + builder.add(JsonLdKeywords.TYPE, DSPACE_CONTRACT_NEGOTIATION); + + builder.add(DSPACE_NEGOTIATION_PROPERTY_PROCESS_ID, value); + builder.add(DSPACE_NEGOTIATION_PROPERTY_STATE, DSPACE_NEGOTIATION_STATE_REQUESTED); + builder.add(DSPACE_NEGOTIATION_PROPERTY_CHECKSUM, value); + + return builder.build(); + } + + @Override + protected DspHttpDispatcherDelegate delegate() { + return delegate; + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b2cfba617b..f0f94d51281 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,6 +69,7 @@ include(":data-protocols:dsp:dsp-catalog:dsp-catalog-spi") include(":data-protocols:dsp:dsp-catalog:dsp-catalog-transform") include(":data-protocols:dsp:dsp-transfer-process:dsp-transfer-process-transform") include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-api") +include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-http-dispatcher") include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-spi") include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-transform") include(":data-protocols:dsp:dsp-http-core")