diff --git a/CHANGELOG.md b/CHANGELOG.md index 035c423b3..0490db0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes +- UIAsset: Replaced unsafe additional and private properties with safer alternative fields `customJsonAsString` (**not** affected by Json LD manipulation) and `customJsonLdAsString` (affected by Json LD manipulation), along with their private counterparts. + #### Patch Changes ### Deployment Migration Notes diff --git a/UPDATES.md b/UPDATES.md new file mode 100644 index 000000000..62e413204 --- /dev/null +++ b/UPDATES.md @@ -0,0 +1,32 @@ + +# Updates checklist + +This is a checklist about the workarounds that we had to use and may cause trouble in the future. +These are not easily testable and require a manual check. + +--- + +After each EDC version update + +- [ ] Check if `org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder` added a new + field and adjust the builder in `de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder.fromEditMetadataRequest` accordingly + +## Context + +### Asset.toBuilder + +A list of the element that may break when updating the EDC version. + +In `de.sovity.edc.ext.wrapper.api.ui.pages.asset.AssetBuilder.fromEditMetadataRequest` + +When re-creating the asset, we can't re-use the `Asset.toBuilder()` as it doesn't allow us to remove properties. + +We must therefore re-build the asset using the same content as that `.toBuilder()`. + +If the Eclipse EDC adds a field in this builder, we will miss it and any write to the JsonLd via the web API +will remove that hypothetical new field. + +#### Workaround + +On the EDC version update, check that `org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder` doesn't set more +fields than what we set. If a new field was added, add it to this function too. diff --git a/docs/sovity-edc-api-wrapper.yaml b/docs/sovity-edc-api-wrapper.yaml index 1f6471ec2..c8ae99553 100644 --- a/docs/sovity-edc-api-wrapper.yaml +++ b/docs/sovity-edc-api-wrapper.yaml @@ -558,34 +558,23 @@ components: type: string description: Data Address description: Data Address - additionalProperties: - type: object - additionalProperties: - type: string - description: Custom Asset Properties (that are strings) - description: Custom Asset Properties (that are strings) - additionalJsonProperties: - type: object - additionalProperties: - type: string - description: Custom Asset Properties (that are not strings but other JSON - values) - description: Custom Asset Properties (that are not strings but other JSON - values) - privateProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that are strings) - description: Private Asset Properties (that are strings) - privateJsonProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that are not strings but other - JSON values) - description: Private Asset Properties (that are not strings but other JSON - values) + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. description: Type-Safe OpenAPI generator friendly Asset Create DTO that supports an opinionated subset of the original EDC Asset Entity. IdResponseDto: @@ -839,34 +828,23 @@ components: type: string description: Temporal coverage end date (inclusive) format: date - additionalProperties: - type: object - additionalProperties: - type: string - description: Custom Asset Properties (that are strings) - description: Custom Asset Properties (that are strings) - additionalJsonProperties: - type: object - additionalProperties: - type: string - description: Custom Asset Properties (that are not strings but other JSON - values) - description: Custom Asset Properties (that are not strings but other JSON - values) - privateProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that are strings) - description: Private Asset Properties (that are strings) - privateJsonProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that are not strings but other - JSON values) - description: Private Asset Properties (that are not strings but other JSON - values) + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. description: Data for editing an asset. AssetPage: required: @@ -1010,37 +988,26 @@ components: type: string description: Temporal coverage end date (inclusive) format: date - additionalProperties: - type: object - additionalProperties: - type: string - description: Unhandled Asset Properties (that were strings) - description: Unhandled Asset Properties (that were strings) - additionalJsonProperties: - type: object - additionalProperties: - type: string - description: Unhandled Asset Properties (that were not strings but other - JSON values) - description: Unhandled Asset Properties (that were not strings but other - JSON values) - privateProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that were strings) - description: Private Asset Properties (that were strings) - privateJsonProperties: - type: object - additionalProperties: - type: string - description: Private Asset Properties (that were not strings but other - JSON values) - description: Private Asset Properties (that were not strings but other JSON - values) assetJsonLd: type: string description: Contains the entire asset in the JSON-LD format + customJsonAsString: + type: string + description: Contains serialized custom properties in the JSON format. + customJsonLdAsString: + type: string + description: "Contains serialized custom properties in the JSON LD format.\ + \ Contrary to the customJsonAsString field, this string must represent\ + \ a JSON LD object and will be affected by JSON LD compaction and expansion.\ + \ Due to a technical limitation, the properties can't be booleans." + privateCustomJsonAsString: + type: string + description: Same as customJsonAsString but the data will be stored in the + private properties. + privateCustomJsonLdAsString: + type: string + description: Same as customJsonLdAsString but the data will be stored in + the private properties. The same limitations apply. description: Type-Safe Asset Metadata as needed by our UI UiContractOffer: required: diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java index 8af27b0ab..f75dbd6f1 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAsset.java @@ -138,18 +138,26 @@ public class UiAsset { @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private LocalDate temporalCoverageToInclusive; - @Schema(description = "Unhandled Asset Properties (that were strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalProperties; - - @Schema(description = "Unhandled Asset Properties (that were not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalJsonProperties; - - @Schema(description = "Private Asset Properties (that were strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateProperties; - - @Schema(description = "Private Asset Properties (that were not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateJsonProperties; - @Schema(description = "Contains the entire asset in the JSON-LD format", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String assetJsonLd; + + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java index 69c2de0b4..ac1930880 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetCreateRequest.java @@ -108,15 +108,23 @@ public class UiAssetCreateRequest { @Schema(description = "Data Address", requiredMode = Schema.RequiredMode.REQUIRED) private Map dataAddressProperties; - @Schema(description = "Custom Asset Properties (that are strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalProperties; - - @Schema(description = "Custom Asset Properties (that are not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalJsonProperties; - - @Schema(description = "Private Asset Properties (that are strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateProperties; - - @Schema(description = "Private Asset Properties (that are not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateJsonProperties; + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java index f321bae58..53ea64c10 100644 --- a/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java +++ b/extensions/wrapper/wrapper-common-api/src/main/java/de/sovity/edc/ext/wrapper/api/common/model/UiAssetEditMetadataRequest.java @@ -102,15 +102,23 @@ public class UiAssetEditMetadataRequest { @Schema(description = "Temporal coverage end date (inclusive)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private LocalDate temporalCoverageToInclusive; - @Schema(description = "Custom Asset Properties (that are strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalProperties; - - @Schema(description = "Custom Asset Properties (that are not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map additionalJsonProperties; - - @Schema(description = "Private Asset Properties (that are strings)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateProperties; - - @Schema(description = "Private Asset Properties (that are not strings but other JSON values)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - private Map privateJsonProperties; + @Schema(description = "Contains serialized custom properties in the JSON format.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonAsString; + + @Schema(description = "Contains serialized custom properties in the JSON LD format. " + + "Contrary to the customJsonAsString field, this string must represent a JSON LD object " + + "and will be affected by JSON LD compaction and expansion. " + + "Due to a technical limitation, the properties can't be booleans.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String customJsonLdAsString; + + @Schema(description = "Same as customJsonAsString but the data will be stored in the private properties.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonAsString; + + @Schema(description = "Same as customJsonLdAsString but the data will be stored in the private properties. " + + "The same limitations apply.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String privateCustomJsonLdAsString; } diff --git a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts index e6a15314d..1b5893174 100644 --- a/extensions/wrapper/wrapper-common-mappers/build.gradle.kts +++ b/extensions/wrapper/wrapper-common-mappers/build.gradle.kts @@ -1,8 +1,9 @@ val lombokVersion: String by project +val assertj: String by project val edcGroup: String by project val edcVersion: String by project -val assertj: String by project +val jsonUnit: String by project val mockitoVersion: String by project plugins { @@ -27,11 +28,12 @@ dependencies { testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") testImplementation("${edcGroup}:json-ld:${edcVersion}") + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.mockito:mockito-inline:${mockitoVersion}") testImplementation("org.mockito:mockito-junit-jupiter:${mockitoVersion}") - testImplementation("org.assertj:assertj-core:${assertj}") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java index a390ede30..3fbee875a 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/JsonBuilderUtils.java @@ -60,4 +60,13 @@ protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder builder.add(key, value); return builder; } + + protected static JsonObjectBuilder addNonNullJsonValue(JsonObjectBuilder builder, String key, JsonValue value) { + if (value == null || value.getValueType() == JsonValue.ValueType.NULL) { + return builder; + } + + builder.add(key, value); + return builder; + } } diff --git a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java index 8895a6fb2..b48acacb8 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java +++ b/extensions/wrapper/wrapper-common-mappers/src/main/java/de/sovity/edc/ext/wrapper/api/common/mappers/utils/UiAssetMapper.java @@ -26,6 +26,7 @@ import jakarta.json.JsonValue; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.val; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -98,6 +99,8 @@ public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, St var publisher = JsonLdUtils.object(properties, Prop.Dcterms.PUBLISHER); uiAsset.setPublisherHomepage(JsonLdUtils.string(publisher, Prop.Foaf.HOMEPAGE)); + uiAsset.setCustomJsonAsString(JsonLdUtils.string(properties, Prop.SovityDcatExt.CUSTOM_JSON)); + uiAsset.setCreatorOrganizationName(creatorOrganizationName); // Additional / Remaining Properties @@ -140,19 +143,40 @@ public UiAsset buildUiAsset(JsonObject assetJsonLd, String connectorEndpoint, St HttpDatasourceHints.BODY, HttpDatasourceHints.METHOD, HttpDatasourceHints.PATH, - HttpDatasourceHints.QUERY_PARAMS + HttpDatasourceHints.QUERY_PARAMS, + + Prop.SovityDcatExt.CUSTOM_JSON )); - uiAsset.setAdditionalProperties(getStringProperties(remaining)); - uiAsset.setAdditionalJsonProperties(getJsonProperties(remaining)); + + // custom properties + val serializedJsonLd = packAsJsonLdProperties(remaining); + uiAsset.setCustomJsonLdAsString(serializedJsonLd); // Private Properties - var privateProperties = JsonLdUtils.tryCompact(getPrivateProperties(assetJsonLd)); - uiAsset.setPrivateProperties(getStringProperties(privateProperties)); - uiAsset.setPrivateJsonProperties(getJsonProperties(privateProperties)); + val privateProperties = getPrivateProperties(assetJsonLd); + if (privateProperties != null) { + val privateCustomJson = JsonLdUtils.string(privateProperties, Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON); + uiAsset.setPrivateCustomJsonAsString(privateCustomJson); + + val privateRemaining = removeHandledProperties( + privateProperties, + List.of(Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON)); + val privateSerializedJsonLd = packAsJsonLdProperties(privateRemaining); + uiAsset.setPrivateCustomJsonLdAsString(privateSerializedJsonLd); + } return uiAsset; } + private static String packAsJsonLdProperties(JsonObject remaining) { + val customJsonLd = Json.createObjectBuilder(); + for (val e : remaining.entrySet()) { + customJsonLd.add(e.getKey(), e.getValue()); + } + JsonObject compacted = JsonLdUtils.tryCompact(customJsonLd.build()); + return JsonUtils.toJson(compacted); + } + @SneakyThrows @Nullable public JsonObject buildAssetJsonLd( @@ -213,37 +237,49 @@ private JsonObjectBuilder getAssetProperties( .add(Prop.Foaf.NAME, organizationName)); var dataAddress = uiAssetCreateRequest.getDataAddressProperties(); - if (dataAddress.get(Prop.Edc.TYPE).equals("HttpData")) { + if (dataAddress != null && dataAddress.get(Prop.Edc.TYPE).equals("HttpData")) { addNonNull(properties, HttpDatasourceHints.BODY, trueIfTrue(dataAddress, Prop.Edc.PROXY_BODY)); addNonNull(properties, HttpDatasourceHints.PATH, trueIfTrue(dataAddress, Prop.Edc.PROXY_PATH)); addNonNull(properties, HttpDatasourceHints.QUERY_PARAMS, trueIfTrue(dataAddress, Prop.Edc.PROXY_QUERY_PARAMS)); addNonNull(properties, HttpDatasourceHints.METHOD, trueIfTrue(dataAddress, Prop.Edc.PROXY_METHOD)); } - var additionalProperties = uiAssetCreateRequest.getAdditionalProperties(); - if (additionalProperties != null) { - additionalProperties.forEach((k, v) -> addNonNull(properties, k, v)); - } + addNonNull(properties, Prop.SovityDcatExt.CUSTOM_JSON, uiAssetCreateRequest.getCustomJsonAsString()); - var additionalJsonProperties = uiAssetCreateRequest.getAdditionalJsonProperties(); - if (additionalJsonProperties != null) { - additionalJsonProperties.forEach((k, v) -> addNonNullJsonValue(properties, k, v)); + val jsonLdStr = uiAssetCreateRequest.getCustomJsonLdAsString(); + if (jsonLdStr != null) { + val jsonLd = JsonUtils.parseJsonObj(jsonLdStr); + for (val e : jsonLd.entrySet()) { + addNonNullJsonValue(properties, e.getKey(), e.getValue()); + } } return properties; } + private void add(JsonObject overrides, JsonObjectBuilder properties, String key, String value) { + val override = JsonLdUtils.string(overrides, key); + if (override != null) { + properties.add(key, override); + } + properties.add(key, value); + } + private JsonObjectBuilder getAssetPrivateProperties(UiAssetCreateRequest uiAssetCreateRequest) { var privateProperties = Json.createObjectBuilder(); - var stringProperties = uiAssetCreateRequest.getPrivateProperties(); - if (stringProperties != null) { - stringProperties.forEach((k, v) -> addNonNull(privateProperties, k, v)); + val privateJsonStr = uiAssetCreateRequest.getPrivateCustomJsonAsString(); + if (privateJsonStr != null) { + addNonNull( + privateProperties, + Prop.SovityDcatExt.PRIVATE_CUSTOM_JSON, + privateJsonStr); } - var jsonProperties = uiAssetCreateRequest.getPrivateJsonProperties(); - if (jsonProperties != null) { - jsonProperties.forEach((k, v) -> addNonNullJsonValue(privateProperties, k, v)); + val privateJsonLdStr = uiAssetCreateRequest.getPrivateCustomJsonLdAsString(); + if (privateJsonLdStr != null) { + val privateJsonLd = JsonUtils.parseJsonObj(privateJsonLdStr); + privateJsonLd.forEach((k, v) -> addNonNullJsonValue(privateProperties, k, v)); } return privateProperties; @@ -260,22 +296,6 @@ private JsonObjectBuilder getDataAddress(UiAssetCreateRequest uiAssetCreateReque .add(Prop.Edc.PROPERTIES, Json.createObjectBuilder(props)); } - private Map getStringProperties(JsonObject jsonObject) { - return getPropertyMap( - jsonObject, - it -> it.getValueType() == JsonValue.ValueType.STRING, - it -> ((JsonString) it).getString() - ); - } - - private Map getJsonProperties(JsonObject jsonObject) { - return getPropertyMap( - jsonObject, - it -> it.getValueType() != JsonValue.ValueType.STRING, - JsonUtils::toJson - ); - } - private JsonObject removeHandledProperties(JsonObject properties, List handledProperties) { var remaining = Json.createObjectBuilder(JsonLdUtils.tryCompact(properties)); handledProperties.forEach(remaining::remove); diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java index 7ccde1954..03ede23ac 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/AssetMapperTest.java @@ -8,20 +8,19 @@ import de.sovity.edc.utils.JsonUtils; import de.sovity.edc.utils.jsonld.vocab.Prop; import lombok.SneakyThrows; +import net.javacrumbs.jsonunit.assertj.JsonAssertions; import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.Arrays; import java.util.List; -import java.util.Map; import static jakarta.json.Json.createArrayBuilder; import static jakarta.json.Json.createObjectBuilder; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -43,7 +42,7 @@ void setup() { @SneakyThrows void test_buildAssetDto() { // Arrange - String assetJsonLd = new String(Files.readAllBytes(Paths.get(getClass().getResource("/example-asset.jsonld").toURI()))); + String assetJsonLd = TestUtils.loadResourceAsString("/example-asset.jsonld"); // Act var uiAsset = assetMapper.buildUiAsset(JsonUtils.parseJsonObj(assetJsonLd), endpoint, participantId); @@ -54,8 +53,10 @@ void test_buildAssetDto() { assertThat(uiAsset.getParticipantId()).isEqualTo(participantId); assertThat(uiAsset.getTitle()).isEqualTo("My Asset"); assertThat(uiAsset.getLanguage()).isEqualTo("https://w3id.org/idsa/code/EN"); - assertThat(uiAsset.getDescription()).isEqualTo("# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"); - assertThat(uiAsset.getDescriptionShortText()).isEqualTo("Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + assertThat(uiAsset.getDescription()).isEqualTo( + "# Lorem Ipsum...\n## h2 title\n[Link text Here](example.com) 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"); + assertThat(uiAsset.getDescriptionShortText()).isEqualTo( + "Lorem Ipsum... h2 title Link text Here 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); assertThat(uiAsset.getIsOwnConnector()).isEqualTo(true); assertThat(uiAsset.getCreatorOrganizationName()).isEqualTo("My Organization Name"); assertThat(uiAsset.getPublisherHomepage()).isEqualTo("https://data-source.my-org/about"); @@ -85,14 +86,48 @@ void test_buildAssetDto() { assertThat(uiAsset.getTemporalCoverageToInclusive()).isEqualTo("2024-01-22"); assertThat(uiAsset.getAssetJsonLd()).contains("\"%s\"".formatted(Prop.Edc.ID)); - assertThat(uiAsset.getAdditionalProperties()).containsExactlyEntriesOf(Map.of( - "http://unknown/some-custom-string", "some-string-value")); - assertThat(uiAsset.getAdditionalJsonProperties()).containsExactlyEntriesOf(Map.of( - "http://unknown/some-custom-obj", "{\"http://unknown/a\":\"b\"}")); - assertThat(uiAsset.getPrivateProperties()).containsExactlyEntriesOf(Map.of( - "http://unknown/some-custom-private-string", "some-private-value")); - assertThat(uiAsset.getPrivateJsonProperties()).containsExactlyEntriesOf(Map.of( - "http://unknown/some-custom-private-obj", "{\"http://unknown/a-private\":\"b-private\"}")); + + JsonAssertions.assertThatJson(uiAsset.getCustomJsonAsString()) + .isEqualTo(""" + { + "array": [3, 1, 4, 1, 5], + "boolean": false, + "null": null, + "number": 116, + "object": { + "key": "value" + }, + "string": "value" + } + """); + JsonAssertions.assertThatJson(uiAsset.getCustomJsonLdAsString()) + .isObject() + .containsEntry("http://unknown/some-custom-string", "some-string-value") + .containsEntry("http://unknown/some-custom-obj", json(""" + { "http://unknown/a": "b" } + """)); + + JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonAsString()) + .isEqualTo(""" + { + "priv_array": [3, 1, 4, 1, 5], + "priv_boolean": false, + "priv_null": null, + "priv_number": 116, + "priv_object": { + "key": "value" + }, + "priv_string": "value" + } + """); + JsonAssertions.assertThatJson(uiAsset.getPrivateCustomJsonLdAsString()) + .isObject() + .containsEntry("http://unknown/some-custom-private-string", "some-private-value") + .containsEntry("http://unknown/some-custom-private-obj", json(""" + { + "http://unknown/a-private": "b-private" + } + """)); } @Test @@ -102,6 +137,7 @@ void test_empty() { var assetJsonLd = createObjectBuilder() .add(Prop.ID, "my-asset-1") .build(); + // Act var uiAsset = assetMapper.buildUiAsset(assetJsonLd, endpoint, participantId); diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java new file mode 100644 index 000000000..113f847f1 --- /dev/null +++ b/extensions/wrapper/wrapper-common-mappers/src/test/java/de/sovity/edc/ext/wrapper/api/common/mappers/TestUtils.java @@ -0,0 +1,15 @@ +package de.sovity.edc.ext.wrapper.api.common.mappers; + +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TestUtils { + @NotNull + @SneakyThrows + public static String loadResourceAsString(String name) { + return new String(Files.readAllBytes(Paths.get(TestUtils.class.getResource(name).toURI()))); + } +} diff --git a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld index 9bd1b1b95..a30868cbc 100644 --- a/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld +++ b/extensions/wrapper/wrapper-common-mappers/src/test/resources/example-asset.jsonld @@ -34,18 +34,29 @@ "http://w3id.org/mds#transportMode": "my-geo-reference-method", "https://semantic.sovity.io/mds-dcat-ext#sovereign": "my-sovereign", "https://semantic.sovity.io/mds-dcat-ext#geolocation": "my-geolocation", - "https://semantic.sovity.io/mds-dcat-ext#nuts-location": ["my-nuts-location1", "my-nuts-location2"], - "https://semantic.sovity.io/mds-dcat-ext#data-sample-urls": ["my-data-sample-urls1", "my-data-sample-urls2"], - "https://semantic.sovity.io/mds-dcat-ext#reference-files": ["my-reference-files1", "my-reference-files2"], + "https://semantic.sovity.io/mds-dcat-ext#nuts-location": [ + "my-nuts-location1", + "my-nuts-location2" + ], + "https://semantic.sovity.io/mds-dcat-ext#data-sample-urls": [ + "my-data-sample-urls1", + "my-data-sample-urls2" + ], + "https://semantic.sovity.io/mds-dcat-ext#reference-files": [ + "my-reference-files1", + "my-reference-files2" + ], "https://semantic.sovity.io/mds-dcat-ext#additional-description": "my-additional-description", "https://semantic.sovity.io/mds-dcat-ext#conditions-for-use": "my-conditions-for-use", "https://semantic.sovity.io/mds-dcat-ext#data-update-frequency": "my-data-update-frequency", "https://semantic.sovity.io/mds-dcat-ext#temporal-coverage-from": "2007-12-03", "https://semantic.sovity.io/mds-dcat-ext#temporal-coverage-to": "2024-01-22", + "https://semantic.sovity.io/dcat-ext#customJson": "{\"array\":[3,1,4,1,5],\"boolean\":false,\"null\":null,\"number\":116,\"object\":{\"key\":\"value\"},\"string\":\"value\"}", "http://unknown/some-custom-string": "some-string-value", "http://unknown/some-custom-obj": {"http://unknown/a": "b"} }, - "privateProperties": { + "https://w3id.org/edc/v0.0.1/ns/privateProperties": { + "https://semantic.sovity.io/dcat-ext#privateCustomJson":"{\"priv_array\":[3,1,4,1,5],\"priv_boolean\":false,\"priv_null\":null,\"priv_number\":116,\"priv_object\":{\"key\":\"value\"},\"priv_string\":\"value\"}", "http://unknown/some-custom-private-string": "some-private-value", "http://unknown/some-custom-private-obj": {"http://unknown/a-private": "b-private"} }, diff --git a/extensions/wrapper/wrapper/build.gradle.kts b/extensions/wrapper/wrapper/build.gradle.kts index 200b23aee..31f75b73c 100644 --- a/extensions/wrapper/wrapper/build.gradle.kts +++ b/extensions/wrapper/wrapper/build.gradle.kts @@ -1,11 +1,12 @@ +val assertj: String by project val edcVersion: String by project val edcGroup: String by project -val restAssured: String by project -val assertj: String by project -val mockitoVersion: String by project -val lombokVersion: String by project -val jettyVersion: String by project val jettyGroup: String by project +val jettyVersion: String by project +val jsonUnit: String by project +val lombokVersion: String by project +val mockitoVersion: String by project +val restAssured: String by project plugins { `java-library` @@ -61,6 +62,7 @@ dependencies { testImplementation("${edcGroup}:dsp-api-configuration:${edcVersion}") testImplementation("${edcGroup}:data-plane-selector-core:${edcVersion}") + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") testImplementation("io.rest-assured:rest-assured:${restAssured}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.assertj:assertj-core:${assertj}") 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 bae70eeb8..3e3206015 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 @@ -22,6 +22,7 @@ import de.sovity.edc.ext.wrapper.api.ui.model.IdResponseDto; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; import lombok.RequiredArgsConstructor; +import lombok.val; import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.asset.Asset; @@ -49,17 +50,17 @@ public List getAssets() { @NotNull public IdResponseDto createAsset(UiAssetCreateRequest request) { - var asset = assetBuilder.fromCreateRequest(request); - asset = assetService.create(asset).orElseThrow(ServiceException::new); - return new IdResponseDto(asset.getId()); + val asset = assetBuilder.fromCreateRequest(request); + val createdAsset = assetService.create(asset).orElseThrow(ServiceException::new); + return new IdResponseDto(createdAsset.getId()); } public IdResponseDto editAsset(String assetId, UiAssetEditMetadataRequest request) { - var asset = assetService.findById(assetId); - Objects.requireNonNull(asset, "Asset with ID %s not found".formatted(assetId)); - asset = assetBuilder.fromEditMetadataRequest(asset, request); - asset = assetService.update(asset).orElseThrow(ServiceException::new); - return new IdResponseDto(asset.getId()); + val foundAsset = assetService.findById(assetId); + Objects.requireNonNull(foundAsset, "Asset with ID %s not found".formatted(assetId)); + val editedAsset = assetBuilder.fromEditMetadataRequest(foundAsset, request); + val updatedAsset = assetService.update(editedAsset).orElseThrow(ServiceException::new); + return new IdResponseDto(updatedAsset.getId()); } @NotNull diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java index 4591a554a..372a052ef 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetBuilder.java @@ -20,6 +20,7 @@ import de.sovity.edc.ext.wrapper.api.common.model.UiAssetEditMetadataRequest; import de.sovity.edc.ext.wrapper.api.ui.pages.dashboard.services.SelfDescriptionService; import lombok.RequiredArgsConstructor; +import lombok.val; import org.eclipse.edc.spi.types.domain.asset.Asset; @RequiredArgsConstructor @@ -52,9 +53,14 @@ public Asset fromEditMetadataRequest(Asset asset, UiAssetEditMetadataRequest req var createRequest = buildCreateRequest(asset, request); var tmpAsset = fromCreateRequest(createRequest); - return asset.toBuilder() + // DEBT: On each EDC update, check that no field was added in + // org.eclipse.edc.spi.types.domain.asset.Asset.toBuilder + return Asset.Builder.newInstance() + .id(asset.getId()) .properties(tmpAsset.getProperties()) .privateProperties(tmpAsset.getPrivateProperties()) + .dataAddress(asset.getDataAddress()) + .createdAt(asset.getCreatedAt()) .build(); } @@ -64,35 +70,35 @@ private UiAssetCreateRequest buildCreateRequest(Asset asset, UiAssetEditMetadata var createRequest = new UiAssetCreateRequest(); createRequest.setId(asset.getId()); + createRequest.setConditionsForUse(editRequest.getConditionsForUse()); + createRequest.setCustomJsonAsString(editRequest.getCustomJsonAsString()); + createRequest.setCustomJsonLdAsString(editRequest.getCustomJsonLdAsString()); createRequest.setDataAddressProperties(dataAddress); - createRequest.setAdditionalJsonProperties(editRequest.getAdditionalJsonProperties()); - createRequest.setAdditionalProperties(editRequest.getAdditionalProperties()); createRequest.setDataCategory(editRequest.getDataCategory()); createRequest.setDataModel(editRequest.getDataModel()); + createRequest.setDataSampleUrls(editRequest.getDataSampleUrls()); createRequest.setDataSubcategory(editRequest.getDataSubcategory()); + createRequest.setDataUpdateFrequency(editRequest.getDataUpdateFrequency()); createRequest.setDescription(editRequest.getDescription()); + createRequest.setGeoLocation(editRequest.getGeoLocation()); createRequest.setGeoReferenceMethod(editRequest.getGeoReferenceMethod()); createRequest.setKeywords(editRequest.getKeywords()); createRequest.setLandingPageUrl(editRequest.getLandingPageUrl()); createRequest.setLanguage(editRequest.getLanguage()); createRequest.setLicenseUrl(editRequest.getLicenseUrl()); createRequest.setMediaType(editRequest.getMediaType()); - createRequest.setPrivateJsonProperties(editRequest.getPrivateJsonProperties()); - createRequest.setPrivateProperties(editRequest.getPrivateProperties()); - createRequest.setPublisherHomepage(editRequest.getPublisherHomepage()); - createRequest.setTitle(editRequest.getTitle()); - createRequest.setTransportMode(editRequest.getTransportMode()); - createRequest.setVersion(editRequest.getVersion()); - createRequest.setSovereignLegalName(editRequest.getSovereignLegalName()); - createRequest.setGeoLocation(editRequest.getGeoLocation()); createRequest.setNutsLocation(editRequest.getNutsLocation()); - createRequest.setDataSampleUrls(editRequest.getDataSampleUrls()); + createRequest.setPrivateCustomJsonAsString(editRequest.getPrivateCustomJsonAsString()); + createRequest.setPrivateCustomJsonLdAsString(editRequest.getPrivateCustomJsonLdAsString()); + createRequest.setPublisherHomepage(editRequest.getPublisherHomepage()); createRequest.setReferenceFileUrls(editRequest.getReferenceFileUrls()); createRequest.setReferenceFilesDescription(editRequest.getReferenceFilesDescription()); - createRequest.setConditionsForUse(editRequest.getConditionsForUse()); - createRequest.setDataUpdateFrequency(editRequest.getDataUpdateFrequency()); + createRequest.setSovereignLegalName(editRequest.getSovereignLegalName()); createRequest.setTemporalCoverageFrom(editRequest.getTemporalCoverageFrom()); createRequest.setTemporalCoverageToInclusive(editRequest.getTemporalCoverageToInclusive()); + createRequest.setTitle(editRequest.getTitle()); + createRequest.setTransportMode(editRequest.getTransportMode()); + createRequest.setVersion(editRequest.getVersion()); return createRequest; } diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java index b54ed364c..de6a5d54e 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/asset/AssetApiServiceTest.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @ApiTest @@ -132,6 +133,28 @@ void testAssetCreation(AssetService assetService) { .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") .dataAddressProperties(dataAddressProperties) + .customJsonAsString("{\"test\":\"value\"}") + .customJsonLdAsString(""" + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1,2,3], + "https://object": { "https://key": "value" }, + "https://booleans/are/not/supported/by/Eclipse/EDC": true, + "https://null/will/be/eliminated": null + } + """) + .privateCustomJsonAsString("{\"private test\":\"private value\"}") + .privateCustomJsonLdAsString(""" + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1,2,3], + "https://private/object": { "https://key": "value" }, + "https://private/booleans/are/not/supported/by/Eclipse/EDC": true, + "https://private/null/will/be/eliminated": null + } + """) .build(); // act @@ -172,6 +195,28 @@ void testAssetCreation(AssetService assetService) { assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); + assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" + { "test": "value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { + "https://string": "value", + "https://number": 3.14, + "https://array": [1.0, 2.0, 3.0], + "https://object": { "https://key": "value" } + } + """); + assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" + { "private test": "private value" } + """); + assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { + "https://private/string": "value", + "https://private/number": 3.14, + "https://private/array": [1.0, 2.0, 3.0], + "https://private/object": { "https://key": "value" } + } + """); var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddressProperties); @@ -215,8 +260,19 @@ void testEditAssetMetadata(AssetService assetService) { .keywords(List.of("keyword1", "keyword2")) .publisherHomepage("publisherHomepage") .dataAddressProperties(dataAddress) + .customJsonAsString(""" + { "test": "value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "value1", + "https://for-deletion": "value2" + } + """) .build(); + client.uiApi().createAsset(createRequest); + var editRequest = UiAssetEditMetadataRequest.builder() .title("AssetTitle 2") .description("AssetDescription 2") @@ -241,6 +297,15 @@ void testEditAssetMetadata(AssetService assetService) { .transportMode("transportMode2") .keywords(List.of("keyword3")) .publisherHomepage("publisherHomepage2") + .customJsonAsString(""" + { "edited": "new value" } + """) + .customJsonLdAsString(""" + { + "https://to-change": "new value LD", + "https://for-deletion": null + } + """) .build(); // act @@ -281,6 +346,12 @@ void testEditAssetMetadata(AssetService assetService) { assertThat(asset.getHttpDatasourceHintsProxyPath()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyQueryParams()).isTrue(); assertThat(asset.getHttpDatasourceHintsProxyBody()).isTrue(); + assertThat(asset.getCustomJsonAsString()).isEqualTo(""" + { "edited": "new value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { "https://to-change": "new value LD" } + """); var assetWithDataAddress = assetService.query(QuerySpec.max()).orElseThrow(FailedMappingException::ofFailure).toList().get(0); assertThat(assetWithDataAddress.getDataAddress().getProperties()).isEqualTo(dataAddress); @@ -376,4 +447,3 @@ private static long dateFormatterToLong(String date) { return formatter.parse(date).getTime(); } } - diff --git a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java index 4a28b4b12..b8defda15 100644 --- a/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java +++ b/extensions/wrapper/wrapper/src/test/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreement/ContractAgreementPageTest.java @@ -82,7 +82,7 @@ void testContractAgreementPage( // arrange assetIndex.create(asset(ASSET_ID)).orElseThrow(storeFailure -> new RuntimeException("Failed to create asset")); contractNegotiationStore.save(contractDefinition(CONTRACT_DEFINITION_ID)); - transferProcessStore.updateOrCreate(transferProcess(1, 1, TransferProcessStates.COMPLETED.code())); + transferProcessStore.save(transferProcess(1, 1, TransferProcessStates.COMPLETED.code())); // act var actual = client.uiApi().getContractAgreementPage().getContractAgreements(); diff --git a/gradle.properties b/gradle.properties index 85ff7ebf8..2888950c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ edcVersion=0.2.1 tractusGroup=org.eclipse.tractusx.edc tractusVersion=0.5.3 assertj=3.23.1 +jsonUnit=3.2.7 jupiterVersion=5.8.2 mockitoVersion=4.8.0 okHttpVersion=4.10.0 diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 5e01c61dc..3ff952878 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -3,18 +3,23 @@ plugins { id("org.gradle.test-retry") version "1.5.7" } +val assertj: String by project val edcVersion: String by project val edcGroup: String by project +val jsonUnit: String by project +val lombokVersion: String by project val mockitoVersion: String by project -val assertj: String by project dependencies { api(project(":launchers:common:base")) api(project(":launchers:common:auth-mock")) + testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}") + testCompileOnly("org.projectlombok:lombok:${lombokVersion}") testImplementation(project(":extensions:test-backend-controller")) testImplementation(project(":utils:test-connector-remote")) testImplementation(project(":extensions:wrapper:clients:java-client")) + testImplementation("net.javacrumbs.json-unit:json-unit-assertj:${jsonUnit}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.assertj:assertj-core:${assertj}") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") diff --git a/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java b/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java index 761782160..9efd13e81 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/Ms8ConnectorMigrationTest.java @@ -32,7 +32,6 @@ import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.List; -import java.util.Map; import java.util.function.Predicate; import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; @@ -124,15 +123,6 @@ void testMs8DataOffer_Properties() { softly.assertThat(asset.getPublisherHomepage()).isEqualTo("https://publisher"); softly.assertThat(asset.getTransportMode()).isEqualTo("Rail"); softly.assertThat(asset.getVersion()).isEqualTo("1.0"); - softly.assertThat(asset.getAdditionalProperties()).isEqualTo(Map.of( - "http://unknown/usecase", "my-use-case", - "http://unknown/custom-prop-1", "1", - "http://custom-prop-2", "2", - "https://custom-prop-3", "3" - )); - softly.assertThat(asset.getAdditionalJsonProperties()).isNullOrEmpty(); - softly.assertThat(asset.getPrivateJsonProperties()).isNullOrEmpty(); - softly.assertThat(asset.getPrivateProperties()).isNullOrEmpty(); }); } 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 f618bdce6..d5e9b5e3f 100644 --- a/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java +++ b/tests/src/test/java/de/sovity/edc/e2e/UiApiWrapperTest.java @@ -43,6 +43,7 @@ import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; import jakarta.json.JsonObject; +import lombok.val; import org.awaitility.Awaitility; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.protocol.dsp.spi.types.HttpMessageProtocol; @@ -62,6 +63,7 @@ import static de.sovity.edc.extension.e2e.connector.DataTransferTestUtil.validateDataTransferred; import static de.sovity.edc.extension.e2e.connector.config.ConnectorConfigFactory.forTestDatabase; import static de.sovity.edc.extension.e2e.connector.config.ConnectorRemoteConfigFactory.fromConnectorConfig; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -163,10 +165,18 @@ void provide_consume_assetMapping_policyMapping_agreements() { Prop.Edc.METHOD, "GET", Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) )) - .additionalProperties(Map.of("http://unknown/a", "x")) - .additionalJsonProperties(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")) - .privateProperties(Map.of("http://unknown/a-private", "x-private")) - .privateJsonProperties(Map.of("http://unknown/b-private", "{\"http://unknown/c-private\":\"y-private\"}")) + .customJsonAsString(""" + {"test": "value"} + """) + .customJsonLdAsString(""" + {"https://public/some#key": "public LD value"} + """) + .privateCustomJsonAsString(""" + {"private_test": "private value"} + """) + .privateCustomJsonLdAsString(""" + {"https://private/some#key": "private LD value"} + """) .build()).getId(); assertThat(assetId).isEqualTo("asset-1"); @@ -235,26 +245,33 @@ void provide_consume_assetMapping_policyMapping_agreements() { assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyPath()).isFalse(); assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyQueryParams()).isFalse(); assertThat(dataOffer.getAsset().getHttpDatasourceHintsProxyBody()).isFalse(); - assertThat(dataOffer.getAsset().getAdditionalProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/a", "x")); - assertThat(dataOffer.getAsset().getAdditionalJsonProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")); - assertThat(dataOffer.getAsset().getPrivateProperties()).isNullOrEmpty(); - assertThat(dataOffer.getAsset().getPrivateJsonProperties()).isNullOrEmpty(); + assertThatJson(dataOffer.getAsset().getCustomJsonAsString()).isEqualTo(""" + {"test": "value"} + """); + assertThatJson(dataOffer.getAsset().getCustomJsonLdAsString()).isEqualTo(""" + {"https://public/some#key":"public LD value"} + """); + assertThat(dataOffer.getAsset().getPrivateCustomJsonAsString()).isNullOrEmpty(); + assertThatJson(dataOffer.getAsset().getPrivateCustomJsonLdAsString()).isObject().isEmpty(); // while the data offer on the consumer side won't contain private properties, the asset page on the provider side should assertThat(asset.getAssetId()).isEqualTo(assetId); assertThat(asset.getTitle()).isEqualTo("AssetName"); assertThat(asset.getConnectorEndpoint()).isEqualTo(getProtocolEndpoint(providerConnector)); assertThat(asset.getParticipantId()).isEqualTo(providerConnector.getParticipantId()); - assertThat(asset.getAdditionalProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/a", "x")); - assertThat(asset.getAdditionalJsonProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/b", "{\"http://unknown/c\":\"y\"}")); - assertThat(asset.getPrivateProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/a-private", "x-private")); - assertThat(asset.getPrivateJsonProperties()) - .containsExactlyEntriesOf(Map.of("http://unknown/b-private", "{\"http://unknown/c-private\":\"y-private\"}")); + + assertThatJson(asset.getCustomJsonAsString()).isEqualTo(""" + { "test": "value" } + """); + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { "https://public/some#key": "public LD value" } + """); + assertThatJson(asset.getPrivateCustomJsonAsString()).isEqualTo(""" + { "private_test": "private value" } + """); + assertThatJson(asset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { "https://private/some#key": "private LD value" } + """); // Contract Agreement assertThat(providerAgreements).hasSize(1); @@ -306,6 +323,50 @@ void provide_consume_assetMapping_policyMapping_agreements() { validateTransferProcessesOk(); } + @Test + void canOverrideTheWellKnowPropertiesUsingTheCustomProperties() { + // arrange + var assetId = providerClient.uiApi().createAsset(UiAssetCreateRequest.builder() + .id("asset-1") + .title("will be overridden") + .dataAddressProperties(Map.of( + Prop.Edc.TYPE, "HttpData", + Prop.Edc.METHOD, "GET", + Prop.Edc.BASE_URL, "http://example.com/base" + )) + .customJsonLdAsString(""" + { + "http://purl.org/dc/terms/title": "The real title", + "https://semantic.sovity.io/mds-dcat-ext#nuts-location": ["a", "b", "c"], + "http://example.com/an-actual-custom-property": "custom value" + } + """) + .build()).getId(); + assertThat(assetId).isEqualTo("asset-1"); + + // act + val assets = providerClient.uiApi().getAssetPage().getAssets(); + assertThat(assets).hasSize(1); + val asset = assets.get(0); + + // assert + + // while the data offer on the consumer side won't contain private properties, the asset page on the provider side should + assertThat(asset.getAssetId()).isEqualTo(assetId); + // overridden property + assertThat(asset.getTitle()).isEqualTo("The real title"); + // added property + assertThat(asset.getNutsLocation()).isEqualTo(List.of("a", "b", "c")); + // remaining custom property + assertThatJson(asset.getCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/an-actual-custom-property": "custom value" + } + """); + } + + // TODO throw an error if the id is overridden + @Test void customTransferRequest() { // arrange @@ -377,6 +438,30 @@ void editAssetMetadataOnLiveContract() { Prop.Edc.METHOD, "GET", Prop.Edc.BASE_URL, dataAddress.getDataSourceUrl(data) )) + .customJsonAsString(""" + { + "test": "value" + } + """) + .customJsonLdAsString(""" + { + "test": "not a valid key, will be deleted", + "http://example.com/key-to-delete": "with a valida key", + "http://example.com/key-to-edit": "with a valida key" + } + """) + .privateCustomJsonAsString(""" + { + "private-test": "value" + } + """) + .privateCustomJsonLdAsString(""" + { + "private-test": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": "private with a valid key", + "http://example.com/private-key-to-edit": "private with a valid key" + } + """) .build()).getId(); providerClient.uiApi().createContractDefinition(ContractDefinitionRequest.builder() @@ -403,12 +488,61 @@ void editAssetMetadataOnLiveContract() { // act providerClient.uiApi().editAssetMetadata(assetId, UiAssetEditMetadataRequest.builder() .title("Good Asset Title") + .customJsonAsString(""" + { + "edited": "new value" + } + """) + .customJsonLdAsString(""" + { + "edited": "not a valid key, will be deleted", + "http://example.com/key-to-delete": null, + "http://example.com/key-to-edit": "with a valid key", + "http://example.com/extra": "value to add" + } + """) + .privateCustomJsonAsString(""" + { + "private-edited": "new value" + } + """) + .privateCustomJsonLdAsString(""" + { + "private-edited": "not a valid key, will be deleted", + "http://example.com/private-key-to-delete": null, + "http://example.com/private-key-to-edit": "private with a valid key", + "http://example.com/private-extra": "private value to add" + } + """) .build()); initiateTransfer(negotiation); // assert assertThat(consumerClient.uiApi().getCatalogPageDataOffers(getProtocolEndpoint(providerConnector)).get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); - assertThat(providerClient.uiApi().getContractAgreementPage().getContractAgreements().get(0).getAsset().getTitle()).isEqualTo("Good Asset Title"); + val firstAsset = providerClient.uiApi().getContractAgreementPage().getContractAgreements().get(0).getAsset(); + assertThat(firstAsset.getTitle()).isEqualTo("Good Asset Title"); + assertThat(firstAsset.getCustomJsonAsString()).isEqualTo(""" + { + "edited": "new value" + } + """); + assertThatJson(firstAsset.getCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/key-to-edit": "with a valid key", + "http://example.com/extra": "value to add" + } + """); + assertThat(firstAsset.getPrivateCustomJsonAsString()).isEqualTo(""" + { + "private-edited": "new value" + } + """); + assertThatJson(firstAsset.getPrivateCustomJsonLdAsString()).isEqualTo(""" + { + "http://example.com/private-key-to-edit": "private with a valid key", + "http://example.com/private-extra": "private value to add" + } + """); validateDataTransferred(dataAddress.getDataSinkSpyUrl(), data); validateTransferProcessesOk(); assertThat(providerClient.uiApi().getTransferHistoryPage().getTransferEntries().get(0).getAssetName()).isEqualTo("Good Asset Title"); diff --git a/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql b/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql index a83a92507..0d40dfbbd 100644 --- a/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql +++ b/tests/src/test/resources/db/additional-test-data/provider/V1_9__ms8-test-contract-provider.sql @@ -41,9 +41,6 @@ INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'h INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:originator', 'http://localhost:21003/api/v1/ids/data', 'java.lang.String'); INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:standardLicense', 'https://standard-license', 'java.lang.String'); INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'asset:prop:usecase', 'my-use-case', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'custom-prop-1', '1', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'http://custom-prop-2', '2', 'java.lang.String'); -INSERT INTO public.edc_asset_property VALUES ('urn:artifact:first-asset:1.0', 'https://custom-prop-3', '3', 'java.lang.String'); INSERT INTO public.edc_asset_property VALUES ('urn:artifact:second-asset', 'asset:prop:id', 'urn:artifact:second-asset', 'java.lang.String'); diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java index e7cbc18a3..53ad296ff 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspContractOfferUtils.java @@ -29,7 +29,7 @@ public class DspContractOfferUtils { * @return A base64 string that can be used as an id for the {@code contract} */ public static String buildStableId(JsonObject contract) { - // FIXME: This doesn't enforce any property order and may cause trouble if the returned policy schema is not consistent. + // NOTE: This doesn't enforce any property order and may cause trouble if the returned policy schema is not consistent. // Use canonical form if needed later. val noId = Json.createObjectBuilder(contract).remove(Prop.ID).build(); val policyId = hash(noId); diff --git a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java index 23ec8b90b..b3364a1ec 100644 --- a/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java +++ b/utils/json-and-jsonld-utils/src/main/java/de/sovity/edc/utils/jsonld/vocab/Prop.java @@ -106,6 +106,8 @@ public class Dcterms { @UtilityClass public class SovityDcatExt { public final String CTX = "https://semantic.sovity.io/dcat-ext#"; + public final String CUSTOM_JSON = CTX + "customJson"; + public final String PRIVATE_CUSTOM_JSON = CTX + "privateCustomJson"; @UtilityClass public class HttpDatasourceHints {