From 0ae66c2dcc193a37b645c7f57213ad138873ae50 Mon Sep 17 00:00:00 2001 From: Christophe Loiseau Date: Thu, 12 Sep 2024 09:40:04 +0200 Subject: [PATCH] feat: add CreateDataOffer API endpoint (#1035) --- CHANGELOG.md | 3 + docs/api/postman_collection.json | 38 ++- docs/api/sovity-edc-api-wrapper.yaml | 138 +++++---- .../edc/ext/wrapper/api/ui/UiResource.java | 8 + .../ui/model/DataOfferCreationRequest.java | 43 +++ .../ui/model/PolicyDefinitionChoiceEnum.java | 21 ++ .../ui/model/PolicyDefinitionCreateDto.java | 1 - .../WrapperExtensionContextBuilder.java | 6 +- .../ext/wrapper/api/ui/UiResourceImpl.java | 6 + .../api/ui/pages/asset/AssetApiService.java | 11 + .../data_offer/DataOfferPageApiService.java | 73 ++++- .../de/sovity/edc/e2e/UiApiWrapperTest.java | 266 +++++++++++++++++- 12 files changed, 548 insertions(+), 66 deletions(-) create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java create mode 100644 extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b25642521..50efff6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- Extend the Wrapper API + - Adds `createDataOffer` endpoint to create an asset, policies and a contract definition in a single call + #### Patch Changes ### Deployment Migration Notes diff --git a/docs/api/postman_collection.json b/docs/api/postman_collection.json index 0a7e8e50a..9d8a0b327 100644 --- a/docs/api/postman_collection.json +++ b/docs/api/postman_collection.json @@ -4,7 +4,7 @@ "name": "sovity EDC Community Edition Copy", "description": "This is the official postman collection for the sovity EDC Community Edition.\n\nThe Management-API is based on core-edc v0.2.1.\n\nsovity EDC Community Edition: [https://github.com/sovity/edc-ce](https://github.com/sovity/edc-ce)\n\nLicense: [https://github.com/sovity/edc-ce/blob/main/LICENSE](https://github.com/sovity/edc-ce/blob/main/LICENSE)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31514741" + "_exporter_id": "32949497" }, "item": [ { @@ -854,6 +854,42 @@ } }, "response": [] + }, + { + "name": "Create Data Offer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"uiAssetCreateRequest\": {\n \"id\": \"create-data-offer-1\",\n \"title\": \"Create Data Offer Example\",\n \"language\": \"https://w3id.org/idsa/code/EN\",\n \"description\": \"Testdescription\",\n \"publisherHomepage\": \"https://www.sovity.de\",\n \"licenseUrl\": \"https://www.apache.org/licenses/LICENSE-2.0\",\n \"version\": \"v1.0\",\n \"keywords\": [\n \"keyword1\",\n \"keyword2\"\n ],\n \"mediaType\": \"application/json\",\n \"landingPageUrl\": \"https://www.google.com\",\n \"dataAddressProperties\": {\n \"https://w3id.org/edc/v0.0.1/ns/type\": \"HttpData\",\n \"https://w3id.org/edc/v0.0.1/ns/baseUrl\": \"https://www.google.com\",\n \"https://w3id.org/edc/v0.0.1/ns/method\": \"GET\",\n \"https://w3id.org/edc/v0.0.1/ns/queryParams\": \"\"\n },\n \"dataSource\": {\n \"type\": \"HTTP_DATA\",\n \"httpData\": {\n \"baseUrl\": \"http://example.com/baseUrl/\"\n }\n }\n },\n \"policy\": \"PUBLISH_RESTRICTED\",\n \"uiPolicyExpression\": {\n \"constraints\": [\n {\n \"left\": \"POLICY_EVALUATION_TIME\",\n \"operator\": \"GEQ\",\n \"right\": {\n \"type\": \"STRING\",\n \"value\": \"2024-03-31T22:00:00.000Z\"\n }\n },\n {\n \"left\": \"POLICY_EVALUATION_TIME\",\n \"operator\": \"LT\",\n \"right\": {\n \"type\": \"STRING\",\n \"value\": \"2024-04-30T22:00:00.000Z\"\n }\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{PROVIDER_EDC_MANAGEMENT_URL}}/wrapper/ui/pages/create-asset/", + "host": [ + "{{PROVIDER_EDC_MANAGEMENT_URL}}" + ], + "path": [ + "wrapper", + "ui", + "pages", + "create-asset", + "" + ] + } + }, + "response": [] } ] } diff --git a/docs/api/sovity-edc-api-wrapper.yaml b/docs/api/sovity-edc-api-wrapper.yaml index ef2167e31..8cda83f2b 100644 --- a/docs/api/sovity-edc-api-wrapper.yaml +++ b/docs/api/sovity-edc-api-wrapper.yaml @@ -117,6 +117,26 @@ paths: application/json: schema: $ref: '#/components/schemas/IdResponseDto' + /wrapper/ui/pages/create-data-offer: + post: + tags: + - UI + description: "Create a new asset, contract definition and optional policies.\ + \ Uses the same id for the asset, the contract policy, the access policy and\ + \ the contract definition" + operationId: createDataOffer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DataOfferCreationRequest' + responses: + default: + description: default response + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' /wrapper/ui/pages/policy-page/policy-definitions: post: tags: @@ -918,6 +938,24 @@ components: - EQ - IN - LIKE + DataOfferCreationRequest: + required: + - policy + - uiAssetCreateRequest + type: object + properties: + uiAssetCreateRequest: + $ref: '#/components/schemas/UiAssetCreateRequest' + policy: + type: string + description: Which policy to apply to this asset. + enum: + - DONT_PUBLISH + - PUBLISH_UNRESTRICTED + - PUBLISH_RESTRICTED + uiPolicyExpression: + $ref: '#/components/schemas/UiPolicyExpression' + description: Request to create a data offer OperatorDto: type: string description: Type-Safe ODRL Policy Operator as supported by the sovity product @@ -935,19 +973,6 @@ components: - IS_ALL_OF - IS_ANY_OF - IS_NONE_OF - PolicyDefinitionCreateRequest: - required: - - policy - - policyDefinitionId - type: object - properties: - policyDefinitionId: - type: string - description: Policy Definition ID - policy: - $ref: '#/components/schemas/UiPolicyCreateRequest' - description: "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto" - deprecated: true UiPolicyConstraint: required: - left @@ -964,17 +989,36 @@ components: $ref: '#/components/schemas/UiPolicyLiteral' description: "ODRL AtomicConstraint as supported by the sovity product landscape.\ \ For example 'a EQ b', 'c IN [d, e, f]'" - UiPolicyCreateRequest: + UiPolicyExpression: + required: + - type type: object properties: - constraints: + type: + $ref: '#/components/schemas/UiPolicyExpressionType' + expressions: type: array - description: Conjunction of required constraints - deprecated: true + description: "Only for types AND, OR, XONE. List of sub-expressions to be\ + \ evaluated according to the expressionType." items: - $ref: '#/components/schemas/UiPolicyConstraint' - description: "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)" - deprecated: true + $ref: '#/components/schemas/UiPolicyExpression' + constraint: + $ref: '#/components/schemas/UiPolicyConstraint' + description: ODRL constraint as supported by the sovity product landscape + UiPolicyExpressionType: + type: string + description: | + Ui Policy Expression types: + * `CONSTRAINT` - Expression 'a=b' + * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. + * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. + * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. + enum: + - EMPTY + - CONSTRAINT + - AND + - OR + - XONE UiPolicyLiteral: required: - type @@ -999,6 +1043,30 @@ components: - STRING - STRING_LIST - JSON + PolicyDefinitionCreateRequest: + required: + - policy + - policyDefinitionId + type: object + properties: + policyDefinitionId: + type: string + description: Policy Definition ID + policy: + $ref: '#/components/schemas/UiPolicyCreateRequest' + description: "[Deprecated] Create a Policy Definition. Use PolicyDefinitionCreateDto" + deprecated: true + UiPolicyCreateRequest: + type: object + properties: + constraints: + type: array + description: Conjunction of required constraints + deprecated: true + items: + $ref: '#/components/schemas/UiPolicyConstraint' + description: "[Deprecated] Conjunction of constraints (simplified UiPolicyExpression)" + deprecated: true PolicyDefinitionCreateDto: required: - expression @@ -1011,36 +1079,6 @@ components: expression: $ref: '#/components/schemas/UiPolicyExpression' description: Create a Policy Definition - UiPolicyExpression: - required: - - type - type: object - properties: - type: - $ref: '#/components/schemas/UiPolicyExpressionType' - expressions: - type: array - description: "Only for types AND, OR, XONE. List of sub-expressions to be\ - \ evaluated according to the expressionType." - items: - $ref: '#/components/schemas/UiPolicyExpression' - constraint: - $ref: '#/components/schemas/UiPolicyConstraint' - description: ODRL constraint as supported by the sovity product landscape - UiPolicyExpressionType: - type: string - description: | - Ui Policy Expression types: - * `CONSTRAINT` - Expression 'a=b' - * `AND` - Conjunction of several expressions. Evaluates to true iff all child expressions are true. - * `OR` - Disjunction of several expressions. Evaluates to true iff at least one child expression is true. - * `XONE` - XONE operation. Evaluates to true iff exactly one child expression is true. - enum: - - EMPTY - - CONSTRAINT - - AND - - OR - - XONE UiAssetEditRequest: type: object properties: diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java index 6268537df..a024ed9dc 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResource.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -136,6 +137,13 @@ interface UiResource { @Operation(description = "Delete a Contract Definition") IdResponseDto deleteContractDefinition(@PathParam("contractDefinitionId") String contractDefinitionId); + @POST + @Path("pages/create-data-offer/") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "Create a new asset, contract definition and optional policies. Uses the same id for the asset, the contract policy, the access policy and the contract definition") + IdResponseDto createDataOffer(DataOfferCreationRequest dataOfferCreationRequest); + @GET @Path("pages/catalog-page/data-offers") @Produces(MediaType.APPLICATION_JSON) diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java new file mode 100644 index 000000000..705aff710 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/DataOfferCreationRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +import de.sovity.edc.ext.wrapper.api.common.model.UiAssetCreateRequest; +import de.sovity.edc.ext.wrapper.api.common.model.UiPolicyExpression; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "Request to create a data offer") +public class DataOfferCreationRequest { + + @Schema(description = "The asset to create", requiredMode = REQUIRED) + private UiAssetCreateRequest uiAssetCreateRequest; + + @Schema(description = "Which policy to apply to this asset.", requiredMode = REQUIRED) + private PolicyDefinitionChoiceEnum policy; + + @Schema(description = "Policy Expression.", requiredMode = NOT_REQUIRED) + private UiPolicyExpression uiPolicyExpression; +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java new file mode 100644 index 000000000..3f494f240 --- /dev/null +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionChoiceEnum.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ + +package de.sovity.edc.ext.wrapper.api.ui.model; + +public enum PolicyDefinitionChoiceEnum { + DONT_PUBLISH, + PUBLISH_UNRESTRICTED, + PUBLISH_RESTRICTED +} diff --git a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java index 8bc4d076d..65a73ef58 100644 --- a/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java +++ b/extensions/wrapper/wrapper-api/src/main/java/de/sovity/edc/ext/wrapper/api/ui/model/PolicyDefinitionCreateDto.java @@ -35,4 +35,3 @@ public class PolicyDefinitionCreateDto { @Schema(description = "Policy Expression", requiredMode = Schema.RequiredMode.REQUIRED) private UiPolicyExpression expression; } - diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index 48574717f..2e8a40fbc 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -250,7 +250,11 @@ public static WrapperExtensionContext buildContext( miwConfigBuilder, selfDescriptionService ); - var dataOfferPageApiService = new DataOfferPageApiService(); + var dataOfferPageApiService = new DataOfferPageApiService( + assetApiService, + contractDefinitionApiService, + policyDefinitionApiService + ); var uiResource = new UiResourceImpl( contractAgreementApiService, contractAgreementTransferApiService, diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java index 53697eeff..4bd6ee056 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/UiResourceImpl.java @@ -26,6 +26,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.ContractNegotiationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.ContractTerminationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.DashboardPage; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.model.InitiateCustomTransferRequest; @@ -135,6 +136,11 @@ public IdResponseDto deleteContractDefinition(String contractDefinitionId) { return contractDefinitionApiService.deleteContractDefinition(contractDefinitionId); } + @Override + public IdResponseDto createDataOffer(DataOfferCreationRequest dataOfferCreationRequest) { + return dslContextFactory.transactionResult(trx -> dataOfferPageApiService.createDataOffer(trx, dataOfferCreationRequest)); + } + @Override public List getCatalogPageDataOffers(String connectorEndpoint) { return catalogApiService.fetchDataOffers(connectorEndpoint); diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java index d2266f393..d5d72bdc0 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiService.java @@ -14,6 +14,8 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.asset; +import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.ext.db.jooq.tables.EdcAsset; import de.sovity.edc.ext.wrapper.api.ServiceException; import de.sovity.edc.ext.wrapper.api.common.mappers.AssetMapper; import de.sovity.edc.ext.wrapper.api.common.model.UiAsset; @@ -27,6 +29,7 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; import java.util.Comparator; import java.util.List; @@ -75,4 +78,12 @@ public IdResponseDto deleteAsset(String assetId) { private List getAllAssets() { return assetService.query(QuerySpec.max()).orElseThrow(ServiceException::new).toList(); } + + public boolean assetExists(DSLContext dsl, String assetId) { + val a = Tables.EDC_ASSET; + return dsl.selectCount() + .from(a) + .where(a.ASSET_ID.eq(assetId)) + .fetchSingleInto(Integer.class) > 0; + } } diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java index 43137206a..4bdfd949a 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/data_offer/DataOfferPageApiService.java @@ -1,16 +1,38 @@ package de.sovity.edc.ext.wrapper.api.ui.pages.data_offer; import de.sovity.edc.ext.db.jooq.Tables; +import de.sovity.edc.ext.wrapper.api.ui.model.ContractDefinitionRequest; +import de.sovity.edc.ext.wrapper.api.ui.model.DataOfferCreationRequest; import de.sovity.edc.ext.wrapper.api.ui.model.IdAvailabilityResponse; +import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; +import de.sovity.edc.ext.wrapper.api.ui.model.PolicyDefinitionCreateDto; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterion; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteral; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionLiteralType; +import de.sovity.edc.ext.wrapper.api.ui.model.UiCriterionOperator; +import de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.contract_definitions.ContractDefinitionApiService; +import de.sovity.edc.ext.wrapper.api.ui.pages.policy.PolicyDefinitionApiService; import lombok.RequiredArgsConstructor; import lombok.val; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.jooq.Table; import org.jooq.TableField; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + @RequiredArgsConstructor public class DataOfferPageApiService { + + private final AssetApiService assetApiService; + private final ContractDefinitionApiService contractDefinitionApiService; + private final PolicyDefinitionApiService policyDefinitionApiService; + @NotNull public IdAvailabilityResponse checkIfPolicyIdAvailable(DSLContext dsl, String id) { val table = Tables.EDC_POLICYDEFINITIONS; @@ -37,9 +59,52 @@ public IdAvailabilityResponse checkIfContractDefinitionIdAvailable(DSLContext ds private boolean isIdAvailable(DSLContext dsl, Table table, TableField idField, String id) { return !dsl.fetchExists( - dsl.select(idField) - .from(table) - .where(idField.eq(id)) - ); + dsl.select(idField) + .from(table) + .where(idField.eq(id)) + ); + } + + public IdResponseDto createDataOffer(DSLContext dsl, DataOfferCreationRequest dataOfferCreationRequest) { + val commonId = dataOfferCreationRequest.getUiAssetCreateRequest().getId(); + + val assetIdExists = checkIfAssetIdAvailable(dsl, commonId).isAvailable(); + if (!assetIdExists) { + throw new InvalidRequestException("Asset with id %s already exists".formatted(commonId)); + } + + val policyIdExists = checkIfPolicyIdAvailable(dsl, commonId).isAvailable(); + if (!policyIdExists) { + throw new InvalidRequestException("Policy with id %s already exists".formatted(commonId)); + } + + val contractDefinitionIdExists = checkIfContractDefinitionIdAvailable(dsl, commonId).isAvailable(); + if (!contractDefinitionIdExists) { + throw new InvalidRequestException("Contract definition with id %s already exists".formatted(commonId)); + } + + assetApiService.createAsset(dataOfferCreationRequest.getUiAssetCreateRequest()); + + val maybeNewPolicy = Optional.ofNullable(dataOfferCreationRequest.getUiPolicyExpression()); + + maybeNewPolicy.ifPresent( + policy -> policyDefinitionApiService.createPolicyDefinitionV2(new PolicyDefinitionCreateDto(commonId, policy))); + + val cd = new ContractDefinitionRequest(); + cd.setAssetSelector(List.of(UiCriterion.builder() + .operandLeft(Asset.PROPERTY_ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(commonId) + .build()) + .build())); + cd.setAccessPolicyId(commonId); + cd.setContractPolicyId(commonId); + cd.setContractDefinitionId(commonId); + + contractDefinitionApiService.createContractDefinition(cd); + + return new IdResponseDto(commonId, OffsetDateTime.now()); } } diff --git a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java index 19dd2cbc1..0b9875b62 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -15,16 +15,21 @@ package de.sovity.edc.e2e; import de.sovity.edc.client.EdcClient; +import de.sovity.edc.client.gen.ApiException; +import de.sovity.edc.client.gen.model.ContractDefinitionEntry; import de.sovity.edc.client.gen.model.ContractDefinitionRequest; import de.sovity.edc.client.gen.model.ContractNegotiationRequest; import de.sovity.edc.client.gen.model.ContractNegotiationSimplifiedState; +import de.sovity.edc.client.gen.model.DataOfferCreationRequest; import de.sovity.edc.client.gen.model.DataSourceAvailability; import de.sovity.edc.client.gen.model.DataSourceType; import de.sovity.edc.client.gen.model.InitiateCustomTransferRequest; import de.sovity.edc.client.gen.model.InitiateTransferRequest; import de.sovity.edc.client.gen.model.OperatorDto; import de.sovity.edc.client.gen.model.PolicyDefinitionCreateDto; +import de.sovity.edc.client.gen.model.PolicyDefinitionDto; import de.sovity.edc.client.gen.model.TransferProcessSimplifiedState; +import de.sovity.edc.client.gen.model.UiAsset; import de.sovity.edc.client.gen.model.UiAssetCreateRequest; import de.sovity.edc.client.gen.model.UiAssetEditRequest; import de.sovity.edc.client.gen.model.UiContractNegotiation; @@ -50,6 +55,7 @@ import de.sovity.edc.extension.e2e.extension.E2eScenario; import de.sovity.edc.extension.e2e.extension.E2eTestExtension; import de.sovity.edc.extension.e2e.extension.Provider; +import de.sovity.edc.extension.policy.AlwaysTruePolicyConstants; import de.sovity.edc.extension.utils.junit.DisabledOnGithub; import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; @@ -58,6 +64,7 @@ import lombok.val; import org.awaitility.Awaitility; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -76,6 +83,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; class UiApiWrapperTest { @@ -650,21 +658,23 @@ void canMakeAnOnDemandDataSourceAvailable( .contactPreferredEmailSubject("Subject") .build()) .build()) - .id("asset") + .id("asset") .title("foo") .build()); // act - providerClient.uiApi().editAsset(assetId, UiAssetEditRequest.builder() - .dataSourceOverrideOrNull(UiDataSource.builder() - .type(DataSourceType.HTTP_DATA) - .httpData(UiDataSourceHttpData.builder() - .method(UiDataSourceHttpDataMethod.GET) - .baseUrl("http://example.com/baseUrl") + providerClient.uiApi().editAsset( + assetId, + UiAssetEditRequest.builder() + .dataSourceOverrideOrNull(UiDataSource.builder() + .type(DataSourceType.HTTP_DATA) + .httpData(UiDataSourceHttpData.builder() + .method(UiDataSourceHttpDataMethod.GET) + .baseUrl("http://example.com/baseUrl") + .build()) .build()) - .build()) - .build()); + .build()); val asset = providerClient.uiApi().getAssetPage().getAssets().stream().filter(it -> it.getAssetId().equals(assetId)).findFirst().get(); @@ -676,6 +686,244 @@ void canMakeAnOnDemandDataSourceAvailable( .isEqualTo("\"http://example.com/baseUrl\""); } + @Test + void canCreateDataOfferWithoutAnyNewPolicy( + @Provider EdcClient providerClient + ) { + // arrange + val dataSource = UiDataSource.builder() + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com") + .method(UiDataSourceHttpDataMethod.GET) + .build()) + .type(DataSourceType.HTTP_DATA) + .build(); + + val assetId = "asset"; + val asset = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(assetId) + .title("My asset") + .build(); + + val dataOfferCreateRequest = new DataOfferCreationRequest( + asset, + DataOfferCreationRequest.PolicyEnum.DONT_PUBLISH, + null + ); + + // act + val returnedId = providerClient.uiApi().createDataOffer(dataOfferCreateRequest).getId(); + + // assert + assertThat(returnedId).isEqualTo(assetId); + + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + .isEqualTo(assetId); + } + + @Test + void canCreateDataOfferWithNewPolicy( + @Provider EdcClient providerClient + ) { + // arrange + val dataSource = UiDataSource.builder() + .httpData(UiDataSourceHttpData.builder() + .baseUrl("http://example.com") + .method(UiDataSourceHttpDataMethod.GET) + .build()) + .type(DataSourceType.HTTP_DATA) + .build(); + + val assetId = "asset"; + val asset = UiAssetCreateRequest.builder() + .dataSource(dataSource) + .id(assetId) + .title("My asset") + .build(); + + val dataOfferCreateRequest = new DataOfferCreationRequest( + asset, + DataOfferCreationRequest.PolicyEnum.DONT_PUBLISH, + UiPolicyExpression.builder() + .constraint(UiPolicyConstraint.builder() + .left("foo") + .operator(OperatorDto.EQ) + .right(UiPolicyLiteral.builder() + .value("bar") + .build()) + .build()) + .build() + ); + + // act + val returnedId = providerClient.uiApi().createDataOffer(dataOfferCreateRequest).getId(); + + // assert + assertThat(returnedId).isEqualTo(assetId); + + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)) + .hasSize(1) + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .first() + .isEqualTo(assetId); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + .isEqualTo(assetId); + } + + @Test + void dontCreateAnythingIfTheAssetAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = scenario.createAsset(); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + .hasSize(1) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(assetId); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()).hasSize(0); + } + + @Test + void dontCreateAnythingIfThePolicyAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = "assetId"; + scenario.createPolicy(assetId, OffsetDateTime.now(), OffsetDateTime.now()); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()).hasSize(0); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(1) + .extracting(PolicyDefinitionDto::getPolicyDefinitionId) + .first() + .isEqualTo("assetId"); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()).hasSize(0); + } + + @Test + void dontCreateAnythingIfTheContractDefinitionAlreadyExists( + E2eScenario scenario, + @Provider EdcClient providerClient + ) { + // arrange + val assetId = "assetId"; + val placeholder = scenario.createAsset(); + providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() + .contractDefinitionId(assetId) + .accessPolicyId("always-true") + .contractPolicyId("always-true") + .assetSelector(List.of(UiCriterion.builder() + .operandLeft(Prop.Edc.ID) + .operator(UiCriterionOperator.EQ) + .operandRight(UiCriterionLiteral.builder() + .type(UiCriterionLiteralType.VALUE) + .value(placeholder) + .build()) + .build())) + .build()); + + // act + assertThrows( + ApiException.class, + () -> providerClient.uiApi() + .createDataOffer(DataOfferCreationRequest.builder() + .uiAssetCreateRequest(UiAssetCreateRequest.builder() + .id(assetId) + .dataSource(UiDataSource.builder() + .type(DataSourceType.ON_REQUEST) + .onRequest(UiDataSourceOnRequest.builder() + .contactEmail("foo@example.com") + .contactPreferredEmailSubject("Subject") + .build()) + .build()) + .build()) + .build())); + + // assert + assertThat(providerClient.uiApi().getAssetPage().getAssets()) + // the asset used for the placeholder contract definition + .hasSize(1) + .extracting(UiAsset::getAssetId) + .first() + .isEqualTo(placeholder); + + assertThat(getAllPoliciesExceptTheAlwaysTruePolicy(providerClient)).hasSize(0); + + assertThat(providerClient.uiApi().getContractDefinitionPage().getContractDefinitions()) + .hasSize(1) + .filteredOn(it -> it.getContractDefinitionId().equals(assetId)) + .extracting(ContractDefinitionEntry::getContractDefinitionId) + .first() + // the already existing one, before the data offer creation attempt + .isEqualTo(assetId); + } + + private static @NotNull List getAllPoliciesExceptTheAlwaysTruePolicy(EdcClient edcClient) { + return edcClient.uiApi().getPolicyDefinitionPage().getPolicies().stream().filter(it -> !it.getPolicyDefinitionId().equals( + AlwaysTruePolicyConstants.POLICY_DEFINITION_ID)).toList(); + } + private UiContractNegotiation negotiate( EdcClient consumerClient, ConnectorRemote consumerConnector,