diff --git a/.github/workflows/build-and-publish-edc.yml b/.github/workflows/build-and-publish-edc.yml index a6092ed..97bd169 100644 --- a/.github/workflows/build-and-publish-edc.yml +++ b/.github/workflows/build-and-publish-edc.yml @@ -25,6 +25,9 @@ jobs: uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 with: arguments: build + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ github.token }} - name: Publish package if: ${{ github.ref == 'refs/heads/main' }} uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 diff --git a/.gitignore b/.gitignore index a455c71..79354d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .DS_Store .idea/ +/**/.classpath +/**/.settings +/**/.project # Ignore Gradle project-specific cache directory .gradle diff --git a/build.gradle.kts b/build.gradle.kts index 06b15a4..fcfd296 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,16 @@ allprojects { repositories { mavenCentral() mavenLocal() + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/ids-basecamp/ids-infomodel-java") + credentials { + username = System.getenv("USERNAME") + password = System.getenv("TOKEN") + } + } + } } } diff --git a/gradle.properties b/gradle.properties index 0d39a2a..1132f36 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ assertj=3.23.1 jupiterVersion=5.8.2 mockitoVersion=4.8.0 okHttpVersion=4.10.0 -jsonVersion=20230618 +jsonVersion=20231013 jettyGroup=org.eclipse.jetty jettyVersion=11.0.15 diff --git a/logging-house-client/build.gradle.kts b/logging-house-client/build.gradle.kts index 636d282..07c9610 100644 --- a/logging-house-client/build.gradle.kts +++ b/logging-house-client/build.gradle.kts @@ -13,9 +13,14 @@ val jsonVersion: String by project dependencies { implementation("${edcGroup}:control-plane-core:${edcVersion}") + implementation("${edcGroup}:http-spi:${edcVersion}") implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") implementation("org.json:json:${jsonVersion}") + implementation("org.glassfish.jersey.media:jersey-media-multipart:3.1.3") + + implementation("de.fraunhofer.iais.eis.ids.infomodel:infomodel-java:1.0.2-basecamp") + implementation("de.fraunhofer.iais.eis.ids.infomodel:infomodel-util:1.0.2-basecamp") testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessage.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessage.java new file mode 100644 index 0000000..88854ba --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessage.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 truzzt 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: + * truzzt GmbH - initial API and implementation + * + */ + +package com.truzzt.extension.logginghouse.client; + +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.net.URI; +import java.net.URL; +import java.util.List; + +public record CreateProcessMessage( + URL clearingHouseLogUrl, + URI connectorBaseUrl, + String processId, + List processOwners +) implements RemoteMessage { + + @Override + public String getProtocol() { + return ExtendedMessageProtocolClearing.IDS_EXTENDED_PROTOCOL_CLEARING; + } + + @Override + public String getCounterPartyAddress() { + return clearingHouseLogUrl.toString(); + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessageSender.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessageSender.java new file mode 100644 index 0000000..58fa5ef --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/CreateProcessMessageSender.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 truzzt 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: + * truzzt GmbH - initial API and implementation + * + */ + +package com.truzzt.extension.logginghouse.client; + +import com.truzzt.extension.logginghouse.client.ids.jsonld.JsonLd; +import com.truzzt.extension.logginghouse.client.ids.multipart.CalendarUtil; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsConstants; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsMultipartParts; +import com.truzzt.extension.logginghouse.client.ids.multipart.MultipartResponse; +import com.truzzt.extension.logginghouse.client.ids.multipart.MultipartSenderDelegate; +import com.truzzt.extension.logginghouse.client.ids.multipart.ResponseUtil; +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.fraunhofer.iais.eis.LogMessageBuilder; +import de.fraunhofer.iais.eis.Message; +import de.fraunhofer.iais.eis.MessageProcessedNotificationMessageImpl; +import org.json.JSONObject; + +import java.util.List; + +public class CreateProcessMessageSender implements MultipartSenderDelegate { + + public CreateProcessMessageSender() { + } + + @Override + public Message buildMessageHeader(CreateProcessMessage createProcessMessage, DynamicAttributeToken token) { + return new LogMessageBuilder() + ._modelVersion_(IdsConstants.INFORMATION_MODEL_VERSION) + ._issued_(CalendarUtil.gregorianNow()) + ._securityToken_(token) + ._issuerConnector_(createProcessMessage.connectorBaseUrl()) + ._senderAgent_(createProcessMessage.connectorBaseUrl()) + .build(); + } + + @Override + public String buildMessagePayload(CreateProcessMessage createProcessMessage) { + var jo = new JSONObject(); + jo.put("owners", createProcessMessage.processOwners()); + return jo.toString(); + } + + @Override + public MultipartResponse getResponseContent(IdsMultipartParts parts) throws Exception { + return ResponseUtil.parseMultipartStringResponse(parts, JsonLd.getObjectMapper()); + } + + @Override + public List> getAllowedResponseTypes() { + return List.of(MessageProcessedNotificationMessageImpl.class); + } + + @Override + public Class getMessageType() { + return CreateProcessMessage.class; + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ExtendedMessageProtocolClearing.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ExtendedMessageProtocolClearing.java new file mode 100644 index 0000000..931fa9d --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ExtendedMessageProtocolClearing.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client; + +public final class ExtendedMessageProtocolClearing { + + private static final String EXTENDED_SUFFIX = "-extended-clearing"; + public static final String IDS_MULTIPART = "ids-multipart"; + public static final String IDS_EXTENDED_PROTOCOL_CLEARING = String.format("%s%s", IDS_MULTIPART, EXTENDED_SUFFIX); + + private ExtendedMessageProtocolClearing() { + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsClearingHouseServiceImpl.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsClearingHouseServiceImpl.java new file mode 100644 index 0000000..8ce4ee7 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsClearingHouseServiceImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client; + +import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFinalized; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.transfer.spi.event.TransferProcessTerminated; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.event.Event; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventSubscriber; +import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.Hostname; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class IdsClearingHouseServiceImpl implements EventSubscriber { + + private final RemoteMessageDispatcherRegistry dispatcherRegistry; + private final URI connectorBaseUrl; + private final URL clearingHouseLogUrl; + private final ContractNegotiationStore contractNegotiationStore; + private final TransferProcessStore transferProcessStore; + private final Monitor monitor; + + public IdsClearingHouseServiceImpl( + RemoteMessageDispatcherRegistry dispatcherRegistry, + Hostname hostname, + URL clearingHouseLogUrl, + ContractNegotiationStore contractNegotiationStore, + TransferProcessStore transferProcessStore, + Monitor monitor) { + this.dispatcherRegistry = dispatcherRegistry; + this.clearingHouseLogUrl = clearingHouseLogUrl; + this.contractNegotiationStore = contractNegotiationStore; + this.transferProcessStore = transferProcessStore; + this.monitor = monitor; + + try { + connectorBaseUrl = getConnectorBaseUrl(hostname); + } catch (URISyntaxException e) { + throw new EdcException("Could not create connectorBaseUrl. Hostname can be set using:" + + " edc.hostname", e); + } + } + + public void createProcess(ContractAgreement contractAgreement, URL clearingHouseLogUrl) { + // Create PID + List processOwners = new ArrayList<>(); + processOwners.add(contractAgreement.getConsumerId()); + processOwners.add(contractAgreement.getProviderId()); + + monitor.info("Creating Process in LoggingHouse"); + var logMessage = new CreateProcessMessage(clearingHouseLogUrl, connectorBaseUrl, contractAgreement.getId(), processOwners); + dispatcherRegistry.dispatch(Object.class, logMessage); + } + + public void logContractAgreement(ContractAgreement contractAgreement, URL clearingHouseLogUrl) { + monitor.info("Logging contract agreement to LoggingHouse"); + var logMessage = new LogMessage(clearingHouseLogUrl, connectorBaseUrl, contractAgreement); + dispatcherRegistry.dispatch(Object.class, logMessage); + } + + public void logTransferProcess(TransferProcess transferProcess, URL clearingHouseLogUrl) { + monitor.info("Logging transferprocess to LoggingHouse"); + var logMessage = new LogMessage(clearingHouseLogUrl, connectorBaseUrl, transferProcess); + dispatcherRegistry.dispatch(Object.class, logMessage); + } + + @Override + public void on(EventEnvelope event) { + try { + if (event.getPayload() instanceof ContractNegotiationFinalized contractNegotiationFinalized) { + var contractAgreement = resolveContractAgreement(contractNegotiationFinalized); + var pid = contractAgreement.getId(); + var extendedUrl = new URL(clearingHouseLogUrl + "/" + pid); + + createProcess(contractAgreement, clearingHouseLogUrl); + logContractAgreement(contractAgreement, extendedUrl); + } else if (event.getPayload() instanceof TransferProcessTerminated transferProcessTerminated) { + var transferProcess = resolveTransferProcess(transferProcessTerminated); + var pid = transferProcess.getContractId(); + var extendedUrl = new URL(clearingHouseLogUrl + "/" + pid); + logTransferProcess(transferProcess, extendedUrl); + } + } catch (Exception e) { + throw new EdcException("Could not create extended clearinghouse url."); + } + } + + private ContractAgreement resolveContractAgreement(ContractNegotiationFinalized contractNegotiationFinalized) throws NullPointerException { + var contractNegotiationId = contractNegotiationFinalized.getContractNegotiationId(); + var contractNegotiation = contractNegotiationStore.findById(contractNegotiationId); + return Objects.requireNonNull(contractNegotiation).getContractAgreement(); + } + + private TransferProcess resolveTransferProcess(TransferProcessTerminated transferProcessTerminated) { + var transferProcessId = transferProcessTerminated.getTransferProcessId(); + return transferProcessStore.findById(transferProcessId); + } + + private URI getConnectorBaseUrl(Hostname hostname) throws URISyntaxException { + return new URI(String.format("https://%s/", hostname.get())); + } +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsMultipartClearingRemoteMessageDispatcher.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsMultipartClearingRemoteMessageDispatcher.java new file mode 100644 index 0000000..6f757cc --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/IdsMultipartClearingRemoteMessageDispatcher.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client; + +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsMultipartRemoteMessageDispatcher; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsMultipartSender; + +public class IdsMultipartClearingRemoteMessageDispatcher extends IdsMultipartRemoteMessageDispatcher { + + public IdsMultipartClearingRemoteMessageDispatcher(IdsMultipartSender idsMultipartSender) { + super(idsMultipartSender); + } + + @Override + public String protocol() { + return ExtendedMessageProtocolClearing.IDS_EXTENDED_PROTOCOL_CLEARING; + } +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessage.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessage.java new file mode 100644 index 0000000..0b0e16a --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessage.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client; + +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.net.URI; +import java.net.URL; + +public record LogMessage(URL clearingHouseLogUrl, + URI connectorBaseUrl, + Object eventToLog) implements RemoteMessage { + @Override + public String getProtocol() { + return ExtendedMessageProtocolClearing.IDS_EXTENDED_PROTOCOL_CLEARING; + } + + @Override + public String getCounterPartyAddress() { + return clearingHouseLogUrl.toString(); + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessageSender.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessageSender.java new file mode 100644 index 0000000..3fbb1d5 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LogMessageSender.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client; + +import com.truzzt.extension.logginghouse.client.ids.jsonld.JsonLd; +import com.truzzt.extension.logginghouse.client.ids.multipart.CalendarUtil; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsConstants; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsMultipartParts; +import com.truzzt.extension.logginghouse.client.ids.multipart.MultipartResponse; +import com.truzzt.extension.logginghouse.client.ids.multipart.MultipartSenderDelegate; +import com.truzzt.extension.logginghouse.client.ids.multipart.ResponseUtil; +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.fraunhofer.iais.eis.LogMessageBuilder; +import de.fraunhofer.iais.eis.Message; +import de.fraunhofer.iais.eis.MessageProcessedNotificationMessageImpl; +import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.spi.EdcException; +import org.json.JSONObject; + +import java.util.List; + +public class LogMessageSender implements MultipartSenderDelegate { + + public LogMessageSender() { + } + + @Override + public Message buildMessageHeader(LogMessage logMessage, DynamicAttributeToken token) { + return new LogMessageBuilder() + ._modelVersion_(IdsConstants.INFORMATION_MODEL_VERSION) + ._issued_(CalendarUtil.gregorianNow()) + ._securityToken_(token) + ._issuerConnector_(logMessage.connectorBaseUrl()) + ._senderAgent_(logMessage.connectorBaseUrl()) + .build(); + } + + @Override + public String buildMessagePayload(LogMessage logMessage) { + if (logMessage.eventToLog() instanceof ContractAgreement contractAgreement) { + return buildContractAgreementPayload(contractAgreement); + } else if (logMessage.eventToLog() instanceof TransferProcess transferProcess) { + return buildTransferProcessPayload(transferProcess); + } else { + throw new EdcException(String.format("ObjectType %s not supported in LogMessageSender", + logMessage.eventToLog().getClass())); + } + } + + @Override + public MultipartResponse getResponseContent(IdsMultipartParts parts) throws Exception { + return ResponseUtil.parseMultipartStringResponse(parts, JsonLd.getObjectMapper()); + } + + @Override + public List> getAllowedResponseTypes() { + return List.of(MessageProcessedNotificationMessageImpl.class); + } + + @Override + public Class getMessageType() { + return LogMessage.class; + } + + private String buildContractAgreementPayload(ContractAgreement contractAgreement) { + var jo = new JSONObject(); + jo.put("AgreementId", contractAgreement.getId()); + jo.put("ProviderId", contractAgreement.getProviderId()); + jo.put("ConsumerId", contractAgreement.getConsumerId()); + jo.put("ContractSigningDate", contractAgreement.getContractSigningDate()); + jo.put("Policy", contractAgreement.getPolicy()); + jo.put("AssetId", contractAgreement.getAssetId()); + return jo.toString(); + } + + private String buildTransferProcessPayload(TransferProcess transferProcess) { + var jo = new JSONObject(); + jo.put("transferProcessId", transferProcess.getId()); + var dataRequest = transferProcess.getDataRequest(); + jo.put("contractId", dataRequest.getContractId()); + jo.put("connectorId", dataRequest.getConnectorId()); + return jo.toString(); + } +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LoggingHouseClientExtension.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LoggingHouseClientExtension.java index 7bffb8a..ea01dfa 100644 --- a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LoggingHouseClientExtension.java +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/LoggingHouseClientExtension.java @@ -13,11 +13,24 @@ package com.truzzt.extension.logginghouse.client; +import com.truzzt.extension.logginghouse.client.ids.jsonld.JsonLd; +import com.truzzt.extension.logginghouse.client.ids.multipart.IdsMultipartSender; +import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFinalized; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.transfer.spi.event.TransferProcessTerminated; +import org.eclipse.edc.connector.transfer.spi.store.TransferProcessStore; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.http.EdcHttpClient; +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.Hostname; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; import java.net.MalformedURLException; import java.net.URL; @@ -25,9 +38,27 @@ public class LoggingHouseClientExtension implements ServiceExtension { + public static final String LOGGINGHOUSE_CLIENT_EXTENSION = "LoggingHouseClientExtension"; private static final String TYPE_MANAGER_SERIALIZER_KEY = "ids-clearinghouse"; + @Inject + private TypeManager typeManager; + @Inject + private EventRouter eventRouter; + @Inject + private IdentityService identityService; + @Inject + private RemoteMessageDispatcherRegistry dispatcherRegistry; + + @Inject + private Hostname hostname; + + @Inject + private ContractNegotiationStore contractNegotiationStore; + @Inject + private TransferProcessStore transferProcessStore; + private static final Map CONTEXT_MAP = Map.of( "cat", "http://w3id.org/mds/data-categories#", "ids", "https://w3id.org/idsa/core/", @@ -77,6 +108,49 @@ private URL readUrlFromSettings(ServiceExtensionContext context, String settings } } + private void registerEventSubscriber(ServiceExtensionContext context) { + var eventSubscriber = new IdsClearingHouseServiceImpl( + dispatcherRegistry, + hostname, + clearingHouseLogUrl, + contractNegotiationStore, + transferProcessStore, + monitor); + + eventRouter.registerSync(ContractNegotiationFinalized.class, eventSubscriber); + eventRouter.registerSync(TransferProcessTerminated.class, eventSubscriber); + context.registerService(IdsClearingHouseServiceImpl.class, eventSubscriber); + } + + private void registerSerializerClearingHouseMessages(ServiceExtensionContext context) { + typeManager.registerContext(TYPE_MANAGER_SERIALIZER_KEY, JsonLd.getObjectMapper()); + registerCommonTypes(typeManager); + } + + private void registerCommonTypes(TypeManager typeManager) { + typeManager.registerSerializer(TYPE_MANAGER_SERIALIZER_KEY, LogMessage.class, + new MultiContextJsonLdSerializer<>(LogMessage.class, CONTEXT_MAP)); + typeManager.registerSerializer(TYPE_MANAGER_SERIALIZER_KEY, CreateProcessMessage.class, + new MultiContextJsonLdSerializer<>(CreateProcessMessage.class, CONTEXT_MAP)); + } + + private void registerClearingHouseMessageSenders(ServiceExtensionContext context) { + var httpClient = context.getService(EdcHttpClient.class); + var monitor = context.getMonitor(); + var objectMapper = typeManager.getMapper(TYPE_MANAGER_SERIALIZER_KEY); + + var logMessageSender = new LogMessageSender(); + var createProcessMessageSender = new CreateProcessMessageSender(); + + var idsMultipartSender = new IdsMultipartSender(monitor, httpClient, identityService, objectMapper); + var dispatcher = new IdsMultipartClearingRemoteMessageDispatcher(idsMultipartSender); + dispatcher.register(logMessageSender); + dispatcher.register(createProcessMessageSender); + + dispatcherRegistry.register(dispatcher); + } + + @Override public void start() { monitor.info("Starting Logginghouse client extension."); diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/MultiContextJsonLdSerializer.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/MultiContextJsonLdSerializer.java new file mode 100644 index 0000000..985dfb2 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/MultiContextJsonLdSerializer.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 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 + * sovity GmbH - Adaption and changes + * + */ + +package com.truzzt.extension.logginghouse.client; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +/** + * Custom Jackson serializer for any {@link Object}. Adds type and context information to result object. + * + * @param The object that should be serialized. + */ +public class MultiContextJsonLdSerializer extends JsonSerializer { + private final Class type; + private final Object contextInformation; + + private static final ThreadLocal CURRENT_RECURSION_DEPTH = ThreadLocal.withInitial(() -> 0); + + public MultiContextJsonLdSerializer(Class type, Object contextInformation) { + this.type = type; + this.contextInformation = contextInformation; + } + + @Override + public void serialize(T value, JsonGenerator generator, SerializerProvider provider) throws IOException { + CURRENT_RECURSION_DEPTH.set(CURRENT_RECURSION_DEPTH.get() + 1); + + Map propertiesMap = null; + try { + var field = value.getClass().getDeclaredField("properties"); + field.setAccessible(true); + propertiesMap = (Map) field.get(value); + } catch (NoSuchFieldException | IllegalAccessException ignore) { + // empty + } + removeProperty(value, "properties"); + + // remove properties "comment" and "label" + removeProperty(value, "comment"); + removeProperty(value, "label"); + + generator.writeStartObject(); + + // write new object + var serializer = instantiateSerializerFromProvider(provider, type); + serializer.unwrappingSerializer(null).serialize(value, generator, provider); + + if (CURRENT_RECURSION_DEPTH.get() == 1) { + // context needed only once (for parent object) + generator.writeObjectField("@context", contextInformation); + } + + // add type property + var type = getTypeName(value.getClass()); + if (type != null) { + generator.writeObjectField("@type", type); + } + + // add custom properties as root properties (not in a separate "properties" map) + if (propertiesMap != null) { + for (var key : propertiesMap.keySet()) { + var val = propertiesMap.get(key); + if (val instanceof URI) { + generator.writeStringField(key, val.toString()); + } else { + generator.writeObjectField(key, val); + } + } + } + + generator.writeEndObject(); + + CURRENT_RECURSION_DEPTH.set(CURRENT_RECURSION_DEPTH.get() - 1); + } + + @Override + public void serializeWithType(T value, JsonGenerator gen, SerializerProvider provider, TypeSerializer ser) throws IOException { + serialize(value, gen, provider); + } + + private void removeProperty(Object value, String name) { + try { + var field = value.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(value, null); + } catch (NoSuchFieldException | IllegalAccessException ignore) { + // empty + } + } + + private String getTypeName(Class clazz) { + var typeName = clazz.getAnnotation(JsonTypeName.class); + if (typeName != null) { + var value = typeName.value(); + if (value == null) { + getTypeName(clazz.getSuperclass()); + } + return value; + } + return null; + } + + private JsonSerializer instantiateSerializerFromProvider(SerializerProvider provider, Class type) throws JsonMappingException { + var javaType = provider.constructType(type); + var beanDescription = provider.getConfig().introspect(javaType); + var staticTyping = provider.isEnabled(MapperFeature.USE_STATIC_TYPING); + return BeanSerializerFactory.instance.findBeanOrAddOnSerializer(provider, javaType, beanDescription, staticTyping); + } +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLd.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLd.java new file mode 100644 index 0000000..c542ae0 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLd.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.text.SimpleDateFormat; + +public final class JsonLd { + + public static ObjectMapper getObjectMapper() { + var customMapper = new ObjectMapper(); + customMapper.registerModule(new JavaTimeModule()); + customMapper.registerModule(new JsonLdModule()); + + customMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")); + customMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + customMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + customMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + customMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + customMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + + return customMapper; + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLdModule.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLdModule.java new file mode 100644 index 0000000..a5c4bfe --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/JsonLdModule.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.net.URI; +import javax.xml.datatype.XMLGregorianCalendar; + +public class JsonLdModule extends SimpleModule { + + public JsonLdModule() { + super(); + + addSerializer(URI.class, new UriSerializer()); + addDeserializer(URI.class, new UriDeserializer()); + + addSerializer(XMLGregorianCalendar.class, new XmlGregorianCalendarSerializer()); + addDeserializer(XMLGregorianCalendar.class, new XmlGregorianCalendarDeserializer()); + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriDeserializer.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriDeserializer.java new file mode 100644 index 0000000..cf684c0 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriDeserializer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import java.io.IOException; +import java.net.URI; + +public class UriDeserializer extends StdDeserializer { + + public UriDeserializer() { + super(URI.class); + } + + @Override + public URI deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return URI.create(getValue(parser.readValueAsTree(), parser)); + } + + private String getValue(TreeNode node, JsonParser parser) throws JsonParseException { + if (node instanceof TextNode) { + return ((TextNode) node).textValue(); + } + + if (node instanceof ObjectNode) { + return getValue(node.get("@id"), parser); + } + + throw new JsonParseException(parser, "Could not read URI"); + } + +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriSerializer.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriSerializer.java new file mode 100644 index 0000000..a2e94dc --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/UriSerializer.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.net.URI; + +public class UriSerializer extends StdSerializer { + + public UriSerializer() { + super(URI.class); + } + + @Override + public void serialize(URI value, JsonGenerator gen, SerializerProvider provider) throws IOException { + var serializedUri = value.toString(); + + var context = gen.getOutputContext(); + if (context.getCurrentName() != null && context.getCurrentName().contains("@id")) { + gen.writeString(serializedUri); + } else { + gen.writeStartObject(); + gen.writeStringField("@id", serializedUri); + gen.writeEndObject(); + } + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarDeserializer.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarDeserializer.java new file mode 100644 index 0000000..116c11c --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarDeserializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.GregorianCalendar; +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +public class XmlGregorianCalendarDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public XmlGregorianCalendarDeserializer() { + super(XMLGregorianCalendar.class); + } + + @Override + public XMLGregorianCalendar deserialize(JsonParser parser, DeserializationContext context) throws IOException { + var tree = parser.readValueAsTree(); + + try { + var value = ((TextNode) tree.get("@value")).textValue(); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(ZonedDateTime.parse(value))); + } catch (DatatypeConfigurationException e) { + return null; + } + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarSerializer.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarSerializer.java new file mode 100644 index 0000000..d42744f --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/jsonld/XmlGregorianCalendarSerializer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.jsonld; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import javax.xml.datatype.XMLGregorianCalendar; + +public class XmlGregorianCalendarSerializer extends StdSerializer { + + public XmlGregorianCalendarSerializer() { + super(XMLGregorianCalendar.class); + } + + @Override + public void serialize(XMLGregorianCalendar calendar, JsonGenerator generator, SerializerProvider provider) throws IOException { + var sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + sdf.setCalendar(calendar.toGregorianCalendar()); + var formatted = sdf.format(calendar.toGregorianCalendar().getTime()); + + generator.writeStartObject(); + generator.writeStringField("@value", formatted); + generator.writeStringField("@type", "http://www.w3.org/2001/XMLSchema#dateTimeStamp"); + generator.writeEndObject(); + } +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/CalendarUtil.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/CalendarUtil.java new file mode 100644 index 0000000..7a7b8bf --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/CalendarUtil.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 Daimler TSS 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: + * Daimler TSS GmbH - Initial API and Implementation + * Fraunhofer Institute for Software and Systems Engineering - added method + * + */ + +package com.truzzt.extension.logginghouse.client.ids.multipart; + +import java.time.ZonedDateTime; +import java.util.GregorianCalendar; +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +public final class CalendarUtil { + + public static XMLGregorianCalendar gregorianNow() { + try { + GregorianCalendar gregorianCalendar = GregorianCalendar.from(ZonedDateTime.now()); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(gregorianCalendar); + } catch (DatatypeConfigurationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsConstants.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsConstants.java new file mode 100644 index 0000000..7a927e8 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsConstants.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020, 2021 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * Fraunhofer Institute for Software and Systems Engineering - add more values + * + */ + +package com.truzzt.extension.logginghouse.client.ids.multipart; + +public final class IdsConstants { + + public static final String INFORMATION_MODEL_VERSION = "4.1.3"; + + public static final String TOKEN_SCOPE = "idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartParts.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartParts.java new file mode 100644 index 0000000..72bcfdf --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartParts.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020, 2021 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 com.truzzt.extension.logginghouse.client.ids.multipart; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.InputStream; +import java.util.Objects; + +public class IdsMultipartParts { + + private final InputStream header; + + @Nullable + private final InputStream payload; + + private IdsMultipartParts(@NotNull InputStream header, @Nullable InputStream payload) { + this.header = header; + this.payload = payload; + } + + @NotNull + public InputStream getHeader() { + return header; + } + + @Nullable + public InputStream getPayload() { + return payload; + } + + public static class Builder { + private InputStream header; + + @Nullable + private InputStream payload; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder header(InputStream header) { + this.header = header; + return this; + } + + public Builder payload(InputStream payload) { + this.payload = payload; + return this; + } + + public IdsMultipartParts build() { + Objects.requireNonNull(header, "header"); + return new IdsMultipartParts(header, payload); + } + } + +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartRemoteMessageDispatcher.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartRemoteMessageDispatcher.java new file mode 100644 index 0000000..7342e56 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartRemoteMessageDispatcher.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 - 2022 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 com.truzzt.extension.logginghouse.client.ids.multipart; + +import org.eclipse.edc.connector.transfer.spi.types.protocol.TransferCompletionMessage; +import org.eclipse.edc.connector.transfer.spi.types.protocol.TransferStartMessage; +import org.eclipse.edc.connector.transfer.spi.types.protocol.TransferTerminationMessage; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.message.RemoteMessageDispatcher; +import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class IdsMultipartRemoteMessageDispatcher implements RemoteMessageDispatcher { + + public static final String PROTOCOL = "ids-multipart"; + + private final IdsMultipartSender multipartSender; + private final Map, MultipartSenderDelegate> delegates = new HashMap<>(); + private final List> unsupportedMessages = List.of( + TransferStartMessage.class, + TransferCompletionMessage.class, + TransferTerminationMessage.class + ); + + public IdsMultipartRemoteMessageDispatcher(IdsMultipartSender idsMultipartSender) { + this.multipartSender = idsMultipartSender; + } + + public void register(MultipartSenderDelegate delegate) { + delegates.put(delegate.getMessageType(), delegate); + } + + @Override + public String protocol() { + return PROTOCOL; + } + + @Override + public CompletableFuture> dispatch(Class responseType, M message) { + Objects.requireNonNull(message, "Message was null"); + + if (unsupportedMessages.stream().anyMatch(it -> it.isInstance(message))) { // these messages are not supposed to be sent on ids-multipart. + return CompletableFuture.completedFuture(null); + } + + var delegate = (MultipartSenderDelegate) delegates.get(message.getClass()); + if (delegate == null) { + throw new EdcException("Message sender not found for message type: " + message.getClass().getName()); + } + + return multipartSender.send(message, delegate); + } + +} \ No newline at end of file diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartSender.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartSender.java new file mode 100644 index 0000000..3677494 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/IdsMultipartSender.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2020 - 2022 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 + * Microsoft Corporation - Use IDS Webhook address for JWT audience claim + * Fraunhofer Institute for Software and Systems Engineering - refactoring + * + */ + +package com.truzzt.extension.logginghouse.client.ids.multipart; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder; +import de.fraunhofer.iais.eis.Message; +import de.fraunhofer.iais.eis.TokenFormat; +import jakarta.ws.rs.core.MediaType; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; +import okhttp3.MultipartReader; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; +import org.glassfish.jersey.media.multipart.ContentDisposition; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpHeaders; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static java.lang.String.format; +import static java.util.concurrent.CompletableFuture.failedFuture; + +public class IdsMultipartSender { + + private final Monitor monitor; + private final EdcHttpClient httpClient; + private final IdentityService identityService; + private final ObjectMapper objectMapper; + + public IdsMultipartSender(Monitor monitor, EdcHttpClient httpClient, + IdentityService identityService, + ObjectMapper objectMapper) { + this.monitor = monitor; + this.httpClient = httpClient; + this.identityService = identityService; + this.objectMapper = objectMapper; + } + + public CompletableFuture> send(M request, MultipartSenderDelegate senderDelegate) { + var remoteConnectorAddress = request.getCounterPartyAddress(); + + // Get Dynamic Attribute Token + var tokenResult = obtainDynamicAttributeToken(remoteConnectorAddress); + if (tokenResult.failed()) { + String message = "Failed to obtain token: " + String.join(",", tokenResult.getFailureMessages()); + monitor.severe(message); + return failedFuture(new EdcException(message)); + } + + var token = tokenResult.getContent(); + + // Get recipient address + var requestUrl = HttpUrl.parse(remoteConnectorAddress); + if (requestUrl == null) { + return failedFuture(new IllegalArgumentException("Connector address not specified")); + } + + // Build IDS message header + Message message; + try { + message = senderDelegate.buildMessageHeader(request, token); + } catch (Exception e) { + return failedFuture(e); + } + + // Build multipart header part + var headerPartHeaders = new Headers.Builder() + .add("Content-Disposition", "form-data; name=\"header\"") + .build(); + + RequestBody headerRequestBody; + try { + headerRequestBody = RequestBody.create( + objectMapper.writeValueAsString(message), + okhttp3.MediaType.get(MediaType.APPLICATION_JSON)); + } catch (IOException exception) { + return failedFuture(exception); + } + + var headerPart = MultipartBody.Part.create(headerPartHeaders, headerRequestBody); + + // Build IDS message payload + String payload; + try { + payload = senderDelegate.buildMessagePayload(request); + } catch (Exception e) { + return failedFuture(e); + } + + // Build multipart payload part + MultipartBody.Part payloadPart = null; + if (payload != null) { + var payloadRequestBody = RequestBody.create(payload, + okhttp3.MediaType.get(MediaType.APPLICATION_JSON)); + + var payloadPartHeaders = new Headers.Builder() + .add("Content-Disposition", "form-data; name=\"payload\"") + .build(); + + payloadPart = MultipartBody.Part.create(payloadPartHeaders, payloadRequestBody); + } + + // Build multipart body + var multipartBuilder = new MultipartBody.Builder() + .setType(okhttp3.MediaType.get(MediaType.MULTIPART_FORM_DATA)) + .addPart(headerPart); + + if (payloadPart != null) { + multipartBuilder.addPart(payloadPart); + } + + var multipartRequestBody = multipartBuilder.build(); + + // Build HTTP request + var httpRequest = new Request.Builder() + .url(requestUrl) + .addHeader("Content-Type", MediaType.MULTIPART_FORM_DATA) + .post(multipartRequestBody) + .build(); + + return httpClient.executeAsync(httpRequest, r -> { + monitor.debug("Response received from connector. Status " + r.code()); + if (r.isSuccessful()) { + try (var body = r.body()) { + if (body == null) { + throw new EdcException("Received an empty body response from connector"); + } else { + var parts = extractResponseParts(body); + var response = senderDelegate.getResponseContent(parts); + + checkResponseType(response, senderDelegate); + + return StatusResult.success(response.payload()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new EdcException(format("Received an error from connector (%s): %s %s", requestUrl, r.code(), r.message())); + } + }); + } + + protected Result obtainDynamicAttributeToken(String recipientAddress) { + var tokenParameters = TokenParameters.Builder.newInstance() + .scope(IdsConstants.TOKEN_SCOPE) + .audience(recipientAddress) + .build(); + return identityService.obtainClientCredentials(tokenParameters) + .map(credentials -> new DynamicAttributeTokenBuilder() + ._tokenFormat_(TokenFormat.JWT) + ._tokenValue_(credentials.getToken()) + .build() + ); + } + + protected IdsMultipartParts extractResponseParts(ResponseBody body) throws Exception { + InputStream header = null; + InputStream payload = null; + try (var multipartReader = new MultipartReader(Objects.requireNonNull(body))) { + MultipartReader.Part part; + while ((part = multipartReader.nextPart()) != null) { + var httpHeaders = HttpHeaders.of( + part.headers().toMultimap(), + (a, b) -> a.equalsIgnoreCase("Content-Disposition") + ); + + var value = httpHeaders.firstValue("Content-Disposition").orElse(null); + if (value == null) { + continue; + } + + var contentDisposition = new ContentDisposition(value); + var multipartName = contentDisposition.getParameters().get("name"); + + if ("header".equalsIgnoreCase(multipartName)) { + header = new ByteArrayInputStream(part.body().readByteArray()); + } else if ("payload".equalsIgnoreCase(multipartName)) { + payload = new ByteArrayInputStream(part.body().readByteArray()); + } + } + } + + return IdsMultipartParts.Builder.newInstance() + .header(header) + .payload(payload) + .build(); + } + + public void checkResponseType(MultipartResponse response, MultipartSenderDelegate senderDelegate) { + var type = senderDelegate.getAllowedResponseTypes(); + if (!type.contains(response.header().getClass())) { + throw new EdcException(String.format("Received %s but expected %s.", response.header().getClass(), type)); + } + } + +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartResponse.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartResponse.java new file mode 100644 index 0000000..83b2ce5 --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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 implementation + * + */ + +package com.truzzt.extension.logginghouse.client.ids.multipart; + +import de.fraunhofer.iais.eis.Message; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record MultipartResponse(Message header, @Nullable T payload) { + + public MultipartResponse(@NotNull Message header, @Nullable T payload) { + this.header = header; + this.payload = payload; + } +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartSenderDelegate.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartSenderDelegate.java new file mode 100644 index 0000000..b5e8bdb --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/MultipartSenderDelegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 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 com.truzzt.extension.logginghouse.client.ids.multipart; + +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.fraunhofer.iais.eis.Message; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.util.List; + +public interface MultipartSenderDelegate { + + Message buildMessageHeader(M request, DynamicAttributeToken token) throws Exception; + + String buildMessagePayload(M request) throws Exception; + + MultipartResponse getResponseContent(IdsMultipartParts parts) throws Exception; + + List> getAllowedResponseTypes(); + + Class getMessageType(); +} diff --git a/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/ResponseUtil.java b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/ResponseUtil.java new file mode 100644 index 0000000..493cc6e --- /dev/null +++ b/logging-house-client/src/main/java/com/truzzt/extension/logginghouse/client/ids/multipart/ResponseUtil.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 - 2022 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 com.truzzt.extension.logginghouse.client.ids.multipart; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iais.eis.Message; + +import java.io.IOException; + +public class ResponseUtil { + + public static MultipartResponse parseMultipartStringResponse(IdsMultipartParts parts, ObjectMapper objectMapper) throws IOException { + var header = objectMapper.readValue(parts.getHeader(), Message.class); + + String payload = null; + if (parts.getPayload() != null) { + payload = new String(parts.getPayload().readAllBytes()); + } + + return new MultipartResponse<>(header, payload); + } + +}