diff --git a/README.md b/README.md index 7213fba..34b6405 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The connector is prepared to be executed inside a container. The following steps - Create a local Docker image: ``` - docker build --tag inesdata/connector:0.1 --build-arg CONNECTOR_JAR=./launchers/connector/build/libs/connector-app.jar -f docker/Dockerfile . + docker build --tag inesdata/connector:0.2.0 --build-arg CONNECTOR_JAR=./launchers/connector/build/libs/connector-app.jar -f docker/Dockerfile . ``` ## Database diff --git a/extensions/vocabulary-api/README.md b/extensions/vocabulary-api/README.md new file mode 100644 index 0000000..6fbc14d --- /dev/null +++ b/extensions/vocabulary-api/README.md @@ -0,0 +1,19 @@ +# Vocabulary API + +Provides a management API for work with vocabularies. This API expands the functionality of the control-plane management API to be able to handle Vocabulary entities. + + +## Vocabulary entity + +An example of a Vocabulary entity is shown below. + +``` +{ + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@id": "vocabularyId", + "name": "Vocabulary name", + "jsonSchema": "{ \"title\": \"vocabulary\", \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\", \"title\": \"Name\" }, \"keyword\": { \"type\": \"array\", \"title\": \"Keywords\", \"items\": { \"type\": \"string\" } } }, \"required\": [ \"name\" ] }" +} +``` diff --git a/extensions/vocabulary-api/build.gradle.kts b/extensions/vocabulary-api/build.gradle.kts new file mode 100644 index 0000000..910d678 --- /dev/null +++ b/extensions/vocabulary-api/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + `java-library` + id("com.gmv.inesdata.edc-application") +} + +dependencies { + api(project(":spi:vocabulary-spi")) + api(libs.edc.spi.core) + implementation(libs.edc.spi.transform) + implementation(libs.edc.web.spi) + + implementation(libs.edc.connector.core) + implementation(libs.edc.api.core) + implementation(libs.edc.lib.util) + implementation(libs.edc.lib.transform) + implementation(libs.edc.dsp.api.configuration) + implementation(libs.edc.api.management.config) + implementation(libs.edc.transaction.spi) + implementation(libs.edc.lib.validator) + implementation(libs.edc.validator.spi) + implementation(libs.swagger.annotations.jakarta) + runtimeOnly(libs.edc.spi.jsonld) + runtimeOnly(libs.edc.json.ld.lib) +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/VocabularyApiExtension.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/VocabularyApiExtension.java new file mode 100644 index 0000000..09b3e91 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/VocabularyApiExtension.java @@ -0,0 +1,120 @@ +package org.upm.inesdata.vocabulary; + +import jakarta.json.Json; +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.health.HealthCheckResult; +import org.eclipse.edc.spi.system.health.HealthCheckService; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.WebService; +import org.upm.inesdata.spi.vocabulary.VocabularyIndex; +import org.upm.inesdata.spi.vocabulary.VocabularyService; +import org.upm.inesdata.vocabulary.controller.VocabularyApiController; +import org.upm.inesdata.vocabulary.service.VocabularyServiceImpl; +import org.upm.inesdata.vocabulary.storage.InMemoryVocabularyIndex; +import org.upm.inesdata.vocabulary.transformer.JsonObjectFromVocabularyTransformer; +import org.upm.inesdata.vocabulary.transformer.JsonObjectToVocabularyTransformer; +import org.upm.inesdata.vocabulary.validator.VocabularyValidator; + +import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.EDC_VOCABULARY_TYPE; + +import java.util.Map; + +/** + * Extension that provides an API for managing vocabularies + */ +@Extension(value = VocabularyApiExtension.NAME) +public class VocabularyApiExtension implements ServiceExtension { + + public static final String NAME = "Vocabulary API Extension"; + private InMemoryVocabularyIndex defaultVocabularyIndex; + + @Inject + private WebService webService; + + @Inject + private VocabularyIndex vocabularyIndex; + + @Inject(required = false) + private HealthCheckService healthCheckService; + + @Inject + private ManagementApiConfiguration config; + + @Inject + private TypeManager typeManager; + + @Inject + private TransactionContext transactionContext; + + @Inject + private TypeTransformerRegistry transformerRegistry; + + @Inject + private JsonObjectValidatorRegistry validator; + + @Override + public String name() { + return NAME; + } + + /** + * Provides a default vocabularyService implementation + */ + @Provider(isDefault = true) + public VocabularyService vocabularyService() { + return new VocabularyServiceImpl(vocabularyIndex, transactionContext); + } + + /** + * Provides a default in memory vocabularyIndex + */ + @Provider(isDefault = true) + public VocabularyIndex defaultVocabularyIndex() { + return getVocabularyIndex(); + } + + /** + * Initializes the service + */ + @Override + public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); + + var managementApiTransformerRegistry = transformerRegistry.forContext("management-api"); + + var factory = Json.createBuilderFactory(Map.of()); + var jsonLdMapper = typeManager.getMapper(JSON_LD); + managementApiTransformerRegistry.register(new JsonObjectFromVocabularyTransformer(factory, jsonLdMapper)); + managementApiTransformerRegistry.register(new JsonObjectToVocabularyTransformer()); + + validator.register(EDC_VOCABULARY_TYPE, VocabularyValidator.instance()); + var vocabularyApiController = new VocabularyApiController(this.vocabularyService(), managementApiTransformerRegistry, monitor, validator); + webService.registerResource(config.getContextAlias(), vocabularyApiController); + + // contribute to the liveness probe + if (healthCheckService != null) { + var successResult = HealthCheckResult.Builder.newInstance().component("FCC Query API").build(); + healthCheckService.addReadinessProvider(() -> successResult); + healthCheckService.addLivenessProvider(() -> successResult); + } + } + + /** + * Creates a InMemoryVocabularyIndex if not exists + */ + private InMemoryVocabularyIndex getVocabularyIndex() { + if (defaultVocabularyIndex == null) { + defaultVocabularyIndex = new InMemoryVocabularyIndex(); + } + return defaultVocabularyIndex; + } +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApi.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApi.java new file mode 100644 index 0000000..acb5a62 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApi.java @@ -0,0 +1,133 @@ +package org.upm.inesdata.vocabulary.controller; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import org.eclipse.edc.api.model.ApiCoreSchema; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.EDC_VOCABULARY_TYPE; + +/** + * Controller for managing {@link Vocabulary} objects. + */ +@OpenAPIDefinition( + info = @Info(description = "Manages the connector vocabularies.", + title = "Vocabulary API", version = "1")) +@Tag(name = "Vocabulary") +public interface VocabularyApi { + + /** + * Get all the vocabularies stored in the vocabularies index. No filters are required due + * to the limited number of vocabularies that each data space will manage. + * + * @return list of vocabularies + */ + @Operation(description = "Obtains all vocabularies", + responses = { + @ApiResponse(responseCode = "200", description = "A list of vocabularies", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ContractOffer.class)))) + } + ) + JsonArray getVocabularies(); + + /** + * Retrieves the {@link Vocabulary} with the given ID + * + * @param id id of the vocabulary + * @return JsonObject with the vocabulary information + */ + @Operation(description = "Gets a vocabulary with the given ID", + responses = { + @ApiResponse(responseCode = "200", description = "The vocabulary", + content = @Content(schema = @Schema(implementation = VocabularyOutputSchema.class))), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "404", description = "A vocabulary with the given ID does not exist", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) + } + ) + JsonObject getVocabulary(String id); + + /** + * Creates a new vocabulary + * + * @param vocabulary the vocabulary + * @return JsonObject with the created vocabulary + */ + @Operation(description = "Creates a new vocabulary", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = VocabularyOutputSchema.class))), + responses = { + @ApiResponse(responseCode = "200", description = "Vocabulary was created successfully", + 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)))), + @ApiResponse(responseCode = "409", description = "Could not create vocabulary, because a vocabulary with that ID already exists", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) } + ) + JsonObject createVocabulary(JsonObject vocabylary); + + /** + * Updates a vocabulary + * + * @param vocabulary the vocabulary to be updated + * @return JsonObject with the updated vocabulary + */ + @Operation(description = "Updates a vocabulary with the given ID if it exists. If the vocabulary is not found, no further action is taken.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = VocabularyOutputSchema.class))), + responses = { + @ApiResponse(responseCode = "204", description = "Vocabulary was updated successfully"), + @ApiResponse(responseCode = "404", description = "Vocabulary could not be updated, because it does not exist."), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + }) + void updateVocabulary(JsonObject vocabulary); + + /** + * Removes the {@link Vocabulary} with the given ID + * + * @param id id of the vocabulary + * @return JsonObject with the updated vocabulary + */ + @Operation(description = "Removes a vocabulary with the given ID if possible", + responses = { + @ApiResponse(responseCode = "204", description = "Vocabulary was deleted successfully"), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))), + @ApiResponse(responseCode = "404", description = "Vocabulary could not be removed, because it does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))) + }) + void removeVocabulary(String id); + + /** + * Vocabulary output + */ + @ArraySchema() + @Schema(name = "VocabularyOutput", example = VocabularyOutputSchema.VOCABULARY_OUTPUT_EXAMPLE) + record VocabularyOutputSchema( + @Schema(name = ID) + String id, + @Schema(name = TYPE, example = EDC_VOCABULARY_TYPE) + String name, + String jsonSchema + ) { + public static final String VOCABULARY_OUTPUT_EXAMPLE = """ + { + "@id": "vocabularyId", + "name": "vocabulary name", + "jsonSchema": "{ \\"title\\": \\"vocabulary\\", \\"type\\": \\"object\\", \\"properties\\": { \\"name\\": { \\"type\\": \\"string\\", \\"title\\": \\"Name\\" }, \\"dct:keyword\\": { \\"type\\": \\"array\\", \\"title\\": \\"Keywords\\", \\"items\\": { \\"type\\": \\"string\\" } } }, \\"required\\": [ \\"name\\" ], \\"@context\\": { \\"dct\\": \\"http:\\/\\/purl.org\\/dc\\/terms\\/\" } }" + } + """; + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApiController.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApiController.java new file mode 100644 index 0000000..48bc1d6 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/controller/VocabularyApiController.java @@ -0,0 +1,119 @@ +package org.upm.inesdata.vocabulary.controller; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +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.ObjectNotFoundException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import org.upm.inesdata.spi.vocabulary.VocabularyService; +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; + +import static jakarta.json.stream.JsonCollectors.toJsonArray; +import static java.util.Optional.of; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.EDC_VOCABULARY_TYPE; + +import org.eclipse.edc.api.model.IdResponse; + +/** + * Implementation of the controller for {@link Vocabulary} managing. + */ +@Consumes({MediaType.APPLICATION_JSON}) +@Produces({MediaType.APPLICATION_JSON}) +@Path("/vocabularies") +public class VocabularyApiController implements VocabularyApi { + private final TypeTransformerRegistry transformerRegistry; + private final VocabularyService service; + private final Monitor monitor; + private final JsonObjectValidatorRegistry validator; + + /** + * Constructor + */ + public VocabularyApiController(VocabularyService service, TypeTransformerRegistry transformerRegistry, + Monitor monitor, JsonObjectValidatorRegistry validator) { + this.transformerRegistry = transformerRegistry; + this.service = service; + this.monitor = monitor; + this.validator = validator; + } + + @POST + @Override + public JsonObject createVocabulary(JsonObject vocabularyJson) { + validator.validate(EDC_VOCABULARY_TYPE, vocabularyJson).orElseThrow(ValidationFailureException::new); + + var vocabulary = transformerRegistry.transform(vocabularyJson, Vocabulary.class) + .orElseThrow(InvalidRequestException::new); + + var idResponse = service.create(vocabulary) + .map(a -> IdResponse.Builder.newInstance() + .id(a.getId()) + .createdAt(a.getCreatedAt()) + .build()) + .orElseThrow(exceptionMapper(Vocabulary.class, vocabulary.getId())); + + return transformerRegistry.transform(idResponse, JsonObject.class) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + + @POST + @Path("/request") + @Override + public JsonArray getVocabularies() { + return service.search().getContent().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()); + } + + @GET + @Path("{id}") + @Override + public JsonObject getVocabulary(@PathParam("id") String id) { + var vocabulary = of(id) + .map(it -> service.findById(id)) + .orElseThrow(() -> new ObjectNotFoundException(Vocabulary.class, id)); + + return transformerRegistry.transform(vocabulary, JsonObject.class) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + + } + + @DELETE + @Path("{id}") + @Override + public void removeVocabulary(@PathParam("id") String id) { + service.delete(id).orElseThrow(exceptionMapper(Vocabulary.class, id)); + } + + @PUT + @Override + public void updateVocabulary(JsonObject vocabularyJson) { + validator.validate(EDC_VOCABULARY_TYPE, vocabularyJson).orElseThrow(ValidationFailureException::new); + + var vocabularyResult = transformerRegistry.transform(vocabularyJson, Vocabulary.class) + .orElseThrow(InvalidRequestException::new); + + service.update(vocabularyResult) + .orElseThrow(exceptionMapper(Vocabulary.class, vocabularyResult.getId())); + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/service/VocabularyServiceImpl.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/service/VocabularyServiceImpl.java new file mode 100644 index 0000000..e72ed5c --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/service/VocabularyServiceImpl.java @@ -0,0 +1,70 @@ +package org.upm.inesdata.vocabulary.service; + +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import org.upm.inesdata.spi.vocabulary.VocabularyIndex; +import org.upm.inesdata.spi.vocabulary.VocabularyService; +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; + +import java.util.List; + +/** + * Implementation of the {@link VocabularyService} interface + */ +public class VocabularyServiceImpl implements VocabularyService { + + private final VocabularyIndex index; + private final TransactionContext transactionContext; + + /** + * Constructor + */ + public VocabularyServiceImpl(VocabularyIndex index, TransactionContext transactionContext) { + this.index = index; + + this.transactionContext = transactionContext; + } + + @Override + public Vocabulary findById(String vocabularyId) { + return transactionContext.execute(() -> index.findById(vocabularyId)); + } + + @Override + public ServiceResult> search() { + return transactionContext.execute(() -> { + try (var stream = index.allVocabularies()) { + return ServiceResult.success(stream.toList()); + } + }); + } + + @Override + public ServiceResult create(Vocabulary vocabulary) { + return transactionContext.execute(() -> { + var createResult = index.create(vocabulary); + if (createResult.succeeded()) { + return ServiceResult.success(vocabulary); + } + return ServiceResult.fromFailure(createResult); + }); + } + + @Override + public ServiceResult delete(String vocabularyId) { + return transactionContext.execute(() -> { + var deleted = index.deleteById(vocabularyId); + return ServiceResult.from(deleted); + }); + } + + @Override + public ServiceResult update(Vocabulary vocabulary) { + return transactionContext.execute(() -> { + var updatedVocabulary = index.updateVocabulary(vocabulary); + return ServiceResult.from(updatedVocabulary); + }); + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/storage/InMemoryVocabularyIndex.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/storage/InMemoryVocabularyIndex.java new file mode 100644 index 0000000..1ce1afb --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/storage/InMemoryVocabularyIndex.java @@ -0,0 +1,107 @@ +package org.upm.inesdata.vocabulary.storage; + +import org.eclipse.edc.spi.result.StoreResult; +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; +import org.upm.inesdata.spi.vocabulary.VocabularyIndex; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Stream; + +import static java.lang.String.format; + +/** + * An ephemeral vocabulary index + */ +public class InMemoryVocabularyIndex implements VocabularyIndex { + private final Map cache = new ConcurrentHashMap<>(); + + private final ReentrantReadWriteLock lock; + + /** + * Constructor + */ + public InMemoryVocabularyIndex() { + lock = new ReentrantReadWriteLock(true); + } + + @Override + public Stream allVocabularies() { + lock.readLock().lock(); + try { + return cache.values().stream(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Vocabulary findById(String vocabularyId) { + lock.readLock().lock(); + try { + return cache.values().stream() + .filter(vocabulary -> vocabulary.getId().equals(vocabularyId)) + .findFirst() + .orElse(null); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public StoreResult create(Vocabulary vocabulary) { + lock.writeLock().lock(); + try { + var id = vocabulary.getId(); + if (cache.containsKey(id)) { + return StoreResult.alreadyExists(format(VOCABULARY_EXISTS_TEMPLATE, id)); + } + Objects.requireNonNull(vocabulary, "vocabulary"); + Objects.requireNonNull(id, "vocabulary.getId()"); + cache.put(id, vocabulary); + } finally { + lock.writeLock().unlock(); + } + return StoreResult.success(); + } + + @Override + public StoreResult deleteById(String vocabularyId) { + lock.writeLock().lock(); + try { + return Optional.ofNullable(delete(vocabularyId)) + .map(StoreResult::success) + .orElse(StoreResult.notFound(format(VOCABULARY_NOT_FOUND_TEMPLATE, vocabularyId))); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public StoreResult updateVocabulary(Vocabulary vocabulary) { + lock.writeLock().lock(); + try { + var id = vocabulary.getId(); + Objects.requireNonNull(vocabulary, "vocabulary"); + Objects.requireNonNull(id, "vocabularyId"); + if (cache.containsKey(id)) { + cache.put(id, vocabulary); + return StoreResult.success(vocabulary); + } + return StoreResult.notFound(format(VOCABULARY_NOT_FOUND_TEMPLATE, id)); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Remove a vocabulary from cache based on its id + */ + private Vocabulary delete(String vocabularyId) { + return cache.remove(vocabularyId); + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectFromVocabularyTransformer.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectFromVocabularyTransformer.java new file mode 100644 index 0000000..92599d5 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectFromVocabularyTransformer.java @@ -0,0 +1,46 @@ +package org.upm.inesdata.vocabulary.transformer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +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.spi.vocabulary.domain.Vocabulary; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; + +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.EDC_VOCABULARY_TYPE; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_JSON_SCHEMA; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_NAME; + +/** + * Creates a JsonObject from a {@link Vocabulary} + */ +public class JsonObjectFromVocabularyTransformer extends AbstractJsonLdTransformer { + private final ObjectMapper mapper; + private final JsonBuilderFactory jsonFactory; + + /** + * Constructor + */ + public JsonObjectFromVocabularyTransformer(JsonBuilderFactory jsonFactory, ObjectMapper jsonLdMapper) { + super(Vocabulary.class, JsonObject.class); + this.jsonFactory = jsonFactory; + this.mapper = jsonLdMapper; + } + + @Override + public @Nullable JsonObject transform(@NotNull Vocabulary vocabulary, @NotNull TransformerContext context) { + var builder = jsonFactory.createObjectBuilder() + .add(ID, vocabulary.getId()) + .add(TYPE, EDC_VOCABULARY_TYPE) + .add(PROPERTY_NAME, vocabulary.getName()) + .add(PROPERTY_JSON_SCHEMA, vocabulary.getJsonSchema()); + + return builder.build(); + } +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectToVocabularyTransformer.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectToVocabularyTransformer.java new file mode 100644 index 0000000..be99616 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/transformer/JsonObjectToVocabularyTransformer.java @@ -0,0 +1,39 @@ +package org.upm.inesdata.vocabulary.transformer; + +import jakarta.json.JsonObject; +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.spi.vocabulary.domain.Vocabulary; + +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_JSON_SCHEMA; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_NAME; + +/** + * Converts from an {@link Vocabulary} as a {@link JsonObject} in JSON-LD expanded form to an {@link Vocabulary}. + */ +public class JsonObjectToVocabularyTransformer extends AbstractJsonLdTransformer { + + /** + * Constructor + */ + public JsonObjectToVocabularyTransformer() { + super(JsonObject.class, Vocabulary.class); + } + + @Override + public @Nullable Vocabulary transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) { + var builder = Vocabulary.Builder.newInstance() + .id(nodeId(jsonObject)); + + visitProperties(jsonObject, key -> switch (key) { + case PROPERTY_NAME -> value -> builder.name(transformString(value, context)); + case PROPERTY_JSON_SCHEMA -> value -> builder.jsonSchema(transformString(value, context)); + default -> doNothing(); + }); + + return builderResult(builder::build, context); + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/MandatoryJsonField.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/MandatoryJsonField.java new file mode 100644 index 0000000..02dfd40 --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/MandatoryJsonField.java @@ -0,0 +1,55 @@ +package org.upm.inesdata.vocabulary.validator; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.stream.JsonParsingException; +import java.io.StringReader; +import org.eclipse.edc.validator.jsonobject.JsonLdPath; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +import java.util.Optional; + +import static java.lang.String.format; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VALUE; +import static org.eclipse.edc.validator.spi.Violation.violation; + +/** + * Verifies that a @value is present and is a Json. + */ +public class MandatoryJsonField implements Validator { + private final JsonLdPath path; + + /** + * Constructor + */ + public MandatoryJsonField(JsonLdPath path) { + this.path = path; + } + + @Override + public ValidationResult validate(JsonObject input) { + return Optional.ofNullable(input.getJsonArray(path.last())) + .filter(it -> !it.isEmpty()) + .map(it -> it.getJsonObject(0)) + .map(it -> it.getString(VALUE)) + .map(this::validateJSON) + .orElseGet(() -> ValidationResult.failure(violation(format("mandatory value '%s'", path), path.toString()))); + } + + /** + * Checks whether a string contains a valid json or not + */ + private ValidationResult validateJSON(String value) { + try { + try (JsonReader jsonReader = Json.createReader(new StringReader(value))) { + jsonReader.read(); + } + } catch (JsonParsingException e) { + return ValidationResult.failure(violation(format("content '%s' should be a valid Json", path), path.toString())); + } + return ValidationResult.success(); + } + +} diff --git a/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/VocabularyValidator.java b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/VocabularyValidator.java new file mode 100644 index 0000000..1b1753c --- /dev/null +++ b/extensions/vocabulary-api/src/main/java/org/upm/inesdata/vocabulary/validator/VocabularyValidator.java @@ -0,0 +1,27 @@ +package org.upm.inesdata.vocabulary.validator; + +import jakarta.json.JsonObject; +import org.eclipse.edc.validator.jsonobject.JsonObjectValidator; +import org.eclipse.edc.validator.jsonobject.validators.MandatoryObject; +import org.eclipse.edc.validator.jsonobject.validators.OptionalIdNotBlank; +import org.eclipse.edc.validator.spi.Validator; + +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_JSON_SCHEMA; +import static org.upm.inesdata.spi.vocabulary.domain.Vocabulary.PROPERTY_NAME; + +/** + * Validator for Vocabulary + */ +public class VocabularyValidator { + + /** + * Defines the rules that a Vocabulary must comply with + */ + public static Validator instance() { + return JsonObjectValidator.newValidator() + .verifyId(OptionalIdNotBlank::new) + .verify(PROPERTY_NAME, MandatoryObject::new) + .verify(PROPERTY_JSON_SCHEMA, MandatoryJsonField::new) + .build(); + } +} diff --git a/extensions/vocabulary-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/vocabulary-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..8570889 --- /dev/null +++ b/extensions/vocabulary-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.upm.inesdata.vocabulary.VocabularyApiExtension \ No newline at end of file diff --git a/extensions/vocabulary-index-sql/README.md b/extensions/vocabulary-index-sql/README.md new file mode 100644 index 0000000..2e8e55c --- /dev/null +++ b/extensions/vocabulary-index-sql/README.md @@ -0,0 +1,30 @@ +# SQL Vocabulary + +Provides SQL persistence for vocabularies. + +## Prerequisites + +Please apply this [schema](docs/schema.sql) to your SQL database. + +## Entity Diagram + +![ER Diagram](//www.plantuml.com/plantuml/png/SoWkIImgAStDuKhDAyaigLH8JKcEByjFJamgpKaigbIevb9Gq5B8JB5IA2ufoinBLx2n2V2simEBvYNcfiB4mG9PnVbvmSaPgRc9ACB9HQc99QafZYLM2ZdvO35TNQvQBeVKl1IWnG00) + + +## Configuration + +| Key | Description | Mandatory | +|:---|:---|---| +| edc.datasource.vocabulary.name | Datasource used to store vocabularies | X | diff --git a/extensions/vocabulary-index-sql/build.gradle.kts b/extensions/vocabulary-index-sql/build.gradle.kts new file mode 100644 index 0000000..f71e25e --- /dev/null +++ b/extensions/vocabulary-index-sql/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `java-library` + id("com.gmv.inesdata.edc-application") +} + +dependencies { + api(project(":spi:vocabulary-spi")) + implementation(project(":extensions:vocabulary-api")) + api(libs.edc.spi.core) + api(libs.edc.transaction.spi) + implementation(libs.edc.transaction.spi) + implementation(libs.edc.transaction.datasource.spi) + implementation(libs.edc.sql.core) + implementation(libs.edc.lib.util) +} + + diff --git a/extensions/vocabulary-index-sql/docs/er.puml b/extensions/vocabulary-index-sql/docs/er.puml new file mode 100644 index 0000000..f081ea7 --- /dev/null +++ b/extensions/vocabulary-index-sql/docs/er.puml @@ -0,0 +1,9 @@ +@startuml +entity edc_vocabulary { + * id: string <> + * name: string + * jsonSchema: string + * createdAt: long + -- +} +@enduml \ No newline at end of file diff --git a/extensions/vocabulary-index-sql/docs/schema.sql b/extensions/vocabulary-index-sql/docs/schema.sql new file mode 100644 index 0000000..e6bd609 --- /dev/null +++ b/extensions/vocabulary-index-sql/docs/schema.sql @@ -0,0 +1,11 @@ +-- table: edc_vocabulary +CREATE TABLE IF NOT EXISTS edc_vocabulary +( + id VARCHAR NOT NULL, + created_at BIGINT NOT NULL, + json_schema JSON DEFAULT '{}', + name VARCHAR NOT NULL, + PRIMARY KEY (id) +); + +COMMENT ON COLUMN edc_vocabulary.json_schema IS 'JSON Schema with the vocabulary'; diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndex.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndex.java new file mode 100644 index 0000000..e2207c4 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndex.java @@ -0,0 +1,157 @@ +package org.upm.inesdata.vocabulary.sql.index; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.store.AbstractSqlStore; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.jetbrains.annotations.Nullable; + +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; +import org.upm.inesdata.vocabulary.sql.index.schema.VocabularyStatements; +import org.upm.inesdata.spi.vocabulary.VocabularyIndex; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static org.eclipse.edc.spi.query.Criterion.criterion; + +/** + * Implementation of the VocabularyIndes with SQL databases + */ +public class SqlVocabularyIndex extends AbstractSqlStore implements VocabularyIndex { + + private final VocabularyStatements vocabularyStatements; + + public SqlVocabularyIndex(DataSourceRegistry dataSourceRegistry, + String dataSourceName, + TransactionContext transactionContext, + ObjectMapper objectMapper, + VocabularyStatements vocabularyStatements, + QueryExecutor queryExecutor) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.vocabularyStatements = Objects.requireNonNull(vocabularyStatements); + } + + @Override + public Stream allVocabularies() { + return transactionContext.execute(() -> { + try { + var statement = vocabularyStatements.createQuery(new QuerySpec()); + return queryExecutor.query(getConnection(), true, this::mapVocabulary, statement.getQueryAsString(), statement.getParameters()); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public @Nullable Vocabulary findById(String vocabularyId) { + Objects.requireNonNull(vocabularyId); + + try (var connection = getConnection()) { + var querySpec = QuerySpec.Builder.newInstance().filter(criterion("id", "=", vocabularyId)).build(); + var statement = vocabularyStatements.createQuery(querySpec); + return queryExecutor.query(connection, true, this::mapVocabulary, statement.getQueryAsString(), statement.getParameters()) + .findFirst().orElse(null); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + } + + @Override + public StoreResult create(Vocabulary vocabulary) { + Objects.requireNonNull(vocabulary); + + var vocabularyId = vocabulary.getId(); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (existsById(vocabularyId, connection)) { + var msg = format(VocabularyIndex.VOCABULARY_NOT_FOUND_TEMPLATE, vocabularyId); + return StoreResult.alreadyExists(msg); + } + + queryExecutor.execute(connection, vocabularyStatements.getInsertVocabularyTemplate(), + vocabularyId, + vocabulary.getCreatedAt(), + vocabulary.getName(), + toJson(vocabulary.getJsonSchema()) + ); + + return StoreResult.success(); + } catch (Exception e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult deleteById(String vocabularyId) { + Objects.requireNonNull(vocabularyId); + + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var vocabulary = findById(vocabularyId); + if (vocabulary == null) { + return StoreResult.notFound(format(VocabularyIndex.VOCABULARY_NOT_FOUND_TEMPLATE, vocabularyId)); + } + + queryExecutor.execute(connection, vocabularyStatements.getDeleteVocabularyByIdTemplate(), vocabularyId); + + return StoreResult.success(vocabulary); + } catch (Exception e) { + throw new EdcPersistenceException(e.getMessage(), e); + } + }); + } + + @Override + public StoreResult updateVocabulary(Vocabulary vocabulary) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var vocabularyId = vocabulary.getId(); + if (existsById(vocabularyId, connection)) { + queryExecutor.execute(connection, vocabularyStatements.getUpdateVocabularyTemplate(), + vocabulary.getName(), + toJson(vocabulary.getJsonSchema()), + vocabularyId + ); + + return StoreResult.success(vocabulary); + } + return StoreResult.notFound(format(VocabularyIndex.VOCABULARY_NOT_FOUND_TEMPLATE, vocabularyId)); + + } catch (Exception e) { + throw new EdcPersistenceException(e); + } + }); + } + + private int mapRowCount(ResultSet resultSet) throws SQLException { + return resultSet.getInt(vocabularyStatements.getCountVariableName()); + } + + private boolean existsById(String vocabularyId, Connection connection) { + var sql = vocabularyStatements.getCountVocabularyByIdClause(); + try (var stream = queryExecutor.query(connection, false, this::mapRowCount, sql, vocabularyId)) { + return stream.findFirst().orElse(0) > 0; + } + } + + private Vocabulary mapVocabulary(ResultSet resultSet) throws SQLException { + return Vocabulary.Builder.newInstance() + .id(resultSet.getString(vocabularyStatements.getVocabularyIdColumn())) + .createdAt(resultSet.getLong(vocabularyStatements.getCreatedAtColumn())) + .name(resultSet.getString(vocabularyStatements.getNameColumn())) + .jsonSchema(resultSet.getString(vocabularyStatements.getJsonSchemaColumn())) + .build(); + } + +} diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndexServiceExtension.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndexServiceExtension.java new file mode 100644 index 0000000..5ad7b15 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/SqlVocabularyIndexServiceExtension.java @@ -0,0 +1,58 @@ +package org.upm.inesdata.vocabulary.sql.index; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.upm.inesdata.spi.vocabulary.VocabularyIndex; +import org.upm.inesdata.vocabulary.sql.index.schema.VocabularyStatements; +import org.upm.inesdata.vocabulary.sql.index.schema.postgres.PostgresDialectStatements; + +/** + * Extension that stores vocabylaries in SQL databases + */ +@Provides({ VocabularyIndex.class }) +@Extension(value = "SQL vocabulary index") +public class SqlVocabularyIndexServiceExtension implements ServiceExtension { + + /** + * Name of the vocabulary datasource. + */ + @Setting(required = true) + public static final String DATASOURCE_SETTING_NAME = "edc.datasource.vocabulary.name"; + + @Inject + private DataSourceRegistry dataSourceRegistry; + + @Inject + private TransactionContext transactionContext; + + @Inject(required = false) + private VocabularyStatements dialect; + + @Inject + private TypeManager typeManager; + + @Inject + private QueryExecutor queryExecutor; + + @Override + public void initialize(ServiceExtensionContext context) { + var dataSourceName = context.getConfig().getString(DATASOURCE_SETTING_NAME, DataSourceRegistry.DEFAULT_DATASOURCE); + + var sqlVocabularyLoader = new SqlVocabularyIndex(dataSourceRegistry, dataSourceName, transactionContext, typeManager.getMapper(), + getDialect(), queryExecutor); + + context.registerService(VocabularyIndex.class, sqlVocabularyLoader); + } + + private VocabularyStatements getDialect() { + return dialect != null ? dialect : new PostgresDialectStatements(); + } +} diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/BaseSqlDialectStatements.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/BaseSqlDialectStatements.java new file mode 100644 index 0000000..19e2fda --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/BaseSqlDialectStatements.java @@ -0,0 +1,68 @@ +package org.upm.inesdata.vocabulary.sql.index.schema; + +import org.upm.inesdata.vocabulary.sql.index.schema.postgres.VocabularyMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.SqlOperatorTranslator; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +/** + * Manages Vocabularies using specific SQL queries + */ +public class BaseSqlDialectStatements implements VocabularyStatements { + + protected final SqlOperatorTranslator operatorTranslator; + + public BaseSqlDialectStatements(SqlOperatorTranslator operatorTranslator) { + this.operatorTranslator = operatorTranslator; + } + + @Override + public String getInsertVocabularyTemplate() { + return executeStatement() + .column(getVocabularyIdColumn()) + .column(getCreatedAtColumn()) + .column(getNameColumn()) + .jsonColumn(getJsonSchemaColumn()) + .insertInto(getVocabularyTable()); + } + + @Override + public String getUpdateVocabularyTemplate() { + return executeStatement() + .column(getNameColumn()) + .jsonColumn(getJsonSchemaColumn()) + .update(getVocabularyTable(), getVocabularyIdColumn()); + } + + @Override + public String getCountVocabularyByIdClause() { + return format("SELECT COUNT(*) AS %s FROM %s WHERE %s = ?", + getCountVariableName(), + getVocabularyTable(), + getVocabularyIdColumn()); + } + + @Override + public String getSelectVocabularyTemplate() { + return format("SELECT * FROM %s AS a", getVocabularyTable()); + } + + @Override + public String getDeleteVocabularyByIdTemplate() { + return executeStatement() + .delete(getVocabularyTable(), getVocabularyIdColumn()); + } + + @Override + public String getCountVariableName() { + return "COUNT"; + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + return new SqlQueryStatement(getSelectVocabularyTemplate(), querySpec, new VocabularyMapping(this), operatorTranslator); + } + +} diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/VocabularyStatements.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/VocabularyStatements.java new file mode 100644 index 0000000..fece7d0 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/VocabularyStatements.java @@ -0,0 +1,79 @@ +package org.upm.inesdata.vocabulary.sql.index.schema; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.statement.SqlStatements; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +/** + * Defines queries used by the SqlVocabularyIndexServiceExtension. + */ +@ExtensionPoint +public interface VocabularyStatements extends SqlStatements { + + /** + * The vocabulary table name. + */ + default String getVocabularyTable() { + return "edc_vocabulary"; + } + + /** + * The vocabulary table ID column. + */ + default String getVocabularyIdColumn() { + return "id"; + } + + default String getNameColumn() { + return "name"; + } + + default String getJsonSchemaColumn() { + return "json_schema"; + } + + default String getCreatedAtColumn() { + return "created_at"; + } + + + /** + * INSERT clause for vocabularys. + */ + String getInsertVocabularyTemplate(); + + /** + * UPDATE clause for vocabularys. + */ + String getUpdateVocabularyTemplate(); + + /** + * SELECT COUNT clause for vocabularys. + */ + String getCountVocabularyByIdClause(); + + /** + * SELECT clause for all vocabularys. + */ + String getSelectVocabularyTemplate(); + + /** + * DELETE clause for vocabularys. + */ + String getDeleteVocabularyByIdTemplate(); + + /** + * The COUNT variable used in SELECT COUNT queries. + */ + String getCountVariableName(); + + /** + * Generates a SQL query using sub-select statements out of the query spec. + * + * @param query querySpec to be executed + * @return A {@link SqlQueryStatement} that contains the SQL and statement parameters + */ + SqlQueryStatement createQuery(QuerySpec query); + +} diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/PostgresDialectStatements.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 0000000..4a076f2 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,17 @@ +package org.upm.inesdata.vocabulary.sql.index.schema.postgres; + +import org.upm.inesdata.vocabulary.sql.index.schema.BaseSqlDialectStatements; +import org.eclipse.edc.sql.dialect.PostgresDialect; +import org.eclipse.edc.sql.translation.PostgresqlOperatorTranslator; + +public class PostgresDialectStatements extends BaseSqlDialectStatements { + + public PostgresDialectStatements() { + super(new PostgresqlOperatorTranslator()); + } + + @Override + public String getFormatAsJsonOperator() { + return PostgresDialect.getJsonCastOperator(); + } +} diff --git a/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/VocabularyMapping.java b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/VocabularyMapping.java new file mode 100644 index 0000000..9df69b4 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/java/org/upm/inesdata/vocabulary/sql/index/schema/postgres/VocabularyMapping.java @@ -0,0 +1,20 @@ +package org.upm.inesdata.vocabulary.sql.index.schema.postgres; + +import org.eclipse.edc.sql.translation.JsonFieldTranslator; +import org.eclipse.edc.sql.translation.TranslationMapping; +import org.upm.inesdata.vocabulary.sql.index.schema.VocabularyStatements; + +/** + * Maps fields of a {@link org.eclipse.edc.spi.types.domain.vocabulary.Vocabulary} onto the + * corresponding SQL schema (= column names) enabling access through Postgres JSON operators where applicable + */ +public class VocabularyMapping extends TranslationMapping { + + public VocabularyMapping(VocabularyStatements statements) { + add("id", statements.getVocabularyIdColumn()); + add("createdAt", statements.getCreatedAtColumn()); + add("name", statements.getNameColumn()); + add("json_schema", new JsonFieldTranslator(statements.getJsonSchemaColumn())); + } + +} diff --git a/extensions/vocabulary-index-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/vocabulary-index-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..897b043 --- /dev/null +++ b/extensions/vocabulary-index-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +org.upm.inesdata.vocabulary.sql.index.SqlVocabularyIndexServiceExtension \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1c9346..9ec91d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,15 +4,17 @@ format.version = "1.1" [versions] assertj = "3.25.3" aws = "2.23.12" -edc = "0.6.2" +edc = "0.6.3" junit-pioneer = "2.2.0" jupiter = "5.10.2" mockito = "5.2.0" postgres = "42.7.3" -rsApi = "3.1.0" +rsApi = "4.0.0" +swagger-annotations-jakarta = "2.2.21" [libraries] edc-api-core = { module = "org.eclipse.edc:api-core", version.ref = "edc" } +edc-api-management-config = { module = "org.eclipse.edc:management-api-configuration", version.ref = "edc" } edc-auth-spi = { module = "org.eclipse.edc:api-core", version.ref = "edc" } edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } edc-build-plugin = { module = "org.eclipse.edc.edc-build:org.eclipse.edc.edc-build.gradle.plugin", version.ref = "edc" } @@ -29,19 +31,31 @@ edc-data-plane-public-api = { module = "org.eclipse.edc:data-plane-public-api", edc-data-plane-selector-api = { module = "org.eclipse.edc:data-plane-selector-api", version.ref = "edc" } edc-data-plane-selector-core = { module = "org.eclipse.edc:data-plane-selector-core", version.ref = "edc" } edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } +edc-dsp-api-configuration = { module = "org.eclipse.edc:dsp-http-api-configuration", version.ref = "edc" } edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } edc-iam-oauth2-core = { module = "org.eclipse.edc:oauth2-core", version.ref = "edc" } edc-iam-oauth2-service = { module = "org.eclipse.edc:oauth2-service", version.ref = "edc" } +edc-json-ld-lib = { module = "org.eclipse.edc:json-ld-lib", version.ref = "edc" } edc-management-api = { module = "org.eclipse.edc:management-api", version.ref = "edc" } edc-micrometer-core = { module = "org.eclipse.edc:micrometer-core", version.ref = "edc" } +edc-spi-core = { module = "org.eclipse.edc:core-spi", version.ref = "edc" } +edc-spi-jsonld = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" } +edc-spi-transform = { module = "org.eclipse.edc:transform-spi", version.ref = "edc" } +edc-transaction-local = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } +edc-transaction-datasource-spi = { module = "org.eclipse.edc:transaction-datasource-spi", version.ref = "edc" } +edc-transaction-spi = { module = "org.eclipse.edc:transaction-spi", version.ref = "edc" } edc-transfer-data-plane = { module = "org.eclipse.edc:transfer-data-plane", version.ref = "edc" } edc-transfer-pull-http-receiver = { module = "org.eclipse.edc:transfer-pull-http-receiver", version.ref = "edc" } edc-vault-filesystem = { module = "org.eclipse.edc:vault-filesystem", version.ref = "edc" } +edc-validator-spi = { module = "org.eclipse.edc:validator-spi", version.ref = "edc" } edc-web-spi = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "rsApi" } +edc-lib-transform = { module = "org.eclipse.edc:transform-lib", version.ref = "edc" } +edc-lib-util = { module = "org.eclipse.edc:util-lib", version.ref = "edc" } +edc-lib-validator = { module = "org.eclipse.edc:validator-lib", version.ref = "edc" } + # SQL Extensions -edc-transaction-local = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-sql-core = { module = "org.eclipse.edc:sql-core", version.ref = "edc" } edc-sql-edr = { module = "org.eclipse.edc:edr-index-sql", version.ref = "edc" } edc-sql-lease = { module = "org.eclipse.edc:sql-lease", version.ref = "edc" } @@ -72,6 +86,7 @@ edc-core-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } aws-s3 = { module = "software.amazon.awssdk:s3", version.ref = "aws" } aws-s3-transfer = { module = "software.amazon.awssdk:s3-transfer-manager", version.ref = "aws" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } +swagger-annotations-jakarta = { module = "io.swagger.core.v3:swagger-annotations-jakarta", version.ref = "swagger-annotations-jakarta" } # No EDC testing references assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } diff --git a/launchers/connector/build.gradle.kts b/launchers/connector/build.gradle.kts index 4aa2578..726afe4 100644 --- a/launchers/connector/build.gradle.kts +++ b/launchers/connector/build.gradle.kts @@ -36,6 +36,9 @@ dependencies { implementation(libs.edc.transfer.pull.http.receiver) implementation(libs.edc.transfer.data.plane) + // Vocabularios + implementation(project(":extensions:vocabulary-api")) + // Persistencia comun implementation(libs.edc.sql.core) implementation(libs.edc.sql.edr) @@ -47,6 +50,7 @@ dependencies { implementation(libs.edc.sql.contract.negotiation.store) implementation(libs.edc.sql.policy.definition.store) implementation(libs.edc.sql.transfer.process.store) + implementation(project(":extensions:vocabulary-index-sql")) // Persistencia data plane implementation(libs.edc.sql.data.plane.store) diff --git a/resources/sql/vocabulary-schema.sql b/resources/sql/vocabulary-schema.sql new file mode 100644 index 0000000..e6bd609 --- /dev/null +++ b/resources/sql/vocabulary-schema.sql @@ -0,0 +1,11 @@ +-- table: edc_vocabulary +CREATE TABLE IF NOT EXISTS edc_vocabulary +( + id VARCHAR NOT NULL, + created_at BIGINT NOT NULL, + json_schema JSON DEFAULT '{}', + name VARCHAR NOT NULL, + PRIMARY KEY (id) +); + +COMMENT ON COLUMN edc_vocabulary.json_schema IS 'JSON Schema with the vocabulary'; diff --git a/settings.gradle.kts b/settings.gradle.kts index cb5f866..2375bba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,9 +5,14 @@ pluginManagement { rootProject.name = "inesdata-connector" +// SPI +include(":spi:vocabulary-spi") + // Extensions include(":extensions:participants-from-configuration") include(":extensions:auth-oauth2-jwt") +include(":extensions:vocabulary-api") +include(":extensions:vocabulary-index-sql") // Connector include(":launchers:connector") diff --git a/spi/vocabulary-spi/README.md b/spi/vocabulary-spi/README.md new file mode 100644 index 0000000..a17be64 --- /dev/null +++ b/spi/vocabulary-spi/README.md @@ -0,0 +1 @@ +This module contains extension points and interfaces specifically for the Inesdata Vocabulary feature. \ No newline at end of file diff --git a/spi/vocabulary-spi/build.gradle.kts b/spi/vocabulary-spi/build.gradle.kts new file mode 100644 index 0000000..5f7ff12 --- /dev/null +++ b/spi/vocabulary-spi/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `java-library` + id("com.gmv.inesdata.edc-application") +} + +dependencies { + api(libs.edc.spi.core) +} diff --git a/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyIndex.java b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyIndex.java new file mode 100644 index 0000000..4bf45b4 --- /dev/null +++ b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyIndex.java @@ -0,0 +1,61 @@ +package org.upm.inesdata.spi.vocabulary; + +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.stream.Stream; + +/** + * Datastore interface for {@link Vocabulary} objects. + */ +@ExtensionPoint +public interface VocabularyIndex { + + String VOCABULARY_EXISTS_TEMPLATE = "Vocabulary with ID %s already exists"; + String VOCABULARY_NOT_FOUND_TEMPLATE = "Vocabulary with ID %s not found"; + + /** + * Finds all stored vocabularies + * + * @return A potentially empty collection of {@link Vocabulary}, never null. + */ + Stream allVocabularies(); + + /** + * Fetches the {@link Vocabulary} with the given ID from the metadata backend. + * + * @param vocabularyId A String that represents the Vocabulary ID, in most cases this will be a UUID. + * @return The {@link Vocabulary} if one was found, or null otherwise. + * @throws NullPointerException If {@code vocabularyId} was null or empty. + */ + Vocabulary findById(String vocabularyId); + + /** + * Stores a {@link Vocabulary} in the vocabulary index, if no vocabulary with the same ID already exists. + * Implementors must ensure that it's stored in a transactional way. + * + * @param vocabulary The {@link Vocabulary} to store + * @return {@link StoreResult#success()} if the objects were stored, {@link StoreResult#alreadyExists(String)} when an object with the same ID already exists. + */ + StoreResult create(Vocabulary vocabulary); + + /** + * Deletes a vocabulary if it exists. + * + * @param vocabularyId Id of the vocabulary to be deleted. + * @return {@link StoreResult#success(Object)} if the object was deleted, {@link StoreResult#notFound(String)} when an object with that ID was not found. + * @throws EdcPersistenceException if something goes wrong. + */ + StoreResult deleteById(String vocabularyId); + + /** + * Updates a vocabulary with the content from the given {@link Vocabulary}. If the vocabulary is not found, no further database interaction takes place. + * + * @param vocabulary The Vocabulary containing the new values. ID will be ignored. + * @return {@link StoreResult#success(Object)} if the object was updated, {@link StoreResult#notFound(String)} when an object with that ID was not found. + */ + StoreResult updateVocabulary(Vocabulary vocabulary); + +} diff --git a/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyService.java b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyService.java new file mode 100644 index 0000000..fccf217 --- /dev/null +++ b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/VocabularyService.java @@ -0,0 +1,53 @@ +package org.upm.inesdata.spi.vocabulary; + +import org.upm.inesdata.spi.vocabulary.domain.Vocabulary; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.util.List; + +/** + * Service interface for {@link Vocabulary} objects. + */ +public interface VocabularyService { + + /** + * Returns a vocabulary by its id + * + * @param vocabularyId id of the vocabulary + * @return the vocabulary, null if it's not found + */ + Vocabulary findById(String vocabularyId); + + /** + * Get Vocabularys + * + * @return the collection of vocabularys stored + */ + ServiceResult> search(); + + /** + * Create a vocabulary + * + * @param vocabulary the vocabulary + * @return successful result if the vocabulary is created correctly, failure otherwise + */ + ServiceResult create(Vocabulary vocabulary); + + /** + * Delete a vocabulary + * + * @param vocabularyId the id of the vocabulary to be deleted + * @return successful result if the vocabulary is deleted correctly, failure otherwise + */ + ServiceResult delete(String vocabularyId); + + /** + * Updates a vocabulary. If the vocabulary does not yet exist, {@link ServiceResult#notFound(String)} will be returned. + * + * @param vocabulary The content of the Vocabulary. Note that {@link Vocabulary#getId()} will be ignored, rather the separately supplied ID is used + * @return successful if updated, a failure otherwise. + */ + ServiceResult update(Vocabulary vocabulary); + +} + \ No newline at end of file diff --git a/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/domain/Vocabulary.java b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/domain/Vocabulary.java new file mode 100644 index 0000000..b2f25ae --- /dev/null +++ b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/domain/Vocabulary.java @@ -0,0 +1,100 @@ +package org.upm.inesdata.spi.vocabulary.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.spi.entity.Entity; + +import java.util.Objects; +import java.util.UUID; + +import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; + + +/** + * The {@link Vocabulary} contains the vocabylary information. + */ +@JsonDeserialize(builder = Vocabulary.Builder.class) +public class Vocabulary extends Entity { + + public static final String PROPERTY_ID = EDC_NAMESPACE + "id"; + public static final String PROPERTY_NAME = EDC_NAMESPACE + "name"; + public static final String PROPERTY_JSON_SCHEMA = EDC_NAMESPACE + "jsonSchema"; + public static final String EDC_VOCABULARY_TYPE = EDC_NAMESPACE + "Vocabulary"; + + + private String name; + private String jsonSchema; + + private Vocabulary() { + } + + public String getName() { + return name; + } + + public String getJsonSchema() { + return jsonSchema; + } + + public Builder toBuilder() { + return Vocabulary.Builder.newInstance() + .id(id) + .name(name) + .jsonSchema(jsonSchema) + .createdAt(createdAt); + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder extends Entity.Builder { + + protected Builder(Vocabulary vocabulary) { + super(vocabulary); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(new Vocabulary()); + } + + @Override + public Builder id(String id) { + entity.id = id; + return self(); + } + + public Builder name(String name) { + entity.name = name; + return self(); + } + + public Builder jsonSchema(String jsonSchema) { + Objects.requireNonNull(jsonSchema); + entity.jsonSchema = jsonSchema; + return self(); + } + + @Override + public Builder createdAt(long value) { + entity.createdAt = value; + return self(); + } + + @Override + public Builder self() { + return this; + } + + @Override + public Vocabulary build() { + super.build(); + + if (entity.getId() == null) { + id(UUID.randomUUID().toString()); + } + + return entity; + } + } + +} diff --git a/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/package-info.java b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/package-info.java new file mode 100644 index 0000000..69fc60f --- /dev/null +++ b/spi/vocabulary-spi/src/main/java/org/upm/inesdata/spi/vocabulary/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 - 2022 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation + * + */ + +@Spi("Vocabulary services") +package org.upm.inesdata.spi.vocabulary; + +import org.eclipse.edc.runtime.metamodel.annotation.Spi; diff --git a/suppressions.xml b/suppressions.xml index 0aebd28..97081c8 100644 --- a/suppressions.xml +++ b/suppressions.xml @@ -2,9 +2,10 @@ - ^pkg:maven/org\.eclipse\.edc/.*sql.*@.*$ + ^pkg:maven/.*sql.*@.*$ cpe:/a:www-sql_project:www-sql @@ -22,4 +23,4 @@ cpe:/a:jetty:jetty cpe:/a:eclipse:jetty - \ No newline at end of file +