All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.tractusx.irs.edc.client.EdcSubmodelClientImpl Maven / Gradle / Ivy

/********************************************************************************
 * Copyright (c) 2022,2024
 *       2022: ZF Friedrichshafen AG
 *       2022: ISTOS GmbH
 *       2022,2024: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
 *       2022,2023: BOSCH AG
 * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 ********************************************************************************/
package org.eclipse.tractusx.irs.edc.client;

import static org.eclipse.tractusx.irs.edc.client.cache.endpointdatareference.EndpointDataReferenceStatus.TokenStatus;
import static org.eclipse.tractusx.irs.edc.client.configuration.JsonLdConfiguration.NAMESPACE_EDC_ID;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.validator.routines.UrlValidator;
import org.eclipse.edc.spi.types.domain.edr.EndpointDataReference;
import org.eclipse.tractusx.irs.edc.client.cache.endpointdatareference.EndpointDataReferenceCacheService;
import org.eclipse.tractusx.irs.edc.client.cache.endpointdatareference.EndpointDataReferenceStatus;
import org.eclipse.tractusx.irs.edc.client.configuration.JsonLdConfiguration;
import org.eclipse.tractusx.irs.edc.client.exceptions.ContractNegotiationException;
import org.eclipse.tractusx.irs.edc.client.exceptions.EdcClientException;
import org.eclipse.tractusx.irs.edc.client.exceptions.TransferProcessException;
import org.eclipse.tractusx.irs.edc.client.exceptions.UsagePolicyExpiredException;
import org.eclipse.tractusx.irs.edc.client.exceptions.UsagePolicyPermissionException;
import org.eclipse.tractusx.irs.edc.client.model.CatalogItem;
import org.eclipse.tractusx.irs.edc.client.model.NegotiationResponse;
import org.eclipse.tractusx.irs.edc.client.model.SubmodelDescriptor;
import org.eclipse.tractusx.irs.edc.client.model.notification.EdcNotification;
import org.eclipse.tractusx.irs.edc.client.model.notification.EdcNotificationResponse;
import org.eclipse.tractusx.irs.edc.client.model.notification.NotificationContent;
import org.eclipse.tractusx.irs.edc.client.util.Masker;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.StopWatch;

/**
 * Public API facade for EDC domain
 */
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({ "PMD.TooManyMethods",
                    "PMD.ExcessiveImports",
                    "PMD.UseObjectForClearerAPI"
})
public class EdcSubmodelClientImpl implements EdcSubmodelClient {

    private static final String DT_DCAT_TYPE_ID = "'" + JsonLdConfiguration.NAMESPACE_DCT + "type'.'@id'";
    private static final String DT_TAXONOMY_REGISTRY =
            JsonLdConfiguration.NAMESPACE_CX_TAXONOMY + "DigitalTwinRegistry";
    private static final String DT_EDC_TYPE = JsonLdConfiguration.NAMESPACE_EDC + "type";
    private static final String DT_DATA_CORE_REGISTRY = "data.core.digitalTwinRegistry";

