diff --git a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApi.java b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApi.java index 11d6a7e..ab9471d 100644 --- a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApi.java +++ b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApi.java @@ -13,6 +13,9 @@ import jakarta.json.JsonObject; import org.eclipse.edc.api.model.ApiCoreSchema; import org.upm.inesdata.complexpolicy.model.PolicyDefinitionCreateDto; +import org.upm.inesdata.complexpolicy.model.PolicyDefinitionDto; + +import java.util.List; @OpenAPIDefinition( info = @Info( @@ -63,4 +66,34 @@ public interface ComplexPolicyDefinitionApi { ) JsonObject createPolicyDefinitionV3(PolicyDefinitionCreateDto var1); + @Operation( + description = "Creates a new policy definition", + requestBody = @RequestBody( + content = {@Content( + schema = @Schema( + ) + )} + ), + responses = {@ApiResponse( + responseCode = "200", + description = "Returns the Policy Definitions", + content = {@Content( + schema = @Schema( + implementation = ApiCoreSchema.IdResponseSchema.class + ) + )} + ), @ApiResponse( + responseCode = "400", + description = "Request body was malformed", + content = {@Content( + array = @ArraySchema( + schema = @Schema( + implementation = ApiCoreSchema.ApiErrorDetailSchema.class + ) + ) + )} + )} + ) + List getPolicyDefinitions(JsonObject querySpecJson); + } diff --git a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApiController.java b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApiController.java index e32fb04..2dcd212 100644 --- a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApiController.java +++ b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/controller/ComplexPolicyDefinitionApiController.java @@ -10,14 +10,21 @@ import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; import org.upm.inesdata.complexpolicy.mapper.PolicyMapper; import org.upm.inesdata.complexpolicy.model.PolicyDefinitionCreateDto; +import org.upm.inesdata.complexpolicy.model.PolicyDefinitionDto; import org.upm.inesdata.complexpolicy.model.UiPolicyExpression; +import java.util.Comparator; +import java.util.List; + import static java.lang.String.format; +import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_TYPE; import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @Consumes({"application/json"}) @@ -59,6 +66,7 @@ public JsonObject createPolicyDefinitionV3(PolicyDefinitionCreateDto request) { .orElseThrow(f -> new EdcException("Error creating response body: " + f.getFailureDetail())); } + public PolicyDefinition buildPolicyDefinition(String id, UiPolicyExpression uiPolicyExpression) { var policy = policyMapper.buildPolicy(uiPolicyExpression); return PolicyDefinition.Builder.newInstance() @@ -66,4 +74,30 @@ public PolicyDefinition buildPolicyDefinition(String id, UiPolicyExpression uiPo .policy(policy) .build(); } + + + + @POST + @Path("request") + public List getPolicyDefinitions(JsonObject querySpecJson) { + QuerySpec querySpec; + if (querySpecJson == null) { + querySpec = QuerySpec.Builder.newInstance().build(); + } else { + validator.validate(EDC_QUERY_SPEC_TYPE, querySpecJson).orElseThrow(ValidationFailureException::new); + + querySpec = transformerRegistry.transform(querySpecJson, QuerySpec.class) + .orElseThrow(InvalidRequestException::new); + } + + var policyDefinitions = getAllPolicyDefinitions(querySpec); + return policyDefinitions.stream() + .sorted(Comparator.comparing(PolicyDefinition::getCreatedAt).reversed()) + .map(policyMapper::buildPolicyDefinitionDto) + .toList(); + } + + private List getAllPolicyDefinitions(QuerySpec querySpec) { + return service.search(querySpec).orElseThrow(f -> new EdcException("Error getting policy definitions: " + f.getFailureDetail())); + } } diff --git a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/mapper/PolicyMapper.java b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/mapper/PolicyMapper.java index 88352b5..b444e94 100644 --- a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/mapper/PolicyMapper.java +++ b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/mapper/PolicyMapper.java @@ -3,12 +3,14 @@ import jakarta.json.JsonObject; import lombok.RequiredArgsConstructor; +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.upm.inesdata.complexpolicy.exception.FailedMappingException; +import org.upm.inesdata.complexpolicy.model.PolicyDefinitionDto; import org.upm.inesdata.complexpolicy.model.UiPolicy; import org.upm.inesdata.complexpolicy.model.UiPolicyExpression; import org.upm.inesdata.complexpolicy.utils.JsonUtils; @@ -101,4 +103,20 @@ public JsonObject buildPolicyJsonLd(Policy policy) { return typeTransformerRegistry.transform(policy, JsonObject.class) .orElseThrow(FailedMappingException::ofFailure); } + + /** + * Builds a simplified policy definition DTO from a policy definition + *

+ * This operation is lossy. + * + * @param policyDefinition policy definition + * @return ui policy + */ + public PolicyDefinitionDto buildPolicyDefinitionDto(PolicyDefinition policyDefinition) { + var policy = buildUiPolicy(policyDefinition.getPolicy()); + return PolicyDefinitionDto.builder() + .policyDefinitionId(policyDefinition.getId()) + .policy(policy) + .build(); + } } diff --git a/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/model/PolicyDefinitionDto.java b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/model/PolicyDefinitionDto.java new file mode 100644 index 0000000..43af5ef --- /dev/null +++ b/extensions/complex-policy-api/src/main/java/org/upm/inesdata/complexpolicy/model/PolicyDefinitionDto.java @@ -0,0 +1,22 @@ +package org.upm.inesdata.complexpolicy.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Policy Definition as required for the Policy Definition Page") +public class PolicyDefinitionDto { + @Schema(description = "Policy Definition ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String policyDefinitionId; + + @Schema(description = "Policy Contents", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicy policy; +} diff --git a/extensions/federated-catalog-cache-api/build.gradle.kts b/extensions/federated-catalog-cache-api/build.gradle.kts index 9952cdc..c597551 100644 --- a/extensions/federated-catalog-cache-api/build.gradle.kts +++ b/extensions/federated-catalog-cache-api/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":spi:federated-catalog-cache-spi")) + api(project(":extensions:complex-policy-api")) api(libs.edc.spi.core) implementation(libs.edc.spi.transform) implementation(libs.edc.web.spi) @@ -26,5 +27,10 @@ dependencies { implementation(libs.edc.federated.catalog.api) implementation(libs.edc.federated.catalog.core) implementation(libs.edc.federated.catalog.spi) + annotationProcessor(libs.lombok) + compileOnly(libs.lombok) + testAnnotationProcessor(libs.lombok) + testCompileOnly(libs.lombok) + api(libs.edc.policy.definition.api) } diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/FederatedCatalogCacheApiExtension.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/FederatedCatalogCacheApiExtension.java index 620fd80..f3ad20d 100644 --- a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/FederatedCatalogCacheApiExtension.java +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/FederatedCatalogCacheApiExtension.java @@ -1,9 +1,12 @@ package org.upm.inesdata.federated; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; -import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfigurationExtension; import org.eclipse.edc.connector.controlplane.transform.edc.from.JsonObjectFromAssetTransformer; import org.eclipse.edc.connector.controlplane.transform.edc.to.JsonObjectToAssetTransformer; +import org.eclipse.edc.connector.controlplane.transform.odrl.OdrlTransformersFactory; +import org.eclipse.edc.connector.controlplane.transform.odrl.to.JsonObjectToPolicyTransformer; +import org.eclipse.edc.connector.core.agent.NoOpParticipantIdMapper; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -17,8 +20,16 @@ import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.spi.WebService; import org.eclipse.edc.web.spi.configuration.ApiContext; +import org.upm.inesdata.complexpolicy.mapper.AtomicConstraintMapper; +import org.upm.inesdata.complexpolicy.mapper.ExpressionExtractor; +import org.upm.inesdata.complexpolicy.mapper.ExpressionMapper; +import org.upm.inesdata.complexpolicy.mapper.LiteralMapper; +import org.upm.inesdata.complexpolicy.mapper.OperatorMapper; +import org.upm.inesdata.complexpolicy.mapper.PolicyMapper; +import org.upm.inesdata.complexpolicy.mapper.PolicyValidator; import org.upm.inesdata.federated.controller.FederatedCatalogCacheApiController; import org.upm.inesdata.federated.service.FederatedCatalogCacheServiceImpl; +import org.upm.inesdata.federated.transformer.JsonObjectFromUiContractOfferTransformer; import org.upm.inesdata.spi.federated.FederatedCatalogCacheService; import org.upm.inesdata.spi.federated.index.PaginatedFederatedCacheStoreIndex; @@ -81,8 +92,18 @@ public void initialize(ServiceExtensionContext context) { managementApiTransformerRegistry.register(new JsonObjectFromAssetTransformer(factory, jsonLdMapper)); managementApiTransformerRegistry.register(new JsonObjectToAssetTransformer()); + var participantIdMapper = new NoOpParticipantIdMapper(); + managementApiTransformerRegistry.register(new JsonObjectToPolicyTransformer(participantIdMapper)); + managementApiTransformerRegistry.register(new JsonObjectFromUiContractOfferTransformer(jsonLdMapper, factory)); + OdrlTransformersFactory.jsonObjectToOdrlTransformers(participantIdMapper).forEach(managementApiTransformerRegistry::register); + + ExpressionMapper expressionMapper = new ExpressionMapper( + new AtomicConstraintMapper(new LiteralMapper(new ObjectMapper()), new OperatorMapper())); + ExpressionExtractor expressionExtractor = new ExpressionExtractor(new PolicyValidator(), expressionMapper); + PolicyMapper policyMapper = new PolicyMapper(expressionExtractor, expressionMapper, managementApiTransformerRegistry); + var federatedCatalogCacheApiController = new FederatedCatalogCacheApiController(this.federatedCatalogCacheService(), managementApiTransformerRegistry, - validator,monitor); + validator, monitor, policyMapper); webService.registerResource(ApiContext.MANAGEMENT, federatedCatalogCacheApiController); } } diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/controller/FederatedCatalogCacheApiController.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/controller/FederatedCatalogCacheApiController.java index 23b1d85..08f0045 100644 --- a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/controller/FederatedCatalogCacheApiController.java +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/controller/FederatedCatalogCacheApiController.java @@ -1,29 +1,31 @@ package org.upm.inesdata.federated.controller; +import jakarta.json.Json; import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; -import jakarta.servlet.annotation.MultipartConfig; -import jakarta.ws.rs.BadRequestException; +import jakarta.json.JsonValue; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; -import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.query.QuerySpec; -import org.eclipse.edc.spi.result.AbstractResult; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import org.jetbrains.annotations.NotNull; +import org.upm.inesdata.complexpolicy.mapper.PolicyMapper; +import org.upm.inesdata.complexpolicy.model.UiPolicy; +import org.upm.inesdata.federated.model.DspContractOffer; +import org.upm.inesdata.federated.model.UiContractOffer; +import org.upm.inesdata.federated.utils.JsonLdUtils; +import org.upm.inesdata.federated.utils.Prop; import org.upm.inesdata.spi.federated.FederatedCatalogCacheService; -import java.util.Objects; - import static jakarta.json.stream.JsonCollectors.toJsonArray; import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_TYPE; import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @@ -32,52 +34,145 @@ * Controller class for the Federated Catalog Cache API. This class implements the {@link FederatedCatalogCacheApi} * interface and provides the API endpoints to interact with the federated catalog cache. */ -@Consumes({ MediaType.APPLICATION_JSON }) -@Produces({ MediaType.APPLICATION_JSON }) +@Consumes({MediaType.APPLICATION_JSON}) +@Produces({MediaType.APPLICATION_JSON}) @Path("/federatedcatalog") public class FederatedCatalogCacheApiController implements FederatedCatalogCacheApi { - private final TypeTransformerRegistry transformerRegistry; - private final FederatedCatalogCacheService service; - private final JsonObjectValidatorRegistry validator; - private final Monitor monitor; - - /** - * Constructs a FederatedCatalogCacheApiController with the specified dependencies. - * - * @param service the service used to access the federated catalog cache. - * @param transformerRegistry the registry for type transformers. - * @param validator the registry for JSON object validators. - * @param monitor the monitor used for logging and monitoring. - */ - public FederatedCatalogCacheApiController(FederatedCatalogCacheService service, - TypeTransformerRegistry transformerRegistry, JsonObjectValidatorRegistry validator, Monitor monitor) { - this.transformerRegistry = transformerRegistry; - this.service = service; - this.validator = validator; - this.monitor = monitor; - } - - /** - * (non-javadoc) - * - * @see FederatedCatalogCacheApi#getFederatedCatalog(JsonObject) - */ - @Override - @POST - @Path("/request") - public JsonArray getFederatedCatalog(JsonObject querySpecJson) { - QuerySpec querySpec; - if (querySpecJson == null) { - querySpec = QuerySpec.Builder.newInstance().build(); - } else { - validator.validate(EDC_QUERY_SPEC_TYPE, querySpecJson).orElseThrow(ValidationFailureException::new); - - querySpec = transformerRegistry.transform(querySpecJson, QuerySpec.class) - .orElseThrow(InvalidRequestException::new); + private final TypeTransformerRegistry transformerRegistry; + private final FederatedCatalogCacheService service; + private final JsonObjectValidatorRegistry validator; + private final Monitor monitor; + private final PolicyMapper policyMapper; + + /** + * Constructs a FederatedCatalogCacheApiController with the specified dependencies. + * + * @param service the service used to access the federated catalog cache. + * @param transformerRegistry the registry for type transformers. + * @param validator the registry for JSON object validators. + * @param monitor the monitor used for logging and monitoring. + * @param policyMapper the mapper used for policies + */ + public FederatedCatalogCacheApiController(FederatedCatalogCacheService service, + TypeTransformerRegistry transformerRegistry, JsonObjectValidatorRegistry validator, Monitor monitor, PolicyMapper policyMapper) { + this.transformerRegistry = transformerRegistry; + this.service = service; + this.validator = validator; + this.monitor = monitor; + this.policyMapper = policyMapper; } - return service.searchPagination(querySpec).orElseThrow(exceptionMapper(QuerySpec.class, null)).stream() + + /** + * (non-javadoc) + * + * @see FederatedCatalogCacheApi#getFederatedCatalog(JsonObject) + */ + @Override + @POST + @Path("/request") + public JsonArray getFederatedCatalog(JsonObject querySpecJson) { + QuerySpec querySpec; + if (querySpecJson == null) { + querySpec = QuerySpec.Builder.newInstance().build(); + } else { + validator.validate(EDC_QUERY_SPEC_TYPE, querySpecJson).orElseThrow(ValidationFailureException::new); + + querySpec = transformerRegistry.transform(querySpecJson, QuerySpec.class) + .orElseThrow(InvalidRequestException::new); + } + + return service.searchPagination(querySpec).orElseThrow(exceptionMapper(QuerySpec.class, null)).stream() .map(it -> transformerRegistry.transform(it, JsonObject.class)) .peek(r -> r.onFailure(f -> monitor.warning(f.getFailureDetail()))).filter(Result::succeeded) - .map(Result::getContent).collect(toJsonArray()); - } + .map(Result::getContent) + .map(this::modifyPolicyResponse) + .collect(toJsonArray()); + } + + private JsonObject modifyPolicyResponse(JsonObject originalResponse) { + if (originalResponse.get(Prop.Dcat.DATASET) != null) { + JsonArray datasets = originalResponse.getJsonArray(Prop.Dcat.DATASET); + JsonArrayBuilder datasetArrayBuilder = Json.createArrayBuilder(); + + for (JsonValue datasetElement : datasets) { + JsonObject dataset = datasetElement.asJsonObject(); + + if (dataset.getJsonArray(Prop.Odrl.HAS_POLICY) != null) { + JsonArray existingPolicies = dataset.getJsonArray(Prop.Odrl.HAS_POLICY); + + var contractOffers = JsonLdUtils.listOfObjects(dataset, Prop.Odrl.HAS_POLICY).stream() + .map(this::buildContractOffer) + .map(this::buildUiContractOffer) + .toList(); + + JsonArrayBuilder uiContractOffersJsonBuilder = Json.createArrayBuilder(); + contractOffers.forEach(offer -> uiContractOffersJsonBuilder.add( + transformerRegistry.transform(offer, JsonObject.class).getContent())); + JsonArray uiContractOffersJson = uiContractOffersJsonBuilder.build(); + + JsonArrayBuilder newHasPolicyArrayBuilder = Json.createArrayBuilder(); + + for (int i = 0; i < uiContractOffersJson.size(); i++) { + JsonObject complexPolicyElement = uiContractOffersJson.getJsonObject(i); + JsonValue offerElement = i < existingPolicies.size() ? existingPolicies.get(i) : null; + + JsonObject newHasPolicyElement = Json.createObjectBuilder() + .add(Prop.Edc.CTX + "complexPolicy", complexPolicyElement) + .add(Prop.Edc.CTX + "offer", offerElement) + .build(); + + newHasPolicyArrayBuilder.add(newHasPolicyElement); + } + + JsonArray newHasPolicyArray = newHasPolicyArrayBuilder.build(); + + JsonObject modifiedDatasetJson = Json.createObjectBuilder(dataset) + .remove(Prop.Odrl.HAS_POLICY) + .add(Prop.Odrl.HAS_POLICY, newHasPolicyArray) + .build(); + + datasetArrayBuilder.add(modifiedDatasetJson); + } else { + datasetArrayBuilder.add(dataset); + } + } + + JsonArray datasetArrayModified = datasetArrayBuilder.build(); + + return Json.createObjectBuilder(originalResponse) + .remove(Prop.Dcat.DATASET) + .add(Prop.Dcat.DATASET, datasetArrayModified) + .build(); + } else { + return originalResponse; + } + } + + @NotNull + private DspContractOffer buildContractOffer(JsonObject json) { + return new DspContractOffer(JsonLdUtils.id(json), json); + } + + private UiPolicy buildUiPolicy(DspContractOffer contractOffer) { + JsonArrayBuilder typeArrayBuilder = Json.createArrayBuilder(); + typeArrayBuilder.add(Prop.Odrl.CTX + "Set"); + + JsonArray typeArray = typeArrayBuilder.build(); + + JsonObject modifiedJson = Json.createObjectBuilder(contractOffer.getPolicyJsonLd()) + .remove(Prop.TYPE) + .add(Prop.TYPE, typeArray) + .build(); + + var policy = policyMapper.buildPolicy(modifiedJson); + return policyMapper.buildUiPolicy(policy); + } + + private UiContractOffer buildUiContractOffer(DspContractOffer contractOffer) { + var uiContractOffer = new UiContractOffer(); + uiContractOffer.setContractOfferId(contractOffer.getContractOfferId()); + uiContractOffer.setPolicy(buildUiPolicy(contractOffer)); + return uiContractOffer; + } + } diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/DspContractOffer.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/DspContractOffer.java new file mode 100644 index 0000000..5a78ae2 --- /dev/null +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/DspContractOffer.java @@ -0,0 +1,10 @@ +package org.upm.inesdata.federated.model; + +import jakarta.json.JsonObject; +import lombok.Data; + +@Data +public class DspContractOffer { + private final String contractOfferId; + private final JsonObject policyJsonLd; +} \ No newline at end of file diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/UiContractOffer.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/UiContractOffer.java new file mode 100644 index 0000000..bb87df4 --- /dev/null +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/model/UiContractOffer.java @@ -0,0 +1,40 @@ +package org.upm.inesdata.federated.model; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.upm.inesdata.complexpolicy.model.UiPolicy; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@Builder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Catalog Data Offer's Contract Offer as required by the UI") +public class UiContractOffer { + @Schema(description = "Contract Offer ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String contractOfferId; + + @Schema(description = "Policy", requiredMode = Schema.RequiredMode.REQUIRED) + private UiPolicy policy; + + public String getContractOfferId() { + return contractOfferId; + } + + public void setContractOfferId(String contractOfferId) { + this.contractOfferId = contractOfferId; + } + + public UiPolicy getPolicy() { + return policy; + } + + public void setPolicy(UiPolicy policy) { + this.policy = policy; + } +} diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/transformer/JsonObjectFromUiContractOfferTransformer.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/transformer/JsonObjectFromUiContractOfferTransformer.java new file mode 100644 index 0000000..084f337 --- /dev/null +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/transformer/JsonObjectFromUiContractOfferTransformer.java @@ -0,0 +1,138 @@ +package org.upm.inesdata.federated.transformer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.upm.inesdata.complexpolicy.model.UiPolicy; +import org.upm.inesdata.complexpolicy.model.UiPolicyConstraint; +import org.upm.inesdata.complexpolicy.model.UiPolicyExpression; +import org.upm.inesdata.complexpolicy.model.UiPolicyLiteral; +import org.upm.inesdata.federated.model.UiContractOffer; +import org.upm.inesdata.federated.utils.Prop; + +import java.io.StringReader; + +public class JsonObjectFromUiContractOfferTransformer extends AbstractJsonLdTransformer { + + private final ObjectMapper objectMapper; + private final JsonBuilderFactory jsonFactory; + + public JsonObjectFromUiContractOfferTransformer(ObjectMapper objectMapper, JsonBuilderFactory jsonFactory) { + super(UiContractOffer.class, JsonObject.class); + this.objectMapper = objectMapper; + this.jsonFactory = jsonFactory; + } + + @Override + public Class getInputType() { + return UiContractOffer.class; + } + + @Override + public Class getOutputType() { + return JsonObject.class; + } + + @Override + public @Nullable JsonObject transform(@NotNull UiContractOffer input, @NotNull TransformerContext context) { + JsonObjectBuilder jsonObjectBuilder = jsonFactory.createObjectBuilder(); + jsonObjectBuilder.add(Prop.Edc.CTX + "contractOfferId", input.getContractOfferId()); + + UiPolicy policy = input.getPolicy(); + if (policy != null) { + JsonObjectBuilder policyBuilder = jsonFactory.createObjectBuilder(); + policyBuilder.add(Prop.Edc.CTX + "policyJsonLd", policy.getPolicyJsonLd()); + + if (policy.getExpression() != null) { + policyBuilder.add(Prop.Edc.CTX + "expression", transformPolicyExpression(policy.getExpression())); + } + + JsonArrayBuilder errorsBuilder = jsonFactory.createArrayBuilder(); + if (policy.getErrors() != null) { + for (String error : policy.getErrors()) { + errorsBuilder.add(error); + } + } + policyBuilder.add(Prop.Edc.CTX + "errors", errorsBuilder); + + jsonObjectBuilder.add(Prop.Edc.CTX + "policy", policyBuilder); + } + + return jsonObjectBuilder.build(); + } + + private JsonValue transformPolicyExpression(UiPolicyExpression expression) { + if (expression == null) { + return jsonFactory.createObjectBuilder().build(); + } + + JsonObjectBuilder expressionBuilder = jsonFactory.createObjectBuilder(); + expressionBuilder.add(Prop.Edc.CTX + "type", expression.getType().name()); + + if (expression.getExpressions() != null) { + JsonArrayBuilder expressionsArrayBuilder = jsonFactory.createArrayBuilder(); + for (UiPolicyExpression subExpression : expression.getExpressions()) { + expressionsArrayBuilder.add(transformPolicyExpression(subExpression)); + } + expressionBuilder.add(Prop.Edc.CTX + "expressions", expressionsArrayBuilder); + } + + if (expression.getConstraint() != null) { + expressionBuilder.add(Prop.Edc.CTX + "constraint", transformPolicyConstraint(expression.getConstraint())); + } + + return expressionBuilder.build(); + } + + private JsonValue transformPolicyConstraint(UiPolicyConstraint constraint) { + if (constraint == null) { + return jsonFactory.createObjectBuilder().build(); + } + + JsonObjectBuilder constraintBuilder = jsonFactory.createObjectBuilder(); + constraintBuilder.add(Prop.Edc.CTX + "left", constraint.getLeft()); + constraintBuilder.add(Prop.Edc.CTX + "operator", constraint.getOperator().name()); + constraintBuilder.add(Prop.Edc.CTX + "right", transformPolicyLiteral(constraint.getRight())); + + return constraintBuilder.build(); + } + + private JsonValue transformPolicyLiteral(UiPolicyLiteral literal) { + if (literal == null) { + return jsonFactory.createObjectBuilder().build(); + } + + JsonObjectBuilder literalBuilder = jsonFactory.createObjectBuilder(); + literalBuilder.add(Prop.Edc.CTX + "type", literal.getType().name()); + + switch (literal.getType()) { + case STRING -> literalBuilder.add(Prop.Edc.CTX + "value", literal.getValue()); + case STRING_LIST -> { + JsonArrayBuilder listBuilder = jsonFactory.createArrayBuilder(); + for (String value : literal.getValueList()) { + listBuilder.add(value); + } + literalBuilder.add(Prop.Edc.CTX + "valueList", listBuilder); + } + case JSON -> { + try { + JsonValue jsonValue = Json.createReader(new StringReader(literal.getValue())).readValue(); + literalBuilder.add(Prop.Edc.CTX + "value", jsonValue); + } catch (Exception e) { + throw new RuntimeException("Error parsing JSON value", e); + } + } + default -> throw new IllegalArgumentException("Unsupported literal type: " + literal.getType()); + } + + return literalBuilder.build(); + } +} diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/JsonLdUtils.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/JsonLdUtils.java new file mode 100644 index 0000000..d50bd98 --- /dev/null +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/JsonLdUtils.java @@ -0,0 +1,300 @@ +package org.upm.inesdata.federated.utils; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.document.JsonDocument; +import jakarta.json.Json; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.upm.inesdata.complexpolicy.utils.JsonUtils; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonLdUtils { + private static final JsonDocument EMPTY_CONTEXT_DOCUMENT = JsonDocument.of(Json.createObjectBuilder() + .add(Prop.CONTEXT, Json.createObjectBuilder()) + .build()); + + /** + * Compact JSON-LD, but don't compact property names to namespaces. + * + * @param json json-ld + * @return compacted values + */ + public static JsonObject tryCompact(JsonObject json) { + try { + return com.apicatalog.jsonld.JsonLd.compact(JsonDocument.of(json), EMPTY_CONTEXT_DOCUMENT).get(); + } catch (JsonLdError e) { + return json; + } + } + + /** + * Compact JSON-LD, but don't compact property names to namespaces. + * + * @param json json-ld + * @return compacted values + */ + public static JsonObject expandKeysOnly(JsonObject json) { + try { + var expanded = com.apicatalog.jsonld.JsonLd.expand(JsonDocument.of(json)).get(); + return com.apicatalog.jsonld.JsonLd.compact(JsonDocument.of(expanded), EMPTY_CONTEXT_DOCUMENT).get(); + } catch (JsonLdError e) { + return json; + } + } + + public static boolean isEmptyArray(JsonValue json) { + return list(json).isEmpty(); + } + + public static boolean isEmptyObject(JsonValue json) { + return object(json).isEmpty(); + } + + + /** + * Get the ID value of an object + * + * @param json json-ld + * @return id or null + */ + public static String id(JsonObject json) { + return string(json, "@id"); + } + + /** + * Get a string property + * + * @param json json-ld + * @return string value or null + */ + public static String string(JsonValue json) { + var value = value(json); + if (value == null) { + return null; + } + + return switch (value.getValueType()) { + case STRING -> ((JsonString) value).getString(); + case NUMBER -> ((JsonNumber) value).bigDecimalValue().toString(); + case FALSE -> "false"; + case TRUE -> "true"; + case NULL -> null; + // We do this over throwing errors because we want to be able to handle invalid json-ld + case ARRAY, OBJECT -> JsonUtils.toJson(value); + }; + } + + /** + * Get a offset date time property + * + * @param json json-ld + * @return offset date time value or null + */ + public static LocalDate localDate(JsonValue json) { + var str = string(json); + if (str == null) { + return null; + } + + try { + return LocalDate.parse(str); + } catch (DateTimeParseException e) { + return null; + } + } + + /** + * Get a boolean property + * + * @param json json-ld + * @return boolean value or null + */ + public static Boolean bool(JsonValue json) { + var value = value(json); + if (value == null) { + return null; + } + + return switch (value.getValueType()) { + case STRING -> switch (((JsonString) value).getString().toLowerCase()) { + case "true" -> Boolean.TRUE; + case "false" -> Boolean.FALSE; + default -> null; + }; + case FALSE -> Boolean.FALSE; + case TRUE -> Boolean.TRUE; + case NUMBER, NULL, ARRAY, OBJECT -> null; + }; + } + + /** + * Get a list property. + * + * @param json json-ld + * @return list of values + */ + public static List list(JsonValue json) { + return switch (json.getValueType()) { + case ARRAY -> json.asJsonArray(); + case FALSE, TRUE, NUMBER, STRING, OBJECT -> List.of(json); + case NULL -> List.of(); + }; + } + + /** + * Get a list property while unwrapping values and only keeping objects. + * + * @param json json-ld + * @return list of values + */ + public static List listOfObjects(JsonValue json) { + return list(json).stream() + .map(JsonLdUtils::value) // unwrap @value + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .toList(); + } + + /** + * Get the innermost @value of an object. Also removes wrappings in lists. + * + * @param json json-ld + * @return innermost @value + */ + public static JsonValue value(JsonValue json) { + return switch (json.getValueType()) { + case ARRAY -> { + var array = json.asJsonArray(); + if (array.isEmpty()) { + yield null; + } + yield value(array.get(0)); + } + case OBJECT -> { + var object = json.asJsonObject(); + if (object.containsKey("@value")) { + yield value(object.get("@value")); + } else { + yield object; + } + } + case STRING, NUMBER, FALSE, TRUE, NULL -> json; + }; + } + + /** + * Get a string property + * + * @param object json-ld + * @param key key + * @return string or null + */ + public static String string(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return string(field); + } + + /** + * Get a offset date time property + * + * @param object json-ld + * @param key key + * @return offset date time or null + */ + public static LocalDate localDate(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return localDate(field); + } + + /** + * Get an object property. Defaults to an empty object for ease of use if not found. + * + * @param object json-ld + * @param key key + * @return string or null + */ + public static JsonObject object(JsonObject object, String key) { + return object(object.get(key)); + } + + /** + * Get an object property. Defaults to an empty object for ease of use if not found. + * + * @param field json-ld + * @return string or null + */ + public static JsonObject object(JsonValue field) { + if (field == null) { + return JsonValue.EMPTY_JSON_OBJECT; + } + + var unwrapped = value(field); + if (unwrapped == null || unwrapped.getValueType() != JsonValue.ValueType.OBJECT) { + return JsonValue.EMPTY_JSON_OBJECT; + } + + return (JsonObject) unwrapped; + } + + /** + * Get a list property while unwrapping values and only keeping objects. + * + * @param object json-ld + * @param key key + * @return list of values + */ + public static List listOfObjects(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return List.of(); + } + return listOfObjects(field); + } + + /** + * Get a list of strings. defaults to empty list + * + * @param object json-ld + * @param key key + * @return string list or empty list + */ + public static List stringList(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return List.of(); + } + return list(field).stream() + .map(JsonLdUtils::string) + .toList(); + } + + /** + * Get a boolean property. defaults to null + * + * @param object json-ld + * @param key key + * @return boolean or null + */ + public static Boolean bool(JsonObject object, String key) { + JsonValue field = object.get(key); + if (field == null) { + return null; + } + return bool(field); + } +} + diff --git a/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/Prop.java b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/Prop.java new file mode 100644 index 0000000..c53f102 --- /dev/null +++ b/extensions/federated-catalog-cache-api/src/main/java/org/upm/inesdata/federated/utils/Prop.java @@ -0,0 +1,219 @@ +package org.upm.inesdata.federated.utils; + +import lombok.experimental.UtilityClass; + +/** + * Constants for used JSON-LD Vocabulary. + *

+ * Please note, that due to how JSON-LD / ontologies are defined, all property names of a namespace are just + * mixed together on the same level. A property, e.g. type, might be used in multiple classes, which is an + * abstraction leak by design. + */ +@UtilityClass +public class Prop { + public final String ID = "@id"; + public final String TYPE = "@type"; + public final String VALUE = "@value"; + public final String CONTEXT = "@context"; + public final String LANGUAGE = "@language"; + public final String PROPERTIES = "properties"; + + @UtilityClass + public class Edc { + public final String CTX = "https://w3id.org/edc/v0.0.1/ns/"; + public final String CTX_ALIAS = "edc"; + public final String TYPE_ASSET = CTX + "Asset"; + public final String TYPE_DATA_ADDRESS = CTX + "DataAddress"; + public final String ID = CTX + "id"; + public final String PARTICIPANT_ID = CTX + "participantId"; + public final String PROPERTIES = CTX + "properties"; + public final String PRIVATE_PROPERTIES = CTX + "privateProperties"; + public final String DATA_ADDRESS = CTX + "dataAddress"; + public final String TYPE = CTX + "type"; + public final String DATA_ADDRESS_TYPE_HTTP_DATA = "HttpData"; + public final String DATA_ADDRESS_TYPE_HTTP_PROXY = "HttpProxy"; + public final String BASE_URL = CTX + "baseUrl"; + public final String METHOD = CTX + "method"; + public final String CONTENT_TYPE = CTX + "contentType"; + public final String QUERY_PARAMS = CTX + "queryParams"; + public final String AUTH_KEY = CTX + "authKey"; + public final String AUTH_CODE = CTX + "authCode"; + public final String SECRET_NAME = CTX + "secretName"; + public final String PROXY_METHOD = CTX + "proxyMethod"; + public final String PROXY_PATH = CTX + "proxyPath"; + public final String PROXY_QUERY_PARAMS = CTX + "proxyQueryParams"; + public final String PROXY_BODY = CTX + "proxyBody"; + + // Transfer Request Related + public static String TYPE_TRANSFER_REQUEST = CTX + "TransferRequest"; + public final String CONNECTOR_ADDRESS = CTX + "connectorAddress"; + public final String CONTRACT_ID = CTX + "contractId"; + public final String CONNECTOR_ID = CTX + "connectorId"; + public final String ASSET_ID = CTX + "assetId"; + public final String DATA_DESTINATION = CTX + "dataDestination"; + public final String RECEIVER_HTTP_ENDPOINT = CTX + "receiverHttpEndpoint"; + } + + /** + * DCAT Vocabulary, see https://www.w3.org/TR/vocab-dcat-3 + */ + @UtilityClass + public class Dcat { + /** + * Context as specified in https://www.w3.org/TR/vocab-dcat-3/#normative-namespaces + */ + public final String CTX = "http://www.w3.org/ns/dcat#"; + + /** + * Context as used in the Core EDC, or atleast how its output from a DCAT request + */ + public final String CTX_WRONG_BUT_USED_BY_CORE_EDC = "https://www.w3.org/ns/dcat/"; + + public final String DATASET = CTX + "dataset"; + public final String DISTRIBUTION = CTX + "distribution"; + public final String DISTRIBUTION_AS_USED_BY_CORE_EDC = CTX_WRONG_BUT_USED_BY_CORE_EDC + "distribution"; + public final String VERSION = CTX + "version"; + public final String KEYWORDS = CTX + "keyword"; + public final String LANDING_PAGE = CTX + "landingPage"; + public final String MEDIATYPE = CTX + "mediaType"; + public final String START_DATE = CTX + "startDate"; + public final String END_DATE = CTX + "endDate"; + public final String DOWNLOAD_URL = CTX + "downloadURL"; + } + + /** + * ODRL Vocabulary, see DCAT 3 Specification + */ + @UtilityClass + public class Odrl { + public final String CTX = "http://www.w3.org/ns/odrl/2/"; + public final String HAS_POLICY = CTX + "hasPolicy"; + public final String ACTION = CTX + "action"; + public final String TYPE = CTX + "type"; + public final String CONSTRAINT = CTX + "constraint"; + public final String AND = CTX + "and"; + public final String PERMISSION = CTX + "permission"; + public final String LEFT_OPERAND = CTX + "leftOperand"; + public final String RIGHT_OPERAND = CTX + "rightOperand"; + public final String USE = "USE"; + } + + /** + * Dcterms Metadata Terms Vocabulary, see DCMI Metadata Terms + */ + @UtilityClass + public class Dcterms { + public final String CTX = "http://purl.org/dc/terms/"; + public final String IDENTIFIER = CTX + "identifier"; + public final String TITLE = CTX + "title"; + public final String DESCRIPTION = CTX + "description"; + public final String LANGUAGE = CTX + "language"; + public final String CREATOR = CTX + "creator"; + public final String PUBLISHER = CTX + "publisher"; + public final String LICENSE = CTX + "license"; + public final String TEMPORAL = CTX + "temporal"; + public final String ACCRUAL_PERIODICITY = CTX + "accrualPeriodicity"; + public final String SPATIAL = CTX + "spatial"; + public final String RIGHTS_HOLDER = CTX + "rightsHolder"; + public final String RIGHTS = CTX + "rights"; + public final String RIGHTS_STATEMENT = CTX + "RightsStatement"; + } + + /** + * Dcterms Metadata Terms Vocabulary, see DCAT 3 Specification + */ + @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"; + public final String DATA_SOURCE_AVAILABILITY = CTX + "dataSourceAvailability"; + public final String DATA_SOURCE_AVAILABILITY_ON_REQUEST = "ON_REQUEST"; + public final String CONTACT_EMAIL = CTX + "contactEmail"; + public final String CONTACT_PREFERRED_EMAIL_SUBJECT = CTX + "contactPreferredEmailSubject"; + + @UtilityClass + public class HttpDatasourceHints { + public final String METHOD = CTX + "httpDatasourceHintsProxyMethod"; + public final String PATH = CTX + "httpDatasourceHintsProxyPath"; + public final String QUERY_PARAMS = CTX + "httpDatasourceHintsProxyQueryParams"; + public final String BODY = CTX + "httpDatasourceHintsProxyBody"; + } + } + + @UtilityClass + public class SovityMessageExt { + public final String CTX = "https://semantic.sovity.io/message/generic/"; + public final String REQUEST = CTX + "request"; + public final String RESPONSE = CTX + "response"; + public final String ERROR_MESSAGE = CTX + "errorMessage"; + public final String HEADER = CTX + "header"; + public final String BODY = CTX + "body"; + } + + /** + * FOAF Vocabulary + */ + @UtilityClass + public class Foaf { + public final String CTX = "http://xmlns.com/foaf/0.1/"; + public final String ORGANIZATION = CTX + "Organization"; + public final String NAME = CTX + "name"; + public final String HOMEPAGE = CTX + "homepage"; + } + + /** + * Namespace mobilitydcatap as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class MobilityDcatAp { + public final String CTX = "https://w3id.org/mobilitydcat-ap/"; + public final String MOBILITY_THEME = CTX + "mobilityTheme"; + + @UtilityClass + public class DataCategoryProps { + public final String CTX = "https://w3id.org/mobilitydcat-ap/mobility-theme/"; + public final String DATA_CATEGORY = CTX + "data-content-category"; + public final String DATA_SUBCATEGORY = CTX + "data-content-sub-category"; + } + + public final String TRANSPORT_MODE = CTX + "transportMode"; + public final String GEO_REFERENCE_METHOD = CTX + "georeferencingMethod"; + public final String MOBILITY_DATA_STANDARD = CTX + "mobilityDataStandard"; + + // Optional property of mobilitydcatap:mobilityDataStandard + public final String SCHEMA = CTX + "schema"; + } + + /** + * Namespace skos as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Skos { + public final String CTX = "http://www.w3.org/2004/02/skos/core#"; + public final String PREF_LABEL = CTX + "prefLabel"; + } + + /** + * Namespace adms as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Adms { + public final String CTX = "http://www.w3.org/ns/adms#"; + public final String SAMPLE = CTX + "sample"; + } + + /** + * Namespace rdfs as specified in + * mobilityDCAT-AP + */ + @UtilityClass + public class Rdfs { + public final String CTX = "http://www.w3.org/2000/01/rdf-schema#"; + public final String LITERAL = CTX + "Literal"; + public final String LABEL = CTX + "label"; + } +}