    private final EdcConfiguration config;
    private final ContractNegotiationService contractNegotiationService;
    private final EdcDataPlaneClient edcDataPlaneClient;
    private final AsyncPollingService pollingService;
    private final RetryRegistry retryRegistry;
    private final EDCCatalogFacade catalogFacade;
    private final EndpointDataReferenceCacheService endpointDataReferenceCacheService;
    private final UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS);

    private static void stopWatchOnEdcTask(final StopWatch stopWatch) {
        stopWatch.stop();
        log.info("EDC Task '{}' took {} ms", stopWatch.getLastTaskName(), stopWatch.getLastTaskTimeMillis());
    }

    private CompletableFuture sendNotificationAsync(final String assetId,
            final EdcNotification notification, final StopWatch stopWatch,
            final EndpointDataReference endpointDataReference) {

        return pollingService.createJob()
                             .action(() -> sendSubmodelNotification(assetId, notification, stopWatch,
                                     endpointDataReference))
                             .timeToLive(config.getSubmodel().getRequestTtl())
                             .description("waiting for submodel notification to be sent")
                             .build()
                             .schedule();
    }

    private Optional retrieveSubmodelData(final String submodelDataplaneUrl,
            final StopWatch stopWatch, final EndpointDataReference endpointDataReference) {
        if (endpointDataReference != null) {
            log.info("Retrieving data from EDC data plane for dataReference with id {}", endpointDataReference.getId());
            final String payload = edcDataPlaneClient.getData(endpointDataReference, submodelDataplaneUrl);
            stopWatchOnEdcTask(stopWatch);

            return Optional.of(new SubmodelDescriptor(endpointDataReference.getContractId(), payload));
        }

        return Optional.empty();
    }

    private Optional retrieveEndpointReference(final String storageId,
            final StopWatch stopWatch) {

        log.info("Retrieving dataReference from storage for storageId (assetId or contractAgreementId): {}",
                Masker.mask(storageId));
        final var dataReference = endpointDataReferenceCacheService.getEndpointDataReferenceFromStorage(storageId);

        if (dataReference.isPresent()) {
            final EndpointDataReference ref = dataReference.get();
            log.info("Retrieving Endpoint Reference data from EDC data plane with id: {}", ref.getId());
            stopWatchOnEdcTask(stopWatch);
            return Optional.of(ref);
        }

        return Optional.empty();
    }

    private Optional sendSubmodelNotification(final String assetId,
            final EdcNotification notification, final StopWatch stopWatch,
            final EndpointDataReference endpointDataReference) {

        if (endpointDataReference != null) {
            log.info("Sending dataReference to EDC data plane for assetId '{}'", assetId);
            final EdcNotificationResponse response = edcDataPlaneClient.sendData(endpointDataReference, notification);
            stopWatchOnEdcTask(stopWatch);
            return Optional.of(response);
        }

        return Optional.empty();
    }

    @Override
    public CompletableFuture getSubmodelPayload(final String connectorEndpoint,
            final String submodelDataplaneUrl, final String assetId, final String bpn) throws EdcClientException {

        final CheckedSupplier> waitingForSubmodelRetrieval = () -> {
            log.info("Requesting raw SubmodelPayload for endpoint '{}'.", connectorEndpoint);
            final StopWatch stopWatch = new StopWatch();
            stopWatch.start("Get EDC Submodel task for raw payload, endpoint " + connectorEndpoint);

            final EndpointDataReference dataReference = getEndpointDataReference(connectorEndpoint, assetId, bpn);

            return pollingService.createJob()
                                 .action(() -> retrieveSubmodelData(submodelDataplaneUrl, stopWatch, dataReference))
                                 .timeToLive(config.getSubmodel().getRequestTtl())
                                 .description("waiting for submodel retrieval")
                                 .build()
                                 .schedule();
        };

        return execute(connectorEndpoint, waitingForSubmodelRetrieval);
    }

    private EndpointDataReference getEndpointDataReference(final String connectorEndpoint, final String assetId,
            final String bpn) throws EdcClientException {

        final EndpointDataReference result;

        log.info("Retrieving endpoint data reference from cache for asset id: {}", assetId);
        final var cachedReference = endpointDataReferenceCacheService.getEndpointDataReference(assetId);

        if (cachedReference.tokenStatus() == TokenStatus.VALID) {
            log.info("Endpoint data reference found in cache with token status valid, reusing cache record.");
            result = cachedReference.endpointDataReference();
        } else {
            result = getEndpointDataReferenceAndAddToStorage(connectorEndpoint, assetId, cachedReference, bpn);
        }

        return result;
    }

    private EndpointDataReference getEndpointDataReferenceAndAddToStorage(final String connectorEndpoint,
            final String assetId, final EndpointDataReferenceStatus cachedEndpointDataReference, final String bpn)
            throws EdcClientException {
        try {
            final EndpointDataReference endpointDataReference = awaitEndpointReferenceForAsset(connectorEndpoint,
                    NAMESPACE_EDC_ID, assetId, cachedEndpointDataReference, bpn).get();
            endpointDataReferenceCacheService.putEndpointDataReferenceIntoStorage(assetId, endpointDataReference);

            return endpointDataReference;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new EdcClientException(e);
        } catch (CompletionException | ExecutionException e) {
            throw new EdcClientException(e);
        }
    }

    @Override
    public CompletableFuture sendNotification(final String connectorEndpoint,
            final String assetId, final EdcNotification notification, final String bpn)
            throws EdcClientException {
        return execute(connectorEndpoint, () -> {
            final StopWatch stopWatch = new StopWatch();
            stopWatch.start("Send EDC notification task, endpoint " + connectorEndpoint);
            final EndpointDataReference endpointDataReference = getEndpointDataReference(connectorEndpoint, assetId,
                    bpn);

            return sendNotificationAsync(assetId, notification, stopWatch, endpointDataReference);
        });
    }

    @Override
    public List> getEndpointReferencesForAsset(final String endpointAddress,
            final String filterKey, final String filterValue, final String bpn) throws EdcClientException {
        return execute(endpointAddress, () -> getEndpointReferencesForAsset(endpointAddress, filterKey, filterValue,
                new EndpointDataReferenceStatus(null, TokenStatus.REQUIRED_NEW), bpn));
    }

    @Override
    public List> getEndpointReferencesForAsset(final String endpointAddress,
            final String filterKey, final String filterValue,
            final EndpointDataReferenceStatus endpointDataReferenceStatus, final String bpn) throws EdcClientException {
        final StopWatch stopWatch = new StopWatch();
        stopWatch.start("Get EDC Submodel task for shell descriptor, endpoint " + endpointAddress);

        final String providerWithSuffix = appendSuffix(endpointAddress, config.getControlplane().getProviderSuffix());

        // CatalogItem = contract offer
        final List contractOffers = catalogFacade.fetchCatalogByFilter(providerWithSuffix, filterKey,
                filterValue, bpn);

        if (contractOffers.isEmpty()) {
            throw new EdcClientException(
                    "Catalog is empty for endpointAddress '%s' filterKey '%s', filterValue '%s'".formatted(
                            endpointAddress, filterKey, filterValue));
        }

        return createCompletableFuturesForContractOffers(endpointDataReferenceStatus, bpn, contractOffers,
                providerWithSuffix, stopWatch);
    }

    // We need to process each contract offer in parallel
    // (see src/docs/arc42/cross-cutting/discovery-DTR--multiple-EDCs-with-multiple-DTRs.puml
    // and src/docs/arc42/cross-cutting/discovery-DTR--multiple-EDCs-with-multiple-DTRs--detailed.puml)
    private @NotNull List> createCompletableFuturesForContractOffers(
            final EndpointDataReferenceStatus endpointDataReferenceStatus, final String bpn,
            final List contractOffers, final String providerWithSuffix, final StopWatch stopWatch) {
        return contractOffers.stream().map(contractOffer -> {
            final NegotiationResponse negotiationResponse;
            try {
                negotiationResponse = negotiateContract(endpointDataReferenceStatus, contractOffer, providerWithSuffix,
                        bpn);

                final String storageId = getStorageId(endpointDataReferenceStatus, negotiationResponse);

                return pollingService.createJob()
                                     .action(() -> retrieveEndpointReference(storageId, stopWatch))
                                     .timeToLive(config.getSubmodel().getRequestTtl())
                                     .description("waiting for Endpoint Reference retrieval")
                                     .build()
                                     .schedule();
            } catch (EdcClientException e) {
                log.warn(("Negotiate contract failed for "
                        + "endpointDataReferenceStatus = '%s', catalogItem = '%s', providerWithSuffix = '%s' ").formatted(
                        endpointDataReferenceStatus, contractOffer, providerWithSuffix));
                return CompletableFuture.failedFuture(e);
            }

        }).toList();
    }

    @Override
    public List> getEndpointReferencesForRegistryAsset(
            final String endpointAddress, final String bpn) throws EdcClientException {
        return execute(endpointAddress, () -> getEndpointReferencesForRegistryAsset(endpointAddress,
                new EndpointDataReferenceStatus(null, TokenStatus.REQUIRED_NEW), bpn));
    }

    public List> getEndpointReferencesForRegistryAsset(
            final String endpointAddress, final EndpointDataReferenceStatus endpointDataReferenceStatus,
            final String bpn) throws EdcClientException {
        final StopWatch stopWatch = new StopWatch();
        stopWatch.start("Get EndpointDataReference task for shell descriptor, endpoint " + endpointAddress);
        final String providerWithSuffix = appendSuffix(endpointAddress, config.getControlplane().getProviderSuffix());

        // CatalogItem = contract offer
        final List contractOffers = new ArrayList<>(
                catalogFacade.fetchCatalogByFilter(providerWithSuffix, DT_DCAT_TYPE_ID, DT_TAXONOMY_REGISTRY, bpn));

        if (contractOffers.isEmpty()) {
            final List contractOffersDataCore = catalogFacade.fetchCatalogByFilter(providerWithSuffix,
                    DT_EDC_TYPE, DT_DATA_CORE_REGISTRY, bpn);
            contractOffers.addAll(contractOffersDataCore);
        }

        if (contractOffers.isEmpty()) {
            throw new EdcClientException(
                    "No DigitalTwinRegistry contract offers found for endpointAddress '%s' filterKey '%s', filterValue '%s' or filterKey '%s', filterValue '%s'".formatted(
                            endpointAddress, DT_DCAT_TYPE_ID, DT_TAXONOMY_REGISTRY, DT_EDC_TYPE,
                            DT_DATA_CORE_REGISTRY));
        }

        return createCompletableFuturesForContractOffers(endpointDataReferenceStatus, bpn, contractOffers,
                providerWithSuffix, stopWatch);
    }

    private NegotiationResponse negotiateContract(final EndpointDataReferenceStatus endpointDataReferenceStatus,
            final CatalogItem catalogItem, final String providerWithSuffix, final String bpn)
            throws EdcClientException {
        final NegotiationResponse response;
        try {
            response = contractNegotiationService.negotiate(providerWithSuffix, catalogItem,
                    endpointDataReferenceStatus, bpn);
        } catch (TransferProcessException | ContractNegotiationException e) {
            throw new EdcClientException(("Negotiation failed for endpoint '%s', " + "tokenStatus '%s', "
                    + "providerWithSuffix '%s', catalogItem '%s'").formatted(
                    endpointDataReferenceStatus.endpointDataReference(), endpointDataReferenceStatus.tokenStatus(),
                    providerWithSuffix, catalogItem), e);
        } catch (UsagePolicyExpiredException | UsagePolicyPermissionException e) {
            throw new EdcClientException("Asset could not be negotiated for providerWithSuffix '%s', BPN '%s', catalogItem '%s'".formatted(providerWithSuffix, bpn, catalogItem), e);
        }
        return response;
    }

    private CompletableFuture awaitEndpointReferenceForAsset(final String endpointAddress,
            final String filterKey, final String filterValue,
            final EndpointDataReferenceStatus endpointDataReferenceStatus, final String bpn) throws EdcClientException {
        final StopWatch stopWatch = new StopWatch();

        stopWatch.start("Get EDC Submodel task for shell descriptor, endpoint " + endpointAddress);
        final String providerWithSuffix = appendSuffix(endpointAddress, config.getControlplane().getProviderSuffix());

        final List items = catalogFacade.fetchCatalogByFilter(providerWithSuffix, filterKey, filterValue,
                bpn);

        final NegotiationResponse response = contractNegotiationService.negotiate(providerWithSuffix,
                items.stream().findFirst().orElseThrow(), endpointDataReferenceStatus, bpn);

        final String storageId = getStorageId(endpointDataReferenceStatus, response);

        return pollingService.createJob()
                             .action(() -> retrieveEndpointReference(storageId, stopWatch))
                             .timeToLive(config.getSubmodel().getRequestTtl())
                             .description("waiting for Endpoint Reference retrieval")
                             .build()
                             .schedule();
    }

    private static String getStorageId(final EndpointDataReferenceStatus endpointDataReferenceStatus,
            final NegotiationResponse response) {
        final String storageId;
        if (response != null) {
            storageId = response.getContractAgreementId();
        } else {
            storageId = endpointDataReferenceStatus.endpointDataReference().getContractId();
        }
        return storageId;
    }

    private String appendSuffix(final String endpointAddress, final String providerSuffix) {
        String addressWithSuffix;
        if (endpointAddress.endsWith(providerSuffix)) {
            addressWithSuffix = endpointAddress;
        } else if (endpointAddress.endsWith("/") && providerSuffix.startsWith("/")) {
            addressWithSuffix = endpointAddress.substring(0, endpointAddress.length() - 1) + providerSuffix;
        } else {
            addressWithSuffix = endpointAddress + providerSuffix;
        }
        return addressWithSuffix;
    }

    @SuppressWarnings({ "PMD.AvoidRethrowingException",
                        "PMD.AvoidCatchingGenericException"
    })
    private  T execute(final String endpointAddress, final CheckedSupplier supplier) throws EdcClientException {
        if (!urlValidator.isValid(endpointAddress)) {
            throw new IllegalArgumentException(String.format("Malformed endpoint address '%s'", endpointAddress));
        }
        final String host = URI.create(endpointAddress).getHost();
        final Retry retry = retryRegistry.retry(host, "default");
        try {
            return Retry.decorateCallable(retry, supplier::get).call();
        } catch (EdcClientException e) {
            throw e;
        } catch (Exception e) {
            throw new EdcClientException(e);
        }
    }

    /**
     * Functional interface for a supplier that may throw a checked exception.
     *
     * @param  the returned type
     */
    private interface CheckedSupplier {
        T get() throws EdcClientException;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy