From 1a3b6ebca6e9ddf7c4af43c660ab0d9a4417d232 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 27 Feb 2025 17:26:51 +0100 Subject: [PATCH 01/23] merge feature/data-resource-history (cherry picked from commit cd989e9a5a1498321b80cbe5f1ed2b2655f28e54) --- .gitignore | 1 + app/pom.xml | 81 ++- .../scl/data/config/FeatureFlagService.java | 21 + .../data/rest/api/scl/HistoryResource.java | 131 ++++ .../rest/monitoring/ReadinessHealthCheck.java | 13 +- .../data/rest/v1/CompasSclDataResource.java | 27 +- app/src/main/openapi/archiving-api.yaml | 560 ++++++++++++++++++ app/src/main/openapi/data-resource-mockup.png | Bin 0 -> 107547 bytes app/src/main/openapi/history-api.yaml | 270 +++++++++ app/src/main/resources/application.properties | 10 +- .../rest/api/scl/HistoryResourceTest.java | 122 ++++ .../scl/HistoryResourceTestdataBuilder.java | 77 +++ .../monitoring/LivenessHealthCheckIT.java | 12 + .../monitoring/LivenessHealthCheckTest.java | 24 + .../monitoring/ReadinessHealthCheckIT.java | 12 + .../monitoring/ReadinessHealthCheckTest.java | 63 ++ .../v1/CompasSclDataResourceAsEditorTest.java | 26 +- .../v1/CompasSclDataResourceAsReaderTest.java | 10 +- docker-compose.yml | 58 ++ .../scl/data/model/HistoryMetaItem.java | 59 ++ .../CompasSclDataPostgreSQLRepository.java | 311 +++++++++- .../db/migration/V1_5__create_scl_history.sql | 32 + .../V1_6__scl_file_add_is_deleted.sql | 5 + .../test/resources/scl_history_testdata.sql | 30 + .../scl/data/model/IHistoryMetaItem.java | 24 + .../repository/CompasSclDataRepository.java | 62 +- .../data/service/CompasSclDataService.java | 178 +++++- .../scl/data/util/SclElementProcessor.java | 10 +- .../service/CompasSclDataServiceTest.java | 33 +- .../data/util/SclElementProcessorTest.java | 8 +- 30 files changed, 2176 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/config/FeatureFlagService.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java create mode 100644 app/src/main/openapi/archiving-api.yaml create mode 100644 app/src/main/openapi/data-resource-mockup.png create mode 100644 app/src/main/openapi/history-api.yaml create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTest.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckIT.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckTest.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckIT.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckTest.java create mode 100644 docker-compose.yml create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_6__scl_file_add_is_deleted.sql create mode 100644 repository-postgresql/src/test/resources/scl_history_testdata.sql create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java diff --git a/.gitignore b/.gitignore index ca6f1e4c..695f7bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ target out lib bin +data .java-version *.orig *.rej diff --git a/app/pom.xml b/app/pom.xml index 2a57c86f..c5b0ba52 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -63,6 +63,10 @@ SPDX-License-Identifier: Apache-2.0 io.quarkus quarkus-resteasy-reactive-jaxb + + io.quarkus + quarkus-resteasy-reactive-jackson + io.quarkus quarkus-websockets @@ -191,6 +195,79 @@ SPDX-License-Identifier: Apache-2.0 + + org.openapitools + openapi-generator-maven-plugin + 7.8.0 + + + generate-history-api + + generate + + + ${project.basedir}/src/main/openapi/history-api.yaml + jaxrs-spec + ${project.build.directory}/generated-sources/openapi + quarkus + org.lfenergy.compas.scl.data.rest.api.scl + org.lfenergy.compas.scl.data.rest.api.scl.model + false + + true + true + java8 + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/openapi/src/gen/java + + + + + + org.apache.maven.plugins maven-surefire-plugin @@ -201,6 +278,7 @@ SPDX-License-Identifier: Apache-2.0 + @@ -242,7 +320,6 @@ SPDX-License-Identifier: Apache-2.0 native-image - true @@ -267,7 +344,6 @@ SPDX-License-Identifier: Apache-2.0 sonar - target/jacoco-report/jacoco.xml, @@ -278,7 +354,6 @@ SPDX-License-Identifier: Apache-2.0 release - true diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/config/FeatureFlagService.java b/app/src/main/java/org/lfenergy/compas/scl/data/config/FeatureFlagService.java new file mode 100644 index 00000000..84e9582a --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/config/FeatureFlagService.java @@ -0,0 +1,21 @@ +package org.lfenergy.compas.scl.data.config; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class FeatureFlagService { + + @ConfigProperty(name = "scl-data-service.features.is-history-enabled", defaultValue = "true") + boolean isHistoryEnabled; + @ConfigProperty(name = "scl-data-service.features.keep-deleted-files", defaultValue = "true") + boolean keepDeletedFiles; + + public boolean isHistoryEnabled() { + return isHistoryEnabled; + } + + public boolean keepDeletedFiles() { + return keepDeletedFiles; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java new file mode 100644 index 00000000..bba08af5 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java @@ -0,0 +1,131 @@ +package org.lfenergy.compas.scl.data.rest.api.scl; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; +import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.rest.api.scl.model.*; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; +import org.lfenergy.compas.scl.extensions.model.SclFileType; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.rmi.UnexpectedException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +@RequestScoped +public class HistoryResource implements HistoryApi { + private static final Logger LOGGER = LogManager.getLogger(HistoryResource.class); + + private final CompasSclDataService compasSclDataService; + + @Inject + public HistoryResource(CompasSclDataService compasSclDataService) { + this.compasSclDataService = compasSclDataService; + } + + @Override + public Uni searchForResources(DataResourceSearch dataResourceSearch) { + LOGGER.info("Triggering search with filter: {}", dataResourceSearch); + return Uni.createFrom() + .item(() -> getHistoryMetaItems(dataResourceSearch)) + .onItem().transform(items -> new DataResourcesResult().results(items.stream().map(this::mapToDataResource).toList())) + .onFailure().recoverWithItem(e -> { + LOGGER.error("Unexpected error while searching for resources", e); + return new DataResourcesResult(); // Return an empty result or handle as needed + }); + } + + private List getHistoryMetaItems(DataResourceSearch dataResourceSearch) { + String uuid = dataResourceSearch.getUuid(); + + if (uuid != null) { + return compasSclDataService.listHistory(UUID.fromString(uuid)); + } + + SclFileType type = dataResourceSearch.getType() != null ? SclFileType.valueOf(dataResourceSearch.getType()) : null; + String name = dataResourceSearch.getName(); + String author = dataResourceSearch.getAuthor(); + OffsetDateTime from = dataResourceSearch.getFrom(); + OffsetDateTime to = dataResourceSearch.getTo(); + + if (type != null || name != null || author != null || from != null || to != null) { + return compasSclDataService.listHistory(type, name, author, from, to); + } + + return compasSclDataService.listHistory(); + } + + private DataResource mapToDataResource(IHistoryMetaItem e) { + return new DataResource() + .uuid(UUID.fromString(e.getId())) + .name(e.getName()) + .author(e.getAuthor()) + .type(e.getType()) + .changedAt(e.getChangedAt()) + .version(e.getVersion()) + .available(e.isAvailable()) + .deleted(e.isDeleted()); + } + + @Override + public Uni retrieveDataResourceHistory(UUID id) { + LOGGER.info("Retrieving history for data resource ID: {}", id); + return Uni.createFrom() + .item(() -> compasSclDataService.listHistoryVersionsByUUID(id)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem().transform(versions -> new DataResourceHistory().versions(versions.stream().map(this::mapToDataResourceVersion).toList())); + } + + private DataResourceVersion mapToDataResourceVersion(IHistoryMetaItem e) { + return new DataResourceVersion() + .uuid(UUID.fromString(e.getId())) + .name(e.getName()) + .author(e.getAuthor()) + .type(e.getType()) + .changedAt(e.getChangedAt()) + .version(e.getVersion()) + .available(e.isAvailable()) + .deleted(e.isDeleted()) + .comment(e.getComment()) + .archived(e.isArchived()); + + } + + @Override + public Uni retrieveDataResourceByVersion(UUID id, String version) { + LOGGER.info("Retrieving data resource for ID: {} and version: {}", id, version); + return Uni.createFrom() + .item(() -> compasSclDataService.findByUUID(id, new Version(version))) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem().transformToUni(this::createTempFileWithData) + .onFailure().transform(e -> { + LOGGER.error("Failed to retrieve or create temp file", e); + return new UnexpectedException("Error while retrieving data resource", (Exception) e); + }); + } + + private Uni createTempFileWithData(String data) { + return Uni.createFrom() + .item(() -> createTempFile(data)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()); + } + + private File createTempFile(String data) { + try { + Path tempFile = Files.createTempFile("resource_", ".tmp"); + Files.writeString(tempFile, data); + return tempFile.toFile(); + } catch (IOException e) { + throw new RuntimeException("Error creating or writing to temp file", e); + } + } +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheck.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheck.java index f082bfed..89832c22 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheck.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheck.java @@ -3,17 +3,28 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.rest.monitoring; +import jakarta.inject.Inject; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Readiness; import jakarta.enterprise.context.ApplicationScoped; +import org.lfenergy.compas.scl.data.config.FeatureFlagService; @Readiness @ApplicationScoped public class ReadinessHealthCheck implements HealthCheck { + + @Inject + FeatureFlagService featureFlagService; + @Override public HealthCheckResponse call() { - return HealthCheckResponse.up("System Ready"); + + return HealthCheckResponse + .named("System Ready") + .up() + .withData("isHistoryEnabled", featureFlagService.isHistoryEnabled()) + .build(); } } \ No newline at end of file diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResource.java index fba0497c..82dba724 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResource.java @@ -6,20 +6,21 @@ import io.quarkus.security.Authenticated; import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.lfenergy.compas.scl.data.config.FeatureFlagService; import org.lfenergy.compas.scl.data.model.Version; import org.lfenergy.compas.scl.data.rest.UserInfoProperties; import org.lfenergy.compas.scl.data.rest.v1.model.*; import org.lfenergy.compas.scl.data.service.CompasSclDataService; import org.lfenergy.compas.scl.extensions.model.SclFileType; -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; import java.util.UUID; import static org.lfenergy.compas.scl.data.rest.Constants.*; @@ -38,6 +39,9 @@ public class CompasSclDataResource { @Inject UserInfoProperties userInfoProperties; + @Inject + FeatureFlagService featureFlagService; + @Inject public CompasSclDataResource(CompasSclDataService compasSclDataService) { this.compasSclDataService = compasSclDataService; @@ -54,8 +58,9 @@ public Uni create(@PathParam(TYPE_PATH_PARAM) SclFileType type, LOGGER.trace("Username used for Who {}", who); var response = new CreateResponse(); - response.setSclData(compasSclDataService.create(type, request.getName(), who, request.getComment(), - request.getSclData())); + response.setSclData(compasSclDataService.create( + type, request.getName(), who, request.getComment(), request.getSclData(), featureFlagService.isHistoryEnabled() + )); return Uni.createFrom().item(response); } @@ -117,7 +122,7 @@ public Uni update(@PathParam(TYPE_PATH_PARAM) SclFileType type, var response = new UpdateResponse(); response.setSclData(compasSclDataService.update(type, id, request.getChangeSetType(), who, request.getComment(), - request.getSclData())); + request.getSclData(), featureFlagService.isHistoryEnabled())); return Uni.createFrom().item(response); } @@ -128,7 +133,7 @@ public Uni update(@PathParam(TYPE_PATH_PARAM) SclFileType type, public Uni deleteAll(@PathParam(TYPE_PATH_PARAM) SclFileType type, @PathParam(ID_PATH_PARAM) UUID id) { LOGGER.info("Removing all versions of SCL File {} for type {} from storage.", id, type); - compasSclDataService.delete(type, id); + compasSclDataService.delete(type, id, featureFlagService.keepDeletedFiles()); return Uni.createFrom().nullItem(); } @@ -140,7 +145,7 @@ public Uni deleteVersion(@PathParam(TYPE_PATH_PARAM) SclFileType type, @PathParam(ID_PATH_PARAM) UUID id, @PathParam(VERSION_PATH_PARAM) Version version) { LOGGER.info("Removing version {} of SCL File {} for type {} from storage.", version, id, type); - compasSclDataService.delete(type, id, version); + compasSclDataService.deleteVersion(type, id, version, featureFlagService.keepDeletedFiles()); return Uni.createFrom().nullItem(); } @@ -149,7 +154,7 @@ public Uni deleteVersion(@PathParam(TYPE_PATH_PARAM) SclFileType type, @Consumes(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML) public Uni checkDuplicateName(@PathParam(TYPE_PATH_PARAM) SclFileType type, - @Valid DuplicateNameCheckRequest request) { + @Valid DuplicateNameCheckRequest request) { LOGGER.info("Checking for duplicate SCL File name."); var response = new DuplicateNameCheckResponse(); diff --git a/app/src/main/openapi/archiving-api.yaml b/app/src/main/openapi/archiving-api.yaml new file mode 100644 index 00000000..78df9315 --- /dev/null +++ b/app/src/main/openapi/archiving-api.yaml @@ -0,0 +1,560 @@ +openapi: 3.0.3 +info: + title: CoMPAS SCL Data Service History API + version: 1.0.0 + +servers: + - url: https://demo.compas.energy + description: DSOM Versatel Production URL + +tags: + - name: locations + description: Endpoints managing locations and assigning resources + - name: archiving + description: Archiving related endpoints + +security: + - open-id-connect: + - read + - write + - admin + +paths: + /api/locations: + post: + tags: + - locations + summary: Create location + security: + - open-id-connect: [ admin ] + operationId: createLocation + requestBody: + description: Location information + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + responses: + '200': + description: Successfully generated location + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + get: + tags: + - locations + summary: Retrieve locations + operationId: getLocations + parameters: + - name: page + in: query + description: Page number starting by 1 + required: false + schema: + type: integer + format: int32 + - name: pageSize + in: query + description: Page size must be > 0 + required: false + schema: + type: integer + format: int32 + default: 25 + responses: + '200': + description: Successfully retrieved locations + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Location' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + /api/locations/{locationId}: + parameters: + - name: locationId + in: path + description: Unique location identifier + required: true + schema: + type: string + format: uuid + get: + tags: + - locations + summary: Retrieve location + operationId: getLocation + responses: + '200': + description: Successfully retrieved location + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location with locationId + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + put: + tags: + - locations + summary: Update location + operationId: updateLocation + requestBody: + description: Location information, location uuid will be ignored + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + responses: + '200': + description: Successfully generated location + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + delete: + tags: + - locations + summary: Delete location + description: Deleting a location is only allowed when location has no resources assigned + operationId: deleteLocation + responses: + '204': + description: Successfully deleted location + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location with locationId + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + /api/locations/{locationId}/resources/{uuid}/assign: + post: + tags: + - locations + summary: Assigns a resource + security: + - open-id-connect: [ write ] + description: |- + Assigns or moves a resource to the specified location. If resource already assigned, the previous assignment + will be removed. + operationId: assignResourceToLocation + parameters: + - name: locationId + in: path + description: Unique location identifier + required: true + schema: + type: string + format: uuid + - name: uuid + in: path + description: Unique resource identifier + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Successfully assigned the resource to the location + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location or resource + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + /api/locations/{locationId}/resources/{uuid}/unassign: + post: + tags: + - locations + summary: Unassigns a resource + security: + - open-id-connect: [ write ] + description: |- + Removes the assignment of a resource from the assigned location. + operationId: unassignResourceToLocation + parameters: + - name: locationId + in: path + description: Unique location identifier + required: true + schema: + type: string + format: uuid + - name: uuid + in: path + description: Unique resource identifier + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Successfully unassigned the resource from the location + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location or resource + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + + /api/archive/scl/{id}/versions/{version}: + parameters: + - name: id + in: path + description: Unique data resource identifier + required: true + schema: + type: string + format: uuid + - name: version + in: path + description: Data resource version + required: true + schema: + type: string + post: + tags: + - archiving + summary: Archive an existing scl file + security: + - open-id-connect: [ write ] + operationId: archiveSclResource + responses: + '200': + description: Successfully generated location + content: + application/json: + schema: + $ref: '#/components/schemas/ArchivedResource' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '403': + description: Authorization failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location or resource + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + /api/archive/referenced-resource/{id}/versions/{version}: + post: + tags: + - archiving + summary: Archive resource linked to existing resource + security: + - open-id-connect: [ write ] + operationId: archiveResource + parameters: + - in: header + name: X-author + description: Name of the author who created the file and send for approval + schema: + type: string + - in: header + name: X-approver + description: Name of the approver + schema: + type: string + - in: header + name: Content-Type + description: File content type + schema: + type: string + - in: header + name: X-filename + description: File name + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: Successfully generated location + content: + application/json: + schema: + $ref: '#/components/schemas/ArchivedResource' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '403': + description: Authorization failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location or resource + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + +components: + securitySchemes: + open-id-connect: # <--- Arbitrary name for the security scheme. Used to refer to it from elsewhere. + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + + schemas: + Locations: + type: object + required: + - locations + properties: + locations: + type: array + description: "List of locations" + items: + $ref: '#/components/schemas/Location' + pagination: + $ref: '#/components/schemas/Pagination' + Pagination: + type: object + required: + - page + - pageSize + properties: + page: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + Location: + type: object + required: + - key + - name + properties: + uuid: + type: string + description: "Unique location uuid generated by backend during creation" + key: + type: string + description: "Location key, defined once manually when creating a location" + name: + type: string + description: "Location name" + description: + type: string + description: "Location description" + ArchivedResource: + type: object + required: + - uuid + - name + - contentType + - version + - fields + properties: + uuid: + type: string + description: "Unique resource identifier" + name: + type: string + description: "Resource name" + contentType: + type: string + description: "Content type" + version: + type: string + description: "Version" + fields: + type: array + items: + $ref: '#/components/schemas/ResourceTag' + ResourceTag: + type: object + required: + - key + - value + properties: + key: + type: string + description: "Tag key" + value: + type: string + description: "Tag value" + ErrorResponseDto: + required: + - timestamp + - code + - message + type: object + properties: + timestamp: + type: string + description: 2017-07-21T17:32:28Z. + format: 'date-time' + code: + type: string + example: RESOURCE_NOT_FOUND + message: + type: string + example: Unable to find resource with id 'xy'. \ No newline at end of file diff --git a/app/src/main/openapi/data-resource-mockup.png b/app/src/main/openapi/data-resource-mockup.png new file mode 100644 index 0000000000000000000000000000000000000000..f2978dff45610e4d6609813f4f7e1e82d0e048d6 GIT binary patch literal 107547 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 testData = new ArrayList<>(); + testData.add(new HistoryResourceTestdataBuilder().build()); + + when(compasSclDataService.listHistory()).thenReturn(testData); + + var response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(search) + .when().post("/search") + .then() + .statusCode(200) + .extract() + .response(); + + var dataResourcesResult = response.as(DataResourcesResult.class); + assertNotNull(dataResourcesResult); + } + + @Test + void searchForResourcesWithUUIDFilter_WhenCalled_ThenReturnsOnlyMatchingData() { + var uuid = UUID.randomUUID(); + var search = new DataResourceSearch(); + search.setUuid(uuid.toString()); + + List testData = new ArrayList<>(); + testData.add(new HistoryResourceTestdataBuilder().setId(uuid.toString()).build()); + + when(compasSclDataService.listHistory(uuid)).thenReturn(testData); + + var response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(search) + .when().post("/search") + .then() + .statusCode(200) + .extract() + .response(); + + var dataResourcesResult = response.as(DataResourcesResult.class); + assertNotNull(dataResourcesResult); + assertEquals(1, dataResourcesResult.getResults().size(), "Expected exactly 1 matching result"); + } + + @Test + void retrieveDataResourceHistory_WhenCalled_ThenReturnsEmptyDataResourceHistory() { + var uuid = UUID.randomUUID(); + List testData = new ArrayList<>(); + testData.add(new HistoryResourceTestdataBuilder().setId(uuid.toString()).build()); + + when(compasSclDataService.listHistoryVersionsByUUID(uuid)).thenReturn(testData); + + var response = given() + .pathParam("id", uuid) + .when().get("/{id}/versions") + .then() + .statusCode(200) + .extract() + .response(); + + var dataResourceHistory = response.as(DataResourceHistory.class); + assertNotNull(dataResourceHistory); + } + + @Test + void retrieveDataResourceByVersion_WhenCalled_ThenReturnsHelloWorldBinaryData() { + var id = UUID.randomUUID(); + var version = "1.0.0"; + + when(compasSclDataService.findByUUID(id, new Version(version))).thenReturn("hello world"); + + var response = given() + .pathParam("id", id) + .pathParam("version", version) + .when().get("/{id}/version/{version}") + .then() + .statusCode(200) + .extract() + .response(); + + byte[] responseData = response.asByteArray(); + + assertNotNull(responseData); + String expectedContent = "hello world"; + assertArrayEquals(expectedContent.getBytes(), responseData); + } +} diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java new file mode 100644 index 00000000..4d4d39f1 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java @@ -0,0 +1,77 @@ +package org.lfenergy.compas.scl.data.rest.api.scl; + +import org.lfenergy.compas.scl.data.model.HistoryMetaItem; +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; + +import java.time.OffsetDateTime; +import java.util.UUID; + +public class HistoryResourceTestdataBuilder { + + // Default values + private String id = UUID.randomUUID().toString(); + private String name = "Name"; + private String version = "1.0.0"; + private String type = "SSD"; + private String author = "Test"; + private String comment = "Created"; + private OffsetDateTime changedAt = OffsetDateTime.now(); + private boolean archived = false; + private boolean available = false; + private boolean deleted = false; + + public HistoryResourceTestdataBuilder setId(String id) { + this.id = id; + return this; + } + + public HistoryResourceTestdataBuilder setName(String name) { + this.name = name; + return this; + } + + public HistoryResourceTestdataBuilder setVersion(String version) { + this.version = version; + return this; + } + + public HistoryResourceTestdataBuilder setType(String type) { + this.type = type; + return this; + } + + public HistoryResourceTestdataBuilder setAuthor(String author) { + this.author = author; + return this; + } + + public HistoryResourceTestdataBuilder setComment(String comment) { + this.comment = comment; + return this; + } + + public HistoryResourceTestdataBuilder setChangedAt(OffsetDateTime changedAt) { + this.changedAt = changedAt; + return this; + } + + public HistoryResourceTestdataBuilder setArchived(boolean archived) { + this.archived = archived; + return this; + } + + public HistoryResourceTestdataBuilder setAvailable(boolean available) { + this.available = available; + return this; + } + + public HistoryResourceTestdataBuilder setDeleted(boolean deleted) { + this.deleted = deleted; + return this; + } + + // Build method to create a new HistoryMetaItem object + public IHistoryMetaItem build() { + return new HistoryMetaItem(id, name, version, type, author, comment, changedAt, archived, available, deleted); + } +} diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckIT.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckIT.java new file mode 100644 index 00000000..435d7cc7 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckIT.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2021 Alliander N.V. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.lfenergy.compas.scl.data.rest.monitoring; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class LivenessHealthCheckIT extends LivenessHealthCheckTest { + // Execute the same tests but in native mode. +} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckTest.java new file mode 100644 index 00000000..a5a5f6f5 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/LivenessHealthCheckTest.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2021 Alliander N.V. +// +// SPDX-License-Identifier: Apache-2.0 +package org.lfenergy.compas.scl.data.rest.monitoring; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + + +@QuarkusTest +class LivenessHealthCheckTest { + + @Test + void testLivenessEndpoint() { + given() + .when().get("/q/health/live") + .then() + .statusCode(200); + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckIT.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckIT.java new file mode 100644 index 00000000..015a495c --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckIT.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2021 Alliander N.V. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.lfenergy.compas.scl.data.rest.monitoring; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class ReadinessHealthCheckIT extends ReadinessHealthCheckTest { + // Execute the same tests but in native mode. +} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckTest.java new file mode 100644 index 00000000..d668e2dc --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/monitoring/ReadinessHealthCheckTest.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 Alliander N.V. +// +// SPDX-License-Identifier: Apache-2.0 +package org.lfenergy.compas.scl.data.rest.monitoring; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; +import org.lfenergy.compas.scl.data.config.FeatureFlagService; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@QuarkusTest +class ReadinessHealthCheckTest { + + @InjectMock + private FeatureFlagService featureFlagService; + + @Test + void testReadinessEndpoint() { + given() + .when().get("/q/health/ready") + .then() + .statusCode(200); + } + + @Test + void testReadinessEndpoint_whenCalledWithHistoryFeatureEnabled_ThenRetrieveIsHistoryEnabledTrue() { + // Mock the feature flag to be enabled + when(featureFlagService.isHistoryEnabled()).thenReturn(true); + + var response = given() + .when().get("/q/health/ready") + .then() + .statusCode(200) + .extract() + .response(); + + assertNotNull(response); + assertEquals(response.jsonPath().getString("checks.find { it.name == 'System Ready' }.name"), "System Ready"); + assertEquals(response.jsonPath().getBoolean("checks.find { it.name == 'System Ready' }.data.isHistoryEnabled"), true); + } + + @Test + void testReadinessEndpoint_whenCalledWithHistoryFeatureDisabled_ThenRetrieveIsHistoryEnabledFalse() { + // Mock the feature flag to be disabled + when(featureFlagService.isHistoryEnabled()).thenReturn(false); + + var response = given() + .when().get("/q/health/ready") + .then() + .statusCode(200) + .extract() + .response(); + + assertNotNull(response); + assertEquals(response.jsonPath().getString("checks.find { it.name == 'System Ready' }.name"), "System Ready"); + assertEquals(response.jsonPath().getBoolean("checks.find { it.name == 'System Ready' }.data.isHistoryEnabled"), false); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsEditorTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsEditorTest.java index e25f7798..c776585d 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsEditorTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsEditorTest.java @@ -11,14 +11,15 @@ import io.quarkus.test.security.jwt.JwtSecurity; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; +import org.lfenergy.compas.scl.data.config.FeatureFlagService; import org.lfenergy.compas.scl.data.model.ChangeSetType; -import org.lfenergy.compas.scl.data.xml.HistoryItem; -import org.lfenergy.compas.scl.data.xml.Item; import org.lfenergy.compas.scl.data.model.Version; import org.lfenergy.compas.scl.data.rest.v1.model.CreateRequest; import org.lfenergy.compas.scl.data.rest.v1.model.DuplicateNameCheckRequest; import org.lfenergy.compas.scl.data.rest.v1.model.UpdateRequest; import org.lfenergy.compas.scl.data.service.CompasSclDataService; +import org.lfenergy.compas.scl.data.xml.HistoryItem; +import org.lfenergy.compas.scl.data.xml.Item; import org.lfenergy.compas.scl.extensions.model.SclFileType; import java.io.IOException; @@ -46,6 +47,9 @@ class CompasSclDataResourceAsEditorTest { @InjectMock private CompasSclDataService compasSclDataService; + @InjectMock + private FeatureFlagService featureFlagService; + @Test void list_WhenCalled_ThenItemResponseRetrieved() { var type = SclFileType.SCD; @@ -159,7 +163,7 @@ void create_WhenCalled_ThenServiceCalledAndUUIDRetrieved() throws IOException { request.setComment(comment); request.setSclData(scl); - when(compasSclDataService.create(type, name, USERNAME, comment, scl)).thenReturn(scl); + when(compasSclDataService.create(type, name, USERNAME, comment, scl, false)).thenReturn(scl); var response = given() .pathParam(TYPE_PATH_PARAM, type) @@ -171,8 +175,8 @@ void create_WhenCalled_ThenServiceCalledAndUUIDRetrieved() throws IOException { .extract() .response(); + verify(compasSclDataService).create(type, name, USERNAME, comment, scl, false); assertEquals(scl, response.xmlPath().getString("CreateWsResponse.SclData")); - verify(compasSclDataService).create(type, name, USERNAME, comment, scl); } @Test @@ -215,7 +219,7 @@ void update_WhenCalled_ThenServiceCalledAndNewUUIDRetrieved() throws IOException request.setComment(comment); request.setSclData(scl); - when(compasSclDataService.update(type, uuid, changeSetType, USERNAME, comment, scl)).thenReturn(scl); + when(compasSclDataService.update(type, uuid, changeSetType, USERNAME, comment, scl, false)).thenReturn(scl); var response = given() .pathParam(TYPE_PATH_PARAM, type) @@ -229,7 +233,7 @@ void update_WhenCalled_ThenServiceCalledAndNewUUIDRetrieved() throws IOException .response(); assertEquals(scl, response.xmlPath().getString("CreateWsResponse.SclData")); - verify(compasSclDataService).update(type, uuid, changeSetType, USERNAME, comment, scl); + verify(compasSclDataService).update(type, uuid, changeSetType, USERNAME, comment, scl, false); } @Test @@ -237,7 +241,7 @@ void deleteAll_WhenCalled_ThenServiceCalled() { var uuid = UUID.randomUUID(); var type = SclFileType.SCD; - doNothing().when(compasSclDataService).delete(type, uuid); + doNothing().when(compasSclDataService).delete(type, uuid, false); given() .pathParam(TYPE_PATH_PARAM, type) @@ -246,7 +250,7 @@ void deleteAll_WhenCalled_ThenServiceCalled() { .then() .statusCode(204); - verify(compasSclDataService).delete(type, uuid); + verify(compasSclDataService).delete(type, uuid, false); } @Test @@ -255,7 +259,7 @@ void deleteVersion_WhenCalled_ThenServiceCalled() { var type = SclFileType.SCD; var version = new Version(1, 2, 3); - doNothing().when(compasSclDataService).delete(type, uuid, version); + doNothing().when(compasSclDataService).deleteVersion(type, uuid, version, false); given() .pathParam(TYPE_PATH_PARAM, type) @@ -265,7 +269,7 @@ void deleteVersion_WhenCalled_ThenServiceCalled() { .then() .statusCode(204); - verify(compasSclDataService).delete(type, uuid, version); + verify(compasSclDataService).deleteVersion(type, uuid, version, false); } @Test @@ -320,7 +324,7 @@ private String readSCL() throws IOException { try (var inputStream = getClass().getResourceAsStream("/scl/icd_import_ied_test.scd")) { assert inputStream != null; - return new String(inputStream.readAllBytes()); + return new String(inputStream.readAllBytes()).replace("\r\n", "\n"); } } } diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsReaderTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsReaderTest.java index 9b18ea5c..edb5a2ee 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsReaderTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclDataResourceAsReaderTest.java @@ -3,18 +3,19 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.rest.v1; +import io.quarkus.test.InjectMock; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; import org.lfenergy.compas.scl.data.model.ChangeSetType; -import org.lfenergy.compas.scl.data.xml.HistoryItem; -import org.lfenergy.compas.scl.data.xml.Item; import org.lfenergy.compas.scl.data.model.Version; import org.lfenergy.compas.scl.data.rest.v1.model.CreateRequest; import org.lfenergy.compas.scl.data.rest.v1.model.UpdateRequest; import org.lfenergy.compas.scl.data.service.CompasSclDataService; +import org.lfenergy.compas.scl.data.xml.HistoryItem; +import org.lfenergy.compas.scl.data.xml.Item; import org.lfenergy.compas.scl.extensions.model.SclFileType; import java.io.IOException; @@ -28,7 +29,6 @@ import static org.lfenergy.compas.scl.data.SclDataServiceConstants.SCL_NS_URI; import static org.lfenergy.compas.scl.data.rest.Constants.*; import static org.mockito.Mockito.*; -import io.quarkus.test.InjectMock; @QuarkusTest @TestHTTPEndpoint(CompasSclDataResource.class) @@ -192,7 +192,7 @@ void deleteAll_WhenCalled_ThenServiceCalled() { var uuid = UUID.randomUUID(); var type = SclFileType.SCD; - doNothing().when(compasSclDataService).delete(type, uuid); + doNothing().when(compasSclDataService).delete(type, uuid, false); given() .pathParam(TYPE_PATH_PARAM, type) @@ -210,7 +210,7 @@ void deleteVersion_WhenCalled_ThenServiceCalled() { var type = SclFileType.SCD; var version = new Version(1, 2, 3); - doNothing().when(compasSclDataService).delete(type, uuid, version); + doNothing().when(compasSclDataService).deleteVersion(type, uuid, version, false); given() .pathParam(TYPE_PATH_PARAM, type) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9f3d2937 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: '3.8' + +services: + postgres: + image: postgres:latest + container_name: compas_postgresql + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: compas + PGDATA: /var/lib/postgresql/data/compas + volumes: + - ./data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - compas-network + + keycloak: + image: compas_keycloak:latest # Assuming the Keycloak image is already built + container_name: compas_keycloak + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + ports: + - "8089:8080" + networks: + - compas-network + + compas-scl-data-service: + image: lfenergy/compas-scl-data-service:0.15.3-SNAPSHOT-postgresql # Use the latest compas-scl-data-service image, you can generate it with ./mvnw package -Pnative-image + container_name: compas-scl-data-service + depends_on: + - postgres + ports: + - "8080:8080" + environment: + POSTGRESQL_HOST: postgres + POSTGRESQL_PORT: 5432 + POSTGRESQL_DB: compas + POSTGRESQL_USERNAME: postgres + POSTGRESQL_PASSWORD: postgres + # JWT Security Variables + JWT_VERIFY_KEY: http://localhost:8089/auth/realms/compas/protocol/openid-connect/certs + JWT_VERIFY_ISSUER: http://localhost:8089/auth/realms/compas + JWT_VERIFY_CLIENT_ID: scl-data-service + JWT_GROUPS_PATH: resource_access/scl-data-service/roles + # UserInfo variables + USERINFO_NAME_CLAIMNAME: name + USERINFO_WHO_CLAIMNAME: name + USERINFO_SESSION_WARNING: 20 + USERINFO_SESSION_EXPIRES: 30 + restart: on-failure # will restart until it's success + networks: + - compas-network + +networks: + compas-network: + driver: bridge diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java new file mode 100644 index 00000000..8ad600d2 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java @@ -0,0 +1,59 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; + +public class HistoryMetaItem extends AbstractItem implements IHistoryMetaItem { + private final String type; + private final String author; + private final String comment; + private final OffsetDateTime changedAt; + private final boolean archived; + private final boolean available; + private final boolean deleted; + + public HistoryMetaItem(String id, String name, String version, String type, String author, String comment, OffsetDateTime changedAt, boolean archived, boolean available, boolean deleted) { + super(id, name, version); + this.type = type; + this.author = author; + this.comment = comment; + this.changedAt = changedAt; + this.archived = archived; + this.available = available; + this.deleted = deleted; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public OffsetDateTime getChangedAt() { + return changedAt; + } + + @Override + public boolean isArchived() { + return archived; + } + + @Override + public boolean isAvailable() { + return available; + } + + @Override + public boolean isDeleted() { + return deleted; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index f6685b08..357865c4 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -4,24 +4,20 @@ package org.lfenergy.compas.scl.data.repository.postgresql; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; import org.lfenergy.compas.scl.extensions.model.SclFileType; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import javax.sql.DataSource; -import jakarta.transaction.Transactional; -import java.sql.Array; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.sql.*; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.*; import static jakarta.transaction.Transactional.TxType.REQUIRED; import static jakarta.transaction.Transactional.TxType.SUPPORTS; @@ -38,6 +34,13 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String HITEM_WHO_FIELD = "hitem_who"; private static final String HITEM_WHEN_FIELD = "hitem_when"; private static final String HITEM_WHAT_FIELD = "hitem_what"; + private static final String HISTORYMETAITEM_TYPE_FIELD = "type"; + private static final String HISTORYMETAITEM_AUTHOR_FIELD = "author"; + private static final String HISTORYMETAITEM_COMMENT_FIELD = "comment"; + private static final String HISTORYMETAITEM_CHANGEDAT_FIELD = "changedAt"; + private static final String HISTORYMETAITEM_AVAILABLE_FIELD = "available"; + private static final String HISTORYMETAITEM_ARCHIVED_FIELD = "archived"; + private static final String HISTORYMETAITEM_IS_DELETED_FIELD = "is_deleted"; private final DataSource dataSource; @@ -56,6 +59,7 @@ public List list(SclFileType type) { from (select distinct on (scl_file.id) * from scl_file where scl_file.type = ? + and scl_file.is_deleted = false order by scl_file.id , scl_file.major_version desc , scl_file.minor_version desc @@ -115,6 +119,7 @@ left outer join ( and scl_data.patch_version = scl_file.patch_version where scl_file.id = ? and scl_file.type = ? + and scl_file.is_deleted = false order by scl_file.major_version , scl_file.minor_version , scl_file.patch_version @@ -184,6 +189,37 @@ public String findByUUID(SclFileType type, UUID id, Version version) { } } + @Override + @Transactional(SUPPORTS) + public String findByUUID(UUID id, Version version) { + var sql = """ + select scl_file.scl_data + from scl_file + where scl_file.id = ? + and scl_file.major_version = ? + and scl_file.minor_version = ? + and scl_file.patch_version = ? + """; + + try (var connection = dataSource.getConnection(); + var stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, id); + stmt.setInt(2, version.getMajorVersion()); + stmt.setInt(3, version.getMinorVersion()); + stmt.setInt(4, version.getPatchVersion()); + + try (var resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + return resultSet.getString(SCL_DATA_FIELD); + } + var message = String.format("No record found with ID '%s' and version '%s'", id, version); + throw new CompasNoDataFoundException(message); + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error select scl data from database!", exp); + } + } + @Override @Transactional(SUPPORTS) public boolean hasDuplicateSclName(SclFileType type, String name) { @@ -221,6 +257,7 @@ public IAbstractItem findMetaInfoByUUID(SclFileType type, UUID id) { from scl_file where scl_file.id = ? and scl_file.type = ? + and scl_file.is_deleted = false order by scl_file.major_version desc, scl_file.minor_version desc, scl_file.patch_version desc """; @@ -322,7 +359,26 @@ public void delete(SclFileType type, UUID id) { @Override @Transactional(REQUIRED) - public void delete(SclFileType type, UUID id, Version version) { + public void softDelete(SclFileType type, UUID id) { + var sql = """ + UPDATE scl_file + SET is_deleted = true + WHERE scl_file.id = ? + AND scl_file.type = ? + """; + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, id); + sclStmt.setObject(2, type.name()); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error marking SCL as deleted in database!", exp); + } + } + + @Override + @Transactional(REQUIRED) + public void deleteVersion(SclFileType type, UUID id, Version version) { var sql = """ delete from scl_file where scl_file.id = ? @@ -345,6 +401,32 @@ public void delete(SclFileType type, UUID id, Version version) { } } + @Override + @Transactional(REQUIRED) + public void softDeleteVersion(SclFileType type, UUID id, Version version) { + var sql = """ + UPDATE scl_file + SET is_deleted = true + WHERE scl_file.id = ? + AND scl_file.type = ? + AND scl_file.major_version = ? + AND scl_file.minor_version = ? + AND scl_file.patch_version = ? + """; + + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, id); + sclStmt.setString(2, type.name()); + sclStmt.setInt(3, version.getMajorVersion()); + sclStmt.setInt(4, version.getMinorVersion()); + sclStmt.setInt(5, version.getPatchVersion()); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error marking SCL version as deleted in database!", exp); + } + } + private String createVersion(ResultSet resultSet) throws SQLException { var version = new Version(resultSet.getInt(MAJOR_VERSION_FIELD), resultSet.getInt(MINOR_VERSION_FIELD), @@ -364,4 +446,207 @@ private List createLabelList(Array sqlArray) throws SQLException { } return labelsList; } -} + + @Override + @Transactional(REQUIRED) + public void createHistoryVersion(UUID id, String name, Version version, SclFileType type, String author, String comment, OffsetDateTime changedAt, Boolean archived, Boolean available) { + var sql = """ + INSERT INTO scl_history(id, name, major_version, minor_version, patch_version, type, author, comment, changedAt, archived, available) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """; + + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, id); + sclStmt.setString(2, name); + sclStmt.setInt(3, version.getMajorVersion()); + sclStmt.setInt(4, version.getMinorVersion()); + sclStmt.setInt(5, version.getPatchVersion()); + sclStmt.setString(6, type.toString()); + sclStmt.setString(7, author); + sclStmt.setString(8, comment); + sclStmt.setObject(9, changedAt); + sclStmt.setBoolean(10, archived); + sclStmt.setBoolean(11, available); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding SCL History to database!", exp); + } + } + + @Override + @Transactional(SUPPORTS) + public List listHistory() { + var sql = """ + SELECT * + FROM ( + SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted + FROM scl_history + JOIN scl_file ON scl_history.id = scl_file.id + AND scl_history.major_version = scl_file.major_version + AND scl_history.minor_version = scl_file.minor_version + AND scl_history.patch_version = scl_file.patch_version + ORDER BY + scl_history.id, + scl_history.major_version DESC, + scl_history.minor_version DESC, + scl_history.patch_version DESC + ) subquery + ORDER BY subquery.name + """; + return executeHistoryQuery(sql, Collections.emptyList()); + } + + @Override + @Transactional(SUPPORTS) + public List listHistory(UUID id) { + var sql = """ + SELECT * + FROM ( + SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted + FROM scl_history + JOIN scl_file + ON scl_history.id = scl_file.id + AND scl_history.major_version = scl_file.major_version + AND scl_history.minor_version = scl_file.minor_version + AND scl_history.patch_version = scl_file.patch_version + WHERE scl_history.id = ? + ORDER BY + scl_history.id, + scl_history.major_version DESC, + scl_history.minor_version DESC, + scl_history.patch_version DESC + ) subquery + ORDER BY subquery.name + """; + return executeHistoryQuery(sql, Collections.singletonList(id)); + } + + @Override + @Transactional(SUPPORTS) + public List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { + StringBuilder sqlBuilder = new StringBuilder(""" + SELECT * + FROM ( + SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted + FROM scl_history + JOIN scl_file + ON scl_history.id = scl_file.id + AND scl_history.major_version = scl_file.major_version + AND scl_history.minor_version = scl_file.minor_version + AND scl_history.patch_version = scl_file.patch_version + ORDER BY + scl_history.id, + scl_history.major_version DESC, + scl_history.minor_version DESC, + scl_history.patch_version DESC + ) subquery + ORDER BY subquery.name + WHERE 1=1 + """); + + List parameters = new ArrayList<>(); + + if (type != null) { + sqlBuilder.append(" AND subquery.type = ?"); + parameters.add(type.toString()); + } + + if (name != null) { + sqlBuilder.append(" AND subquery.name ILIKE ?"); + parameters.add("%" + name + "%"); + } + + if (author != null) { + sqlBuilder.append(" AND subquery.author = ?"); + parameters.add(author); + } + + if (from != null) { + sqlBuilder.append(" AND subquery.changedAt >= ?"); + parameters.add(from); + } + + if (to != null) { + sqlBuilder.append(" AND subquery.changedAt <= ?"); + parameters.add(to); + } + + sqlBuilder.append(System.lineSeparator()); + sqlBuilder.append(""" + ORDER BY + scl_history.name, + scl_history.major_version, + scl_history.minor_version, + scl_history.patch_version + """); + + return executeHistoryQuery(sqlBuilder.toString(), parameters); + } + + + @Override + @Transactional(SUPPORTS) + public List listHistoryVersionsByUUID(UUID id) { + var sql = """ + SELECT scl_history.*, scl_file.is_deleted + FROM scl_history + JOIN scl_file + ON scl_history.id = scl_file.id + AND scl_history.major_version = scl_file.major_version + AND scl_history.minor_version = scl_file.minor_version + AND scl_history.patch_version = scl_file.patch_version + WHERE scl_history.id = ? + ORDER BY + scl_history.name, + scl_history.major_version, + scl_history.minor_version, + scl_history.patch_version + """; + return executeHistoryQuery(sql, Collections.singletonList(id)); + } + + private List executeHistoryQuery(String sql, List parameters) { + List items = new ArrayList<>(); + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql); + ) { + for (int i = 0; i < parameters.size(); i++) { + stmt.setObject(i + 1, parameters.get(i)); + } + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + items.add(mapResultSetToHistoryMetaItem(resultSet)); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException( + POSTGRES_SELECT_ERROR_CODE, + "Error listing scl entries from database!", exp + ); + } + return items; + } + + private HistoryMetaItem mapResultSetToHistoryMetaItem(ResultSet resultSet) throws SQLException { + return new HistoryMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + createVersion(resultSet), + resultSet.getString(HISTORYMETAITEM_TYPE_FIELD), + resultSet.getString(HISTORYMETAITEM_AUTHOR_FIELD), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), + convertToOffsetDateTime(resultSet.getTimestamp(HISTORYMETAITEM_CHANGEDAT_FIELD)), + resultSet.getBoolean(HISTORYMETAITEM_ARCHIVED_FIELD), + resultSet.getBoolean(HISTORYMETAITEM_AVAILABLE_FIELD), + resultSet.getBoolean(HISTORYMETAITEM_IS_DELETED_FIELD) + ); + } + + private OffsetDateTime convertToOffsetDateTime(Timestamp sqlTimestamp) { + return sqlTimestamp != null + ? sqlTimestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() + : null; + } +} \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql new file mode 100644 index 00000000..d16e5dde --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql @@ -0,0 +1,32 @@ +-- +-- Creating table to hold SCL History Data. The SCL is identified by it's ID, but there can be multiple versions. +-- +create table scl_history ( + id uuid not null, + name varchar(255) not null, + major_version smallint not null, + minor_version smallint not null, + patch_version smallint not null, + type varchar(3) not null, + author varchar(255) not null, + comment varchar(255) not null, + changedAt TIMESTAMP WITH TIME ZONE not null, + archived boolean not null default false, + available boolean not null default true, + primary key (id, major_version, minor_version, patch_version) +); + +create index scl_history_type on scl_history(type); + +comment on table scl_history is 'Table holding all the SCL History Data. The combination id and version are unique (pk).'; +comment on column scl_history.id is 'Unique ID generated according to standards'; +comment on column scl_history.name is 'The name of the SCL File'; +comment on column scl_history.major_version is 'Versioning according to Semantic Versioning (Major Position)'; +comment on column scl_history.minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; +comment on column scl_history.patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; +comment on column scl_history.type is 'The type of SCL stored'; +comment on column scl_history.author is 'The author of the change in this version of the SCL File'; +comment on column scl_history.comment is 'The comment of the change in this version of the SCL File'; +comment on column scl_history.changedAt is 'The date of the change in this version of the SCL File'; +comment on column scl_history.archived is 'Is this version of the SCL File archived'; +comment on column scl_history.available is 'Is this version of the SCL File available'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_6__scl_file_add_is_deleted.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_6__scl_file_add_is_deleted.sql new file mode 100644 index 00000000..15ad771a --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_6__scl_file_add_is_deleted.sql @@ -0,0 +1,5 @@ +-- Adding the boolean field 'is_deleted' with default value false +ALTER TABLE scl_file + ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN scl_file.is_deleted IS 'Indicates if the SCL entry is marked as deleted (soft delete)'; diff --git a/repository-postgresql/src/test/resources/scl_history_testdata.sql b/repository-postgresql/src/test/resources/scl_history_testdata.sql new file mode 100644 index 00000000..12d3d25f --- /dev/null +++ b/repository-postgresql/src/test/resources/scl_history_testdata.sql @@ -0,0 +1,30 @@ +DELETE +FROM scl_file; +DELETE +FROM scl_history; + +-- Insert test data into scl_history table +INSERT INTO scl_history (id, name, major_version, minor_version, patch_version, type, author, comment, changedAt, + archived, available) +VALUES ('1e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 11', 1, 0, 0, 'SSD', 'Author 1', + 'Initial creation of Test Item 1', '2023-10-15T14:25:12.510436+02:00', false, false), + ('1e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 11', 1, 1, 0, 'SSD', 'Author 1', + 'Minor update to Test Item 2', '2024-09-15T14:25:12.510436+02:00', false, false), + ('1e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 11', 2, 0, 0, 'SSD', 'Author 1', + 'Patch update for Test Item 3', '2024-10-12T14:25:12.510436+02:00', false, true), + ('2e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 12', 1, 0, 0, 'SCD', 'Author 1', + 'Major update for Test Item 4', '2023-09-15T14:25:12.510436+02:00', false, false), + ('2e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 12', 2, 0, 0, 'SCD', 'Author 1', + 'Initial creation of Test Item 5', '2024-10-15T14:25:12.510436+02:00', false, false), + ('3e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 3', 1, 0, 0, 'SSD', 'Author 2', + 'Patch update for Test Item 6', '2023-11-15T14:25:12.510436+02:00', false, false), + ('3e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 3', 1, 0, 1, 'SSD', 'Author 2', + 'Minor update to Test Item 7', '2024-10-15T14:20:12.510436+02:00', false, false), + ('3e40a51a-2e4f-482e-9410-72bb6177ec47', 'Test Item 3', 1, 1, 0, 'SSD', 'Author 2', + 'Archived version of Test Item 8', '2024-10-10T16:25:12.510436+02:00', false, false); + +-- Insert test data into scl_file table +INSERT INTO scl_file(id, major_version, minor_version, patch_version, type, name, scl_data, creation_date, created_by) +VALUES ('1e40a51a-2e4f-482e-9410-72bb6177ec47', 2, 0, 0, 'SSD', 'Test Item 11', 'Hello World', + '2024-10-15T14:25:12.510436+02:00', 'Author 1'); + diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java new file mode 100644 index 00000000..9e21a347 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Alliander N.V. +// +// SPDX-License-Identifier: Apache-2.0 +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; + +public interface IHistoryMetaItem extends IAbstractItem { + + String getType(); + + String getAuthor(); + + String getComment(); + + OffsetDateTime getChangedAt(); + + boolean isArchived(); + + boolean isAvailable(); + + boolean isDeleted(); + +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index 4535fdda..a710b2b9 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -6,6 +6,7 @@ import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.extensions.model.SclFileType; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -49,6 +50,15 @@ public interface CompasSclDataRepository { */ IAbstractItem findMetaInfoByUUID(SclFileType type, UUID id); + /** + * Return the specific version of a specific SCL Entry. + * + * @param id The ID of the SCL to search for. + * @param version The version of the ScL to search for. + * @return The SCL XML File Content that is search for. + */ + String findByUUID(UUID id, Version version); + /** * Return the specific version of a specific SCL Entry. * @@ -94,11 +104,59 @@ public interface CompasSclDataRepository { void delete(SclFileType type, UUID id); /** - * Delete passed versions for a specific SCL File using its ID. + * Mark all versions as deleted for a specific SCL File using its ID without really deleting them. + * + * @param type The type of SCL where to find the SCL File + * @param id The ID of the SCL File to delete. + */ + void softDelete(SclFileType type, UUID id); + + /** + * Delete passed version for a specific SCL File using its ID. * * @param type The type of SCL where to find the SCL File * @param id The ID of the SCL File to delete. * @param version The version of that SCL File to delete. */ - void delete(SclFileType type, UUID id, Version version); + void deleteVersion(SclFileType type, UUID id, Version version); + + /** + * Mark passed version for a specific SCL File as deleted using its ID without really deleting it. + * + * @param type The type of SCL where to find the SCL File + * @param id The ID of the SCL File to delete. + * @param version The version of that SCL File to delete. + */ + void softDeleteVersion(SclFileType type, UUID id, Version version); + + /** + * List the latest version of all SCL History Entries. + * + * @return The list of entries found. + */ + List listHistory(); + + /** + * List the latest version of all SCL History Entries. + * + * @return The list of entries found. + */ + List listHistory(UUID id); + + /** + * List the latest version of all SCL History Entries. + * + * @return The list of entries found. + */ + List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to); + + /** + * List all history versions for a specific SCL Entry. + * + * @param id The ID of the SCL to search for. + * @return The list of versions found for that specific sCl Entry. + */ + List listHistoryVersionsByUUID(UUID id); + + void createHistoryVersion(UUID id, String name, Version version, SclFileType type, String author, String comment, OffsetDateTime changedAt, Boolean archived, Boolean available); } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index dbbccd31..85057e34 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -3,26 +3,30 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.service; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import org.lfenergy.compas.core.commons.ElementConverter; import org.lfenergy.compas.core.commons.exception.CompasException; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.ChangeSetType; -import org.lfenergy.compas.scl.data.xml.HistoryItem; -import org.lfenergy.compas.scl.data.xml.Item; +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; import org.lfenergy.compas.scl.data.model.Version; import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; import org.lfenergy.compas.scl.data.util.SclElementProcessor; +import org.lfenergy.compas.scl.data.xml.HistoryItem; +import org.lfenergy.compas.scl.data.xml.Item; import org.lfenergy.compas.scl.extensions.model.SclFileType; import org.w3c.dom.Element; import org.w3c.dom.Node; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -40,6 +44,7 @@ */ @ApplicationScoped public class CompasSclDataService { + private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; @@ -81,9 +86,9 @@ public List listVersionsByUUID(SclFileType type, UUID id) { throw new CompasNoDataFoundException(message); } return items - .stream() - .map(e -> new HistoryItem(e.getId(), e.getName(), e.getVersion(), e.getWho(), e.getWhen(), e.getWhat())) - .toList(); + .stream() + .map(e -> new HistoryItem(e.getId(), e.getName(), e.getVersion(), e.getWho(), e.getWhen(), e.getWhat())) + .toList(); } /** @@ -98,6 +103,18 @@ public String findByUUID(SclFileType type, UUID id) { return repository.findByUUID(type, id); } + /** + * Get a specific version of a specific SCL XML File (using the UUID) for a specific type. + * + * @param id The UUID of the record to search for. + * @param version The version to search for. + * @return The found version of the SCL XML Files. + */ + @Transactional(SUPPORTS) + public String findByUUID(UUID id, Version version) { + return repository.findByUUID(id, version); + } + /** * Get a specific version of a specific SCL XML File (using the UUID) for a specific type. * @@ -123,6 +140,23 @@ public String findByUUID(SclFileType type, UUID id, Version version) { */ @Transactional(REQUIRED) public String create(SclFileType type, String name, String who, String comment, String sclData) { + return create(type, name, who, comment, sclData, false); + } + + /** + * Create a new record for the passed SCL XML File with the passed name for a specific type. + * A new UUID is generated to be set and also the CoMPAS Private Elements are added on SCL Level. + * + * @param type The type to create it for. + * @param name The name that will be stored as CoMPAS Private extension. + * @param comment Some comments that will be added to the THistory entry being added. + * @param sclData The SCL XML File to store. + * @param isHistoryEnabled Feature Flag to enable creation of history entries in a history repository. + * @return The ID of the new created SCL XML File in the database. + */ + @Transactional(REQUIRED) + public String create(SclFileType type, String name, String who, String comment, String sclData, boolean isHistoryEnabled) { + var when = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()); var scl = converter.convertToElement(new BufferedInputStream(new ByteArrayInputStream(sclData.getBytes(StandardCharsets.UTF_8))), SCL_ELEMENT_NAME, SCL_NS_URI); if (scl == null) { throw new CompasException(NO_SCL_ELEMENT_FOUND_ERROR_CODE, "No valid SCL found in the passed SCL Data."); @@ -140,7 +174,8 @@ public String create(SclFileType type, String name, String who, String comment, // Update the Header of the SCL (or create if not exists.) var header = createOrUpdateHeader(scl, id, version); sclElementProcessor.cleanupHistoryItem(header, version); - sclElementProcessor.addHistoryItem(header, who, createMessage("SCL created", comment), version); + var what = createMessage("SCL created", comment); + sclElementProcessor.addHistoryItem(header, who, when, what, version); // Update or add the Compas Private Element to the SCL File. setSclCompasPrivateElement(scl, name, type); @@ -150,10 +185,15 @@ public String create(SclFileType type, String name, String who, String comment, var newSclData = converter.convertToString(scl); repository.create(type, id, name, newSclData, version, who, labels); + + if (isHistoryEnabled) { + repository.createHistoryVersion(id, name, version, type, who, what, OffsetDateTime.parse(when), false, true); + } + return newSclData; } - public boolean hasDuplicateSclName(SclFileType type, String name){ + public boolean hasDuplicateSclName(SclFileType type, String name) { return repository.hasDuplicateSclName(type, name); } @@ -171,6 +211,25 @@ public boolean hasDuplicateSclName(SclFileType type, String name){ */ @Transactional(REQUIRED) public String update(SclFileType type, UUID id, ChangeSetType changeSetType, String who, String comment, String sclData) { + return update(type, id, changeSetType, who, comment, sclData, false); + } + + /** + * Create a new version of a specific SCL XML File. The content will be the passed SCL XML File. + * The UUID and new version (depending on the passed ChangeSetType) are set and + * the CoMPAS Private elements will also be copied, the SCL Name will only be copied if it isn't available in the + * SCL XML File. + * + * @param type The type to update it for. + * @param id The UUID of the record to update. + * @param changeSetType The type of change to determine the new version. + * @param comment Some comments that will be added to the THistory entry being added. + * @param sclData The SCL XML File with the updated content. + * @param isHistoryEnabled Feature Flag to enable creation of history entries in a history repository. + */ + @Transactional(REQUIRED) + public String update(SclFileType type, UUID id, ChangeSetType changeSetType, String who, String comment, String sclData, boolean isHistoryEnabled) { + var when = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()); var scl = converter.convertToElement(new BufferedInputStream(new ByteArrayInputStream(sclData.getBytes(StandardCharsets.UTF_8))), SCL_ELEMENT_NAME, SCL_NS_URI); if (scl == null) { throw new CompasException(NO_SCL_ELEMENT_FOUND_ERROR_CODE, "No valid SCL found in the passed SCL Data."); @@ -192,7 +251,8 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str // Update the Header of the SCL (or create if not exists.) var header = createOrUpdateHeader(scl, id, version); sclElementProcessor.cleanupHistoryItem(header, version); - sclElementProcessor.addHistoryItem(header, who, createMessage("SCL updated", comment), version); + var what = createMessage("SCL updated", comment); + sclElementProcessor.addHistoryItem(header, who, when, what, version); // Update or add the Compas Private Element to the SCL File. var newSclName = newFileName.orElse(currentSclMetaInfo.getName()); @@ -204,30 +264,44 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str var newSclData = converter.convertToString(scl); repository.create(type, id, newSclName, newSclData, version, who, labels); + if (isHistoryEnabled) { + repository.createHistoryVersion(id, newSclName, version, type, who, what, OffsetDateTime.parse(when), false, true); + } + return newSclData; } /** * Delete all versions for a specific SCL File using it's ID. * - * @param type The type of SCL where to find the SCL File - * @param id The ID of the SCL File to delete. + * @param type The type of SCL where to find the SCL File + * @param id The ID of the SCL File to delete. + * @param isPersistentDeleteEnabled Feature Flag for a soft delete to mark the data as deleted instead of deleting it. */ @Transactional(REQUIRED) - public void delete(SclFileType type, UUID id) { - repository.delete(type, id); + public void delete(SclFileType type, UUID id, boolean isPersistentDeleteEnabled) { + if (isPersistentDeleteEnabled) { + repository.softDelete(type, id); + } else { + repository.delete(type, id); + } } /** * Delete passed versions for a specific SCL File using it's ID. * - * @param type The type of SCL where to find the SCL File - * @param id The ID of the SCL File to delete. - * @param version The version of that SCL File to delete. + * @param type The type of SCL where to find the SCL File + * @param id The ID of the SCL File to delete. + * @param version The version of that SCL File to delete. + * @param isPersistentDeleteEnabled Feature Flag for a soft delete to mark the data as deleted instead of deleting it. */ @Transactional(REQUIRED) - public void delete(SclFileType type, UUID id, Version version) { - repository.delete(type, id, version); + public void deleteVersion(SclFileType type, UUID id, Version version, boolean isPersistentDeleteEnabled) { + if (isPersistentDeleteEnabled) { + repository.softDeleteVersion(type, id, version); + } else { + repository.deleteVersion(type, id, version); + } } /** @@ -276,19 +350,19 @@ private void setSclCompasPrivateElement(Element scl, String name, SclFileType fi .orElseGet(() -> sclElementProcessor.addCompasPrivate(scl)); sclElementProcessor.getChildNodeByName(compasPrivate, COMPAS_SCL_NAME_EXTENSION, COMPAS_EXTENSION_NS_URI) - .ifPresentOrElse( - // Override the value of the element with the name passed. - element -> element.setTextContent(name), - () -> sclElementProcessor.addCompasElement(compasPrivate, COMPAS_SCL_NAME_EXTENSION, name) - ); + .ifPresentOrElse( + // Override the value of the element with the name passed. + element -> element.setTextContent(name), + () -> sclElementProcessor.addCompasElement(compasPrivate, COMPAS_SCL_NAME_EXTENSION, name) + ); // Always set the file type as private element. sclElementProcessor.getChildNodeByName(compasPrivate, COMPAS_SCL_FILE_TYPE_EXTENSION, COMPAS_EXTENSION_NS_URI) - .ifPresentOrElse( - element -> element.setTextContent(fileType.toString()), - () -> sclElementProcessor.addCompasElement(compasPrivate, COMPAS_SCL_FILE_TYPE_EXTENSION, - fileType.toString()) - ); + .ifPresentOrElse( + element -> element.setTextContent(fileType.toString()), + () -> sclElementProcessor.addCompasElement(compasPrivate, COMPAS_SCL_FILE_TYPE_EXTENSION, + fileType.toString()) + ); } /** @@ -339,8 +413,8 @@ List validateLabels(Element scl) { } return labelElements.stream() - .map(Element::getTextContent) - .toList(); + .map(Element::getTextContent) + .toList(); } /** @@ -353,4 +427,44 @@ boolean validateLabel(Element labelElement) { String label = labelElement.getTextContent(); return label.matches("[A-Za-z][0-9A-Za-z_-]*"); } + + /** + * List the latest version of all SCL File history entries. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistory() { + return repository.listHistory(); + } + + /** + * List the latest version of all SCL File history entries. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistory(UUID id) { + return repository.listHistory(id); + } + + /** + * List the latest version of all SCL File history entries. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { + return repository.listHistory(type, name, author, from, to); + } + + /** + * List the history entries of an SCL File specified by an uuid. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistoryVersionsByUUID(UUID id) { + return repository.listHistoryVersionsByUUID(id); + } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/util/SclElementProcessor.java b/service/src/main/java/org/lfenergy/compas/scl/data/util/SclElementProcessor.java index 6111dbf4..d3e486bd 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/util/SclElementProcessor.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/util/SclElementProcessor.java @@ -3,15 +3,13 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.util; +import jakarta.enterprise.context.ApplicationScoped; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.Version; import org.w3c.dom.Element; import org.w3c.dom.Node; -import jakarta.enterprise.context.ApplicationScoped; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Optional; @@ -146,12 +144,12 @@ boolean shouldRemoveHItem(Element hItemElement, Version version) { * * @param header The Header Element from SCL under which the History Element can be found/added. * @param who Teh name of the user that made the change (who). + * @param when The date of the change * @param fullmessage The message that will be set (what). * @param version The version to be set (version). * @return The Hitem created and added to the History Element. */ - public Element addHistoryItem(Element header, String who, String fullmessage, Version version) { - var formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + public Element addHistoryItem(Element header, String who, String when, String fullmessage, Version version) { var document = header.getOwnerDocument(); var history = getChildNodesByName(header, SCL_HISTORY_ELEMENT_NAME, SCL_NS_URI).stream().findFirst() @@ -164,7 +162,7 @@ public Element addHistoryItem(Element header, String who, String fullmessage, Ve Element hItem = document.createElementNS(SCL_NS_URI, SCL_HITEM_ELEMENT_NAME); hItem.setAttribute(SCL_VERSION_ATTR, version.toString()); hItem.setAttribute(SCL_REVISION_ATTR, ""); - hItem.setAttribute(SCL_WHEN_ATTR, formatter.format(new Date())); + hItem.setAttribute(SCL_WHEN_ATTR, when); hItem.setAttribute(SCL_WHO_ATTR, who); hItem.setAttribute(SCL_WHAT_ATTR, fullmessage); history.appendChild(hItem); diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 142674b8..90205ee0 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -15,11 +15,10 @@ import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.ChangeSetType; import org.lfenergy.compas.scl.data.model.IHistoryItem; -import org.lfenergy.compas.scl.data.xml.HistoryItem; -import org.lfenergy.compas.scl.data.xml.SclMetaInfo; import org.lfenergy.compas.scl.data.model.Version; import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; import org.lfenergy.compas.scl.data.util.SclElementProcessor; +import org.lfenergy.compas.scl.data.xml.SclMetaInfo; import org.lfenergy.compas.scl.extensions.model.SclFileType; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -188,6 +187,26 @@ void create_WhenCalledWithXMLStringWithoutSCL_ThenCompasExceptionThrown() { assertEquals(NO_SCL_ELEMENT_FOUND_ERROR_CODE, exception.getErrorCode()); } + @Test + void create_WhenCalledWithFeatureHistoryEnabled_ThenCreateHistoryVersionEntriesInRepository() throws IOException { + var name = "JUSTSOMENAME"; + var comment = "Some comments"; + var who = "User A"; + + var scl = readSCL("scl_test_file.scd"); + + when(compasSclDataRepository.hasDuplicateSclName(SCL_TYPE, name)).thenReturn(false); + doNothing().when(compasSclDataRepository).create(eq(SCL_TYPE), any(UUID.class), eq(name), anyString(), eq(INITIAL_VERSION), eq(who), eq(emptyList())); + + scl = compasSclDataService.create(SCL_TYPE, name, who, comment, scl, true); + + assertNotNull(scl); + assertCompasExtension(scl, name); + assertHistoryItem(scl, 2, INITIAL_VERSION, comment); + verify(compasSclDataRepository).create(eq(SCL_TYPE), any(UUID.class), eq(name), anyString(), eq(INITIAL_VERSION), eq(who), eq(emptyList())); + verify(compasSclDataRepository).hasDuplicateSclName(SCL_TYPE, name); + } + @Test void update_WhenCalledWithoutCompasElements_ThenSCLReturnedWithCorrectCompasExtensionAndHistory() throws IOException { var previousName = "Previous SCL Filename"; @@ -306,21 +325,21 @@ void delete_WhenCalledWithoutVersion_ThenRepositoryIsCalled() { doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid); - compasSclDataService.delete(SCL_TYPE, uuid); + compasSclDataService.delete(SCL_TYPE, uuid, false); verify(compasSclDataRepository).delete(SCL_TYPE, uuid); } @Test - void delete_WhenCalledWithVersion_ThenRepositoryIsCalled() { + void deleteVersion_WhenCalledWithVersion_ThenRepositoryIsCalled() { var uuid = UUID.randomUUID(); var version = new Version(1, 0, 0); - doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, version); + doNothing().when(compasSclDataRepository).deleteVersion(SCL_TYPE, uuid, version); - compasSclDataService.delete(SCL_TYPE, uuid, version); + compasSclDataService.deleteVersion(SCL_TYPE, uuid, version, false); - verify(compasSclDataRepository).delete(SCL_TYPE, uuid, version); + verify(compasSclDataRepository).deleteVersion(SCL_TYPE, uuid, version); } @Test diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/util/SclElementProcessorTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/util/SclElementProcessorTest.java index 0191a302..44bb5455 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/util/SclElementProcessorTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/util/SclElementProcessorTest.java @@ -9,6 +9,8 @@ import org.lfenergy.compas.scl.data.model.Version; import org.w3c.dom.Element; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import java.util.Optional; @@ -125,6 +127,7 @@ void addCompasElement_WhenCalledWithoutCompasElement_ThenNewElementIsAdded() { @Test void addHistoryItem_WhenCalledWithoutHistoryElement_ThenElementIsAdded() { + var when = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()); var version = new Version("1.3.2"); var username = "The Tester"; var message = "The Message"; @@ -136,7 +139,7 @@ void addHistoryItem_WhenCalledWithoutHistoryElement_ThenElementIsAdded() { var historyElement = processor.getChildNodeByName(header.get(), SCL_HISTORY_ELEMENT_NAME, SCL_NS_URI); assertFalse(historyElement.isPresent()); - var result = processor.addHistoryItem(header.get(), username, message, version); + var result = processor.addHistoryItem(header.get(), username, when, message, version); assertNotNull(result); historyElement = processor.getChildNodeByName(header.get(), SCL_HISTORY_ELEMENT_NAME, SCL_NS_URI); @@ -152,6 +155,7 @@ void addHistoryItem_WhenCalledWithoutHistoryElement_ThenElementIsAdded() { @Test void addHistoryItem_WhenCalledWithHistoryItemElement_ThenElementIsAdded() { + var when = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()); var version = new Version("1.3.2"); var scl = readSCL("scl_with_history.scd"); var header = processor.getSclHeader(scl); @@ -160,7 +164,7 @@ void addHistoryItem_WhenCalledWithHistoryItemElement_ThenElementIsAdded() { var historyElement = processor.getChildNodeByName(header.get(), SCL_HISTORY_ELEMENT_NAME, SCL_NS_URI); assertTrue(historyElement.isPresent()); - var result = processor.addHistoryItem(header.get(), "The Tester", "The Message", version); + var result = processor.addHistoryItem(header.get(), "The Tester", when, "The Message", version); assertNotNull(result); assertEquals(version.toString(), result.getAttribute(SCL_VERSION_ATTR)); From 1304b93cf9e1ef918005a80eee34749eae274091 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 4 Nov 2024 12:54:25 +0100 Subject: [PATCH 02/23] updated archiving API, added search endpoint (cherry picked from commit c5caa38e58aaacaa4dbc143889b78c95fcb7af7b) --- app/src/main/openapi/archiving-api.yaml | 112 +++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/app/src/main/openapi/archiving-api.yaml b/app/src/main/openapi/archiving-api.yaml index 78df9315..21ab2349 100644 --- a/app/src/main/openapi/archiving-api.yaml +++ b/app/src/main/openapi/archiving-api.yaml @@ -1,6 +1,6 @@ openapi: 3.0.3 info: - title: CoMPAS SCL Data Service History API + title: CoMPAS SCL Data Archiving API version: 1.0.0 servers: @@ -454,6 +454,51 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponseDto' + /api/archive/resources/search: + post: + tags: + - archiving + summary: Search for archived resources + security: + - open-id-connect: [ read ] + operationId: searchArchivedResources + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ArchivedResourcesSearch' + responses: + '200': + description: Successfully generated location + content: + application/json: + schema: + $ref: '#/components/schemas/ArchivedResources' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '403': + description: Authorization failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to find location or resource + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + components: securitySchemes: @@ -509,9 +554,12 @@ components: required: - uuid - name + - author - contentType - version - fields + - modifiedAt + - archivedAt properties: uuid: type: string @@ -519,12 +567,33 @@ components: name: type: string description: "Resource name" + note: + type: string + description: "Versioning note" + author: + type: string + description: "Modifying author" + aprover: + type: string + description: "Name of the aprover" + type: + type: string + description: "Content type" contentType: type: string description: "Content type" + voltage: + type: string + description: "Content type" version: type: string description: "Version" + modifiedAt: + type: string + format: date-time + archivedAt: + type: string + format: date-time fields: type: array items: @@ -541,6 +610,47 @@ components: value: type: string description: "Tag value" + ArchivedResourcesSearch: + type: object + properties: + uuid: + type: string + description: "If uuid is set no other filter must be set" + location: + type: string + description: "Exact match of a location" + name: + type: string + description: "Partially match allowed" + aprover: + type: string + description: "Fulltext match which can be retrieved via extra endpoint" + contentType: + type: string + description: "Fulltext match set to one of the supported scl types: SSD, IID, ICD, SCD, CID, SED, ISD, STD, etc." + type: + type: string + description: "Type of the documented entity eg. Schütz, Leittechnik, etc" + voltage: + type: string + description: "Voltage of the documented entity eg. 110, 220, 380" + from: + type: string + format: date-time + description: "Starting date from where resources have been archived. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z)." + to: + type: string + format: date-time + description: "Ending date from where resources have been archived. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z)." + ArchivedResources: + type: object + required: + - resources + properties: + resources: + type: array + items: + $ref: '#/components/schemas/ArchivedResource' ErrorResponseDto: required: - timestamp From 2f4a638093797347fc4aa4150ccd5874947149b9 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Wed, 6 Nov 2024 09:56:59 +0100 Subject: [PATCH 03/23] added location to resource & assignedResources to location (cherry picked from commit 9f367cd3c0fd5a6a1b06521fa642716a35ddc6ce) --- app/src/main/openapi/archiving-api.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/openapi/archiving-api.yaml b/app/src/main/openapi/archiving-api.yaml index 21ab2349..90409db3 100644 --- a/app/src/main/openapi/archiving-api.yaml +++ b/app/src/main/openapi/archiving-api.yaml @@ -549,6 +549,10 @@ components: description: type: string description: "Location description" + assignedResources: + type: integer + format: int32 + description: "Number of resources assigned to this location" ArchivedResource: type: object required: @@ -564,6 +568,9 @@ components: uuid: type: string description: "Unique resource identifier" + location: + type: string + description: "Location of the resource, might be empty" name: type: string description: "Resource name" From 164392be4edf6c1666623ebb524c2b60a1cf4ff0 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 18 Nov 2024 14:52:43 +0100 Subject: [PATCH 04/23] feat: implement location api --- app/pom.xml | 46 ++-- .../data/rest/api/locations/LocationsApi.java | 52 +++++ .../rest/api/locations/LocationsResource.java | 109 +++++++++ .../api/locations/model/ErrorResponseDto.java | 119 ++++++++++ .../rest/api/locations/model/Location.java | 164 ++++++++++++++ .../rest/api/locations/model/Locations.java | 116 ++++++++++ .../rest/api/locations/model/Pagination.java | 96 ++++++++ app/src/main/openapi/archiving-api.yaml | 22 +- .../LocationResourceTestDataBuilder.java | 47 ++++ .../api/locations/LocationsResourceTest.java | 213 ++++++++++++++++++ .../scl/data/model/LocationMetaItem.java | 42 ++++ .../CompasSclDataPostgreSQLRepository.java | 170 ++++++++++++++ .../db/migration/V1_7__create_location.sql | 19 ++ .../CompasSclDataServiceErrorCode.java | 1 + .../scl/data/model/ILocationMetaItem.java | 9 + .../repository/CompasSclDataRepository.java | 61 +++++ service/pom.xml | 2 +- .../data/service/CompasSclDataService.java | 86 ++++++- 18 files changed, 1343 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationResourceTestDataBuilder.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/LocationMetaItem.java create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/ILocationMetaItem.java diff --git a/app/pom.xml b/app/pom.xml index c5b0ba52..2c7853ba 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -223,29 +223,29 @@ SPDX-License-Identifier: Apache-2.0 - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java new file mode 100644 index 00000000..cc720f01 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java @@ -0,0 +1,52 @@ +package org.lfenergy.compas.scl.data.rest.api.locations; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import org.lfenergy.compas.scl.data.rest.api.locations.model.Location; + +import java.util.List; +import java.util.UUID; + +@Path("/api/locations") +public interface LocationsApi { + @POST + @Blocking + @Path("/{locationId}/resources/{uuid}/assign") + @Produces({ "application/json" }) + Uni assignResourceToLocation(@PathParam("locationId") UUID locationId, @PathParam("uuid") UUID uuid); + + @POST + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + Uni createLocation(@Valid @NotNull Location location); + + @DELETE + @Blocking + @Path("/{locationId}") + @Produces({ "application/json" }) + Uni deleteLocation(@PathParam("locationId") UUID locationId); + + @GET + @Path("/{locationId}") + @Produces({ "application/json" }) + Uni getLocation(@PathParam("locationId") UUID locationId); + + @GET + @Produces({ "application/json" }) + Uni> getLocations(@QueryParam("page") Integer page, @QueryParam("pageSize") @DefaultValue("25") Integer pageSize); + + @POST + @Blocking + @Path("/{locationId}/resources/{uuid}/unassign") + @Produces({ "application/json" }) + Uni unassignResourceFromLocation(@PathParam("locationId") UUID locationId,@PathParam("uuid") UUID uuid); + + @PUT + @Path("/{locationId}") + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + Uni updateLocation(@PathParam("locationId") UUID locationId,@Valid @NotNull Location location); +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java new file mode 100644 index 00000000..f53cf35a --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -0,0 +1,109 @@ +package org.lfenergy.compas.scl.data.rest.api.locations; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; +import org.lfenergy.compas.scl.data.rest.api.locations.model.Location; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; + +import java.util.List; +import java.util.UUID; + +import static org.lfenergy.compas.scl.data.exception.CompasSclDataServiceErrorCode.LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE; + +@RequestScoped +public class LocationsResource implements LocationsApi { + private static final Logger LOGGER = LogManager.getLogger(LocationsResource.class); + + private final CompasSclDataService compasSclDataService; + + @Inject + public LocationsResource(CompasSclDataService compasSclDataService) { + this.compasSclDataService = compasSclDataService; + } + + @Override + public Uni assignResourceToLocation(UUID locationId, UUID uuid) { + compasSclDataService.assignResourceToLocation(locationId, uuid); + return Uni.createFrom().nullItem(); + } + + @Override + public Uni createLocation(Location location) { + return Uni.createFrom() + .item(() -> compasSclDataService.createLocation( + location.getKey(), + location.getName(), + location.getDescription() + )) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToLocation); + } + + @Override + public Uni deleteLocation(UUID locationId) { + int assignedResourceCount = compasSclDataService.findLocationByUUID(locationId).getAssignedResources(); + if (assignedResourceCount > 0) { + return Uni.createFrom().failure(new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, + String.format("Deletion of Location %s not allowed, unassign resources before deletion", locationId))); + } + compasSclDataService.deleteLocation(locationId); + return Uni.createFrom().nullItem(); + } + + @Override + public Uni getLocation(UUID locationId) { + LOGGER.info("Retrieving location for ID: {}", locationId); + return Uni.createFrom() + .item(() -> compasSclDataService.findLocationByUUID(locationId)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToLocation); + } + + @Override + public Uni> getLocations(Integer page, Integer pageSize) { + int pageLocation; + if (page != null && page > 1) { + pageLocation = page; + } else { + pageLocation = 0; + } + return Uni.createFrom() + .item(() -> compasSclDataService.listLocations(pageLocation, pageSize)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(list -> list.stream().map(this::mapToLocation).toList()); + } + + @Override + public Uni unassignResourceFromLocation(UUID locationId, UUID uuid) { + compasSclDataService.unassignResourceFromLocation(locationId, uuid); + return Uni.createFrom().nullItem(); + } + + @Override + public Uni updateLocation(UUID locationId, Location location) { + return Uni.createFrom(). + item(() -> compasSclDataService.updateLocation(locationId, location.getKey(), location.getName(), location.getDescription())) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToLocation); + } + + private Location mapToLocation(ILocationMetaItem location) { + return new Location() + .uuid(location.getId()) + .name(location.getName()) + .key(location.getKey()) + .description(location.getDescription()) + .assignedResources(location.getAssignedResources()); + } +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java new file mode 100644 index 00000000..217bb663 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java @@ -0,0 +1,119 @@ +package org.lfenergy.compas.scl.data.rest.api.locations.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.time.OffsetDateTime; +import java.util.Objects; + + + +@JsonTypeName("ErrorResponseDto") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T07:52:46.875467800+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ErrorResponseDto { + private OffsetDateTime timestamp; + private String code; + private String message; + + /** + * 2017-07-21T17:32:28Z. + **/ + public ErrorResponseDto timestamp(OffsetDateTime timestamp) { + this.timestamp = timestamp; + return this; + } + + + @JsonProperty("timestamp") + @NotNull public OffsetDateTime getTimestamp() { + return timestamp; + } + + @JsonProperty("timestamp") + public void setTimestamp(OffsetDateTime timestamp) { + this.timestamp = timestamp; + } + + /** + **/ + public ErrorResponseDto code(String code) { + this.code = code; + return this; + } + + + @JsonProperty("code") + @NotNull public String getCode() { + return code; + } + + @JsonProperty("code") + public void setCode(String code) { + this.code = code; + } + + /** + **/ + public ErrorResponseDto message(String message) { + this.message = message; + return this; + } + + + @JsonProperty("message") + @NotNull public String getMessage() { + return message; + } + + @JsonProperty("message") + public void setMessage(String message) { + this.message = message; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ErrorResponseDto errorResponseDto = (ErrorResponseDto) o; + return Objects.equals(this.timestamp, errorResponseDto.timestamp) && + Objects.equals(this.code, errorResponseDto.code) && + Objects.equals(this.message, errorResponseDto.message); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, code, message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ErrorResponseDto {\n"); + + sb.append(" timestamp: ").append(toIndentedString(timestamp)).append("\n"); + sb.append(" code: ").append(toIndentedString(code)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java new file mode 100644 index 00000000..1ad76a58 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java @@ -0,0 +1,164 @@ +package org.lfenergy.compas.scl.data.rest.api.locations.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.util.Objects; + + + +@JsonTypeName("Location") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T07:52:46.875467800+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class Location { + private String uuid; + private String key; + private String name; + private String description; + private Integer assignedResources; + + /** + * Unique location uuid generated by backend during creation + **/ + public Location uuid(String uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + public String getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Location key, defined once manually when creating a location + **/ + public Location key(String key) { + this.key = key; + return this; + } + + + @JsonProperty("key") + @NotNull public String getKey() { + return key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + * Location name + **/ + public Location name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + * Location description + **/ + public Location description(String description) { + this.description = description; + return this; + } + + + @JsonProperty("description") + public String getDescription() { + return description; + } + + @JsonProperty("description") + public void setDescription(String description) { + this.description = description; + } + + /** + * Number of resources assigned to this location + **/ + public Location assignedResources(Integer assignedResources) { + this.assignedResources = assignedResources; + return this; + } + + + @JsonProperty("assignedResources") + public Integer getAssignedResources() { + return assignedResources; + } + + @JsonProperty("assignedResources") + public void setAssignedResources(Integer assignedResources) { + this.assignedResources = assignedResources; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Location location = (Location) o; + return Objects.equals(this.uuid, location.uuid) && + Objects.equals(this.key, location.key) && + Objects.equals(this.name, location.name) && + Objects.equals(this.description, location.description) && + Objects.equals(this.assignedResources, location.assignedResources); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, key, name, description, assignedResources); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Location {\n"); + + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" assignedResources: ").append(toIndentedString(assignedResources)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java new file mode 100644 index 00000000..cff9ca61 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java @@ -0,0 +1,116 @@ +package org.lfenergy.compas.scl.data.rest.api.locations.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + + +@JsonTypeName("Locations") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T07:52:46.875467800+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class Locations { + private @Valid List<@Valid Location> locations = new ArrayList<>(); + private Pagination pagination; + + /** + * List of locations + **/ + public Locations locations(List<@Valid Location> locations) { + this.locations = locations; + return this; + } + + + @JsonProperty("locations") + @NotNull @Valid public List<@Valid Location> getLocations() { + return locations; + } + + @JsonProperty("locations") + public void setLocations(List<@Valid Location> locations) { + this.locations = locations; + } + + public Locations addLocationsItem(Location locationsItem) { + if (this.locations == null) { + this.locations = new ArrayList<>(); + } + + this.locations.add(locationsItem); + return this; + } + + public Locations removeLocationsItem(Location locationsItem) { + if (locationsItem != null && this.locations != null) { + this.locations.remove(locationsItem); + } + + return this; + } + /** + **/ + public Locations pagination(Pagination pagination) { + this.pagination = pagination; + return this; + } + + + @JsonProperty("pagination") + @Valid public Pagination getPagination() { + return pagination; + } + + @JsonProperty("pagination") + public void setPagination(Pagination pagination) { + this.pagination = pagination; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Locations locations = (Locations) o; + return Objects.equals(this.locations, locations.locations) && + Objects.equals(this.pagination, locations.pagination); + } + + @Override + public int hashCode() { + return Objects.hash(locations, pagination); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Locations {\n"); + + sb.append(" locations: ").append(toIndentedString(locations)).append("\n"); + sb.append(" pagination: ").append(toIndentedString(pagination)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java new file mode 100644 index 00000000..6eb5db8f --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java @@ -0,0 +1,96 @@ +package org.lfenergy.compas.scl.data.rest.api.locations.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.util.Objects; + + + +@JsonTypeName("Pagination") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T07:52:46.875467800+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class Pagination { + private Integer page; + private Integer pageSize; + + /** + **/ + public Pagination page(Integer page) { + this.page = page; + return this; + } + + + @JsonProperty("page") + @NotNull public Integer getPage() { + return page; + } + + @JsonProperty("page") + public void setPage(Integer page) { + this.page = page; + } + + /** + **/ + public Pagination pageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + + @JsonProperty("pageSize") + @NotNull public Integer getPageSize() { + return pageSize; + } + + @JsonProperty("pageSize") + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pagination pagination = (Pagination) o; + return Objects.equals(this.page, pagination.page) && + Objects.equals(this.pageSize, pagination.pageSize); + } + + @Override + public int hashCode() { + return Objects.hash(page, pageSize); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pagination {\n"); + + sb.append(" page: ").append(toIndentedString(page)).append("\n"); + sb.append(" pageSize: ").append(toIndentedString(pageSize)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/openapi/archiving-api.yaml b/app/src/main/openapi/archiving-api.yaml index 90409db3..64299b04 100644 --- a/app/src/main/openapi/archiving-api.yaml +++ b/app/src/main/openapi/archiving-api.yaml @@ -285,7 +285,7 @@ paths: - open-id-connect: [ write ] description: |- Removes the assignment of a resource from the assigned location. - operationId: unassignResourceToLocation + operationId: unassignResourceFromLocation parameters: - name: locationId in: path @@ -389,6 +389,20 @@ paths: schema: $ref: '#/components/schemas/ErrorResponseDto' /api/archive/referenced-resource/{id}/versions/{version}: + parameters: + - name: id + in: path + description: Unique data resource identifier + required: true + schema: + type: string + format: uuid + - name: version + in: path + description: Data resource version + required: true + schema: + type: string post: tags: - archiving @@ -580,9 +594,9 @@ components: author: type: string description: "Modifying author" - aprover: + approver: type: string - description: "Name of the aprover" + description: "Name of the approver" type: type: string description: "Content type" @@ -629,7 +643,7 @@ components: name: type: string description: "Partially match allowed" - aprover: + approver: type: string description: "Fulltext match which can be retrieved via extra endpoint" contentType: diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationResourceTestDataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationResourceTestDataBuilder.java new file mode 100644 index 00000000..10442dbc --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationResourceTestDataBuilder.java @@ -0,0 +1,47 @@ +package org.lfenergy.compas.scl.data.rest.api.locations; + +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; +import org.lfenergy.compas.scl.data.model.LocationMetaItem; + +import java.util.UUID; + +public class LocationResourceTestDataBuilder { + // Default values + private String id = UUID.randomUUID().toString(); + private String name = "Name"; + private String key = "Key"; + private String description = "Description"; + private int assignedResources = 0; + + public LocationResourceTestDataBuilder() { + } + + public LocationResourceTestDataBuilder setId(String id) { + this.id = id; + return this; + } + + public LocationResourceTestDataBuilder setName(String name) { + this.name = name; + return this; + } + + public LocationResourceTestDataBuilder setKey(String key) { + this.key = key; + return this; + } + + public LocationResourceTestDataBuilder setDescription(String description) { + this.description = description; + return this; + } + + public LocationResourceTestDataBuilder setAssignedResources(int assignedResources) { + this.assignedResources = assignedResources; + return this; + } + + public ILocationMetaItem build() { + return new LocationMetaItem(id, key, name, description, assignedResources); + } +} diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java new file mode 100644 index 00000000..24934b89 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java @@ -0,0 +1,213 @@ +package org.lfenergy.compas.scl.data.rest.api.locations; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.response.Response; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; +import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; +import org.lfenergy.compas.scl.data.rest.api.locations.model.Location; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.lfenergy.compas.scl.data.exception.CompasSclDataServiceErrorCode.POSTGRES_INSERT_ERROR_CODE; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@QuarkusTest +@TestHTTPEndpoint(LocationsResource.class) +@TestSecurity(user = "test-user") +class LocationsResourceTest { + + @InjectMock + private CompasSclDataService compasSclDataService; + + @Test + void createLocation_WhenCalled_ThenReturnsCreatedLocation() { + UUID uuid = UUID.randomUUID(); + String key = "Key"; + String name = "Name"; + String description = "Description"; + Location location = new Location(); + location.setUuid(uuid.toString()); + location.setKey(key); + location.setName(name); + location.setDescription(description); + ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); + + when(compasSclDataService.createLocation(key, name, description)).thenReturn(testData); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(location) + .when().post("/") + .then() + .statusCode(200) + .extract() + .response(); + + Location result = response.as(Location.class); + assertEquals(uuid, UUID.fromString(result.getUuid())); + assertEquals(key, result.getKey()); + assertEquals(name, result.getName()); + assertEquals(description, result.getDescription()); + } + + @Test + void deleteLocation_WhenCalled_ThenDeletesLocation() { + UUID uuid = UUID.randomUUID(); + ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); + when(compasSclDataService.findLocationByUUID(uuid)).thenReturn(testData); + given() + .contentType(MediaType.APPLICATION_JSON) + .when().delete("/" + uuid) + .then() + .statusCode(204) + .extract() + .response(); + } + + @Test + void getLocation_WhenCalled_ThenReturnsLocation() { + UUID uuid = UUID.randomUUID(); + String key = "Key"; + String name = "Name"; + String description = "Description"; + Location location = new Location(); + location.setUuid(uuid.toString()); + location.setKey(key); + location.setName(name); + location.setDescription(description); + ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); + + when(compasSclDataService.findLocationByUUID(uuid)).thenReturn(testData); + + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(location) + .when().get("/" + uuid) + .then() + .statusCode(200) + .extract() + .response(); + + Location result = response.as(Location.class); + assertEquals(uuid, UUID.fromString(result.getUuid())); + assertEquals(key, result.getKey()); + assertEquals(name, result.getName()); + assertEquals(description, result.getDescription()); + } + + @Test + void getLocations_WhenCalled_ThenReturnsLocations() { + UUID uuid = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + String key = "Key"; + String name = "Name"; + String description = "Description"; + Location location = new Location(); + location.setUuid(uuid.toString()); + location.setKey(key); + location.setName(name); + location.setDescription(description); + ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); + ILocationMetaItem testData2 = new LocationResourceTestDataBuilder().setId(uuid2.toString()).build(); + + when(compasSclDataService.listLocations(0, 25)).thenReturn(List.of(testData, testData2)); + + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(location) + .when().get("/") + .then() + .statusCode(200) + .extract() + .response(); + + List result = response.as(List.class); + List mappedResult = new ArrayList<>(); + result.stream().map(entry -> + new Location() + .uuid((String) entry.get("uuid")) + .key((String) entry.get("key")) + .name((String) entry.get("name")) + .description((String) entry.get("description"))) + .forEach(mappedResult::add); + assertEquals(uuid, UUID.fromString(mappedResult.get(0).getUuid())); + assertEquals(key, mappedResult.get(0).getKey()); + assertEquals(name, mappedResult.get(0).getName()); + assertEquals(description, mappedResult.get(0).getDescription()); + assertEquals(uuid2, UUID.fromString(mappedResult.get(1).getUuid())); + assertEquals(key, mappedResult.get(1).getKey()); + assertEquals(name, mappedResult.get(1).getName()); + assertEquals(description, mappedResult.get(1).getDescription()); + } + + @Test + void unassignResourceFromLocation_WhenSqlExceptionOccurs_ThenReturns500StatusCode() { + UUID locationUuid = UUID.randomUUID(); + UUID resourceUuid = UUID.randomUUID(); + + doThrow(new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error unassigning SCL Resource from Location in database!")) + .when(compasSclDataService).unassignResourceFromLocation(locationUuid, resourceUuid); + + given() + .contentType(MediaType.APPLICATION_JSON) + .when().post("/"+locationUuid+"/resources/"+resourceUuid+"/unassign") + .then() + .statusCode(500); + } + + @Test + void assignResourceToLocation_WhenSqlExceptionOccurs_ThenReturns500StatusCode() { + UUID locationUuid = UUID.randomUUID(); + UUID resourceUuid = UUID.randomUUID(); + + doThrow(new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error assigning SCL Resource from Location in database!")) + .when(compasSclDataService).assignResourceToLocation(locationUuid, resourceUuid); + + given() + .contentType(MediaType.APPLICATION_JSON) + .when().post("/"+locationUuid+"/resources/"+resourceUuid+"/assign") + .then() + .statusCode(500); + } + + @Test + void updateLocation_WhenCalled_ThenReturnsUpdatedLocation() { + UUID uuid = UUID.randomUUID(); + String key = "Key"; + String name = "Name"; + String description = "Description"; + Location location = new Location(); + location.setUuid(uuid.toString()); + location.setKey(key); + location.setName(name); + location.setDescription(description); + ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); + + when(compasSclDataService.updateLocation(uuid, key, name, description)).thenReturn(testData); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(location) + .when().put("/" + uuid) + .then() + .statusCode(200) + .extract() + .response(); + + Location result = response.as(Location.class); + assertEquals(uuid, UUID.fromString(result.getUuid())); + assertEquals(key, result.getKey()); + assertEquals(name, result.getName()); + assertEquals(description, result.getDescription()); + } +} \ No newline at end of file diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/LocationMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/LocationMetaItem.java new file mode 100644 index 00000000..d99f4fc2 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/LocationMetaItem.java @@ -0,0 +1,42 @@ +package org.lfenergy.compas.scl.data.model; + +public class LocationMetaItem implements ILocationMetaItem { + + String id; + String key; + String name; + String description; + int assignedResources; + + public LocationMetaItem(String id, String key, String name, String description, int assignedResources) { + this.id = id; + this.key = key; + this.name = name; + this.description = description; + this.assignedResources = assignedResources; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getDescription() { + return description; + } + + public int getAssignedResources() { + return assignedResources; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 357865c4..1168f42f 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -30,6 +30,9 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String MINOR_VERSION_FIELD = "minor_version"; private static final String PATCH_VERSION_FIELD = "patch_version"; private static final String NAME_FIELD = "name"; + private static final String LOCATIONMETAITEM_KEY_FIELD = "key"; + private static final String LOCATIONMETAITEM_DESCRIPTION_FIELD = "description"; + private static final String LOCATIONMETAITEM_RESOURCE_ID_FIELD = "resource_id"; private static final String SCL_DATA_FIELD = "scl_data"; private static final String HITEM_WHO_FIELD = "hitem_who"; private static final String HITEM_WHEN_FIELD = "hitem_when"; @@ -606,6 +609,143 @@ public List listHistoryVersionsByUUID(UUID id) { return executeHistoryQuery(sql, Collections.singletonList(id)); } + @Override + @Transactional(REQUIRED) + public ILocationMetaItem createLocation(UUID id, String key, String name, String description) { + String sql = """ + INSERT INTO location (id, key, name, description, resource_id) + VALUES (?, ?, ?, ?, null); + """; + + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, id); + sclStmt.setString(2, key); + sclStmt.setString(3, name); + sclStmt.setString(4, description == null ? "" : description); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding Location to database!", exp); + } + + return findLocationByUUID(id); + } + + @Override + @Transactional(SUPPORTS) + public List listLocations(int page, int pageSize) { + String sql = """ + SELECT * + FROM location + ORDER BY name + OFFSET ? + LIMIT ?; + """; + int offset = 0; + if (page > 1) { + offset = (page - 1) * pageSize; + } + return executeLocationQuery(sql, List.of(offset, pageSize)); + } + + @Override + @Transactional(SUPPORTS) + public ILocationMetaItem findLocationByUUID(UUID locationId) { + String sql = """ + SELECT * + FROM location + WHERE id = ? + ORDER BY name; + """; + List retrievedLocation = executeLocationQuery(sql, Collections.singletonList(locationId)); + if (retrievedLocation.isEmpty()) { + throw new CompasNoDataFoundException(String.format("Unable to find Location with id %s.", locationId)); + } + return retrievedLocation.get(0); + } + + @Override + @Transactional(REQUIRED) + public void deleteLocation(UUID locationId) { + String sql = """ + DELETE FROM location + WHERE id = ?; + """; + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, locationId); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_DELETE_ERROR_CODE, "Error removing Location from database", exp); + } + } + + @Override + @Transactional(REQUIRED) + public ILocationMetaItem updateLocation(UUID locationId, String key, String name, String description) { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append(""" + UPDATE location + SET key = ?, name = ? + """); + if (description != null && !description.isBlank()) { + sqlBuilder.append(", description = ?"); + sqlBuilder.append("\n"); + } + sqlBuilder.append("WHERE id = ?;"); + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sqlBuilder.toString())) { + sclStmt.setString(1, key); + sclStmt.setString(2, name); + if (description == null || description.isBlank()) { + sclStmt.setObject(3, locationId); + } else { + sclStmt.setString(3, description); + sclStmt.setObject(4, locationId); + } + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error updating Location in database!", exp); + } + return findLocationByUUID(locationId); + } + + @Override + @Transactional(REQUIRED) + public void assignResourceToLocation(UUID locationId, UUID resourceId) { + String sql = """ + UPDATE location + SET resource_id = ? + WHERE id = ?; + """; + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, resourceId); + sclStmt.setObject(2, locationId); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error assigning SCL Resource to Location in database!", exp); + } + } + + @Override + @Transactional(REQUIRED) + public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { + String sql = """ + UPDATE location + SET resource_id = NULL + WHERE id = ? AND resource_id = ?; + """; + try (var connection = dataSource.getConnection(); + var sclStmt = connection.prepareStatement(sql)) { + sclStmt.setObject(1, locationId); + sclStmt.setObject(2, resourceId); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error unassigning SCL Resource from Location in database!", exp); + } + } + private List executeHistoryQuery(String sql, List parameters) { List items = new ArrayList<>(); try ( @@ -649,4 +789,34 @@ private OffsetDateTime convertToOffsetDateTime(Timestamp sqlTimestamp) { ? sqlTimestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() : null; } + + private List executeLocationQuery(String sql, List parameters) { + List items = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + for (int i = 0; i < parameters.size(); i++) { + stmt.setObject(i + 1, parameters.get(i)); + } + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + items.add(mapResultSetToLocationMetaItem(resultSet)); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error listing Location entries from database!", exp); + } + return items; + } + + private LocationMetaItem mapResultSetToLocationMetaItem(ResultSet resultSet) throws SQLException { + UUID resourceId = resultSet.getObject(LOCATIONMETAITEM_RESOURCE_ID_FIELD, UUID.class); + int resourceCount = resourceId == null ? 0 : 1; + return new LocationMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(LOCATIONMETAITEM_KEY_FIELD), + resultSet.getString(NAME_FIELD), + resultSet.getString(LOCATIONMETAITEM_DESCRIPTION_FIELD), + resourceCount + ); + } } \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql new file mode 100644 index 00000000..fd240448 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql @@ -0,0 +1,19 @@ +-- +-- Creating table to hold Location Data. The Location is identified by its ID. +-- + +create table location ( + id uuid not null, + key varchar(255) not null, + name varchar(255) not null, + description varchar(255), + resource_id uuid default null, + primary key (id) +); + +comment on table location is 'Table holding all the Location Data and its resource assignments. The id is unique (pk).'; +comment on column location.id is 'Unique ID generated according to standards'; +comment on column location.key is 'The key of the Location'; +comment on column location.name is 'The name of the Location'; +comment on column location.description is 'The description of the Location'; +comment on column location.resource_id is 'The unique ID of the assigned resource, NULL if no resource is assigned'; diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java index 19fca817..2b98c400 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java @@ -16,6 +16,7 @@ public class CompasSclDataServiceErrorCode { public static final String DUPLICATE_SCL_NAME_ERROR_CODE = "SDS-0007"; public static final String INVALID_LABEL_ERROR_CODE = "SDS-0008"; public static final String TOO_MANY_LABEL_ERROR_CODE = "SDS-0009"; + public static final String LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE = "SDS-0009"; public static final String POSTGRES_SELECT_ERROR_CODE = "SDS-2000"; public static final String POSTGRES_INSERT_ERROR_CODE = "SDS-2001"; diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/ILocationMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/ILocationMetaItem.java new file mode 100644 index 00000000..baebdb8b --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/ILocationMetaItem.java @@ -0,0 +1,9 @@ +package org.lfenergy.compas.scl.data.model; + +public interface ILocationMetaItem { + String getId(); + String getKey(); + String getName(); + String getDescription(); + int getAssignedResources(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index a710b2b9..6027a8f8 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -159,4 +159,65 @@ public interface CompasSclDataRepository { List listHistoryVersionsByUUID(UUID id); void createHistoryVersion(UUID id, String name, Version version, SclFileType type, String author, String comment, OffsetDateTime changedAt, Boolean archived, Boolean available); + + /** + * Create a new Location + * + * @param id The uuid of the Location + * @param key The key of the Location + * @param name The name of the Location + * @param description The description of the Location + * @return The created Location + */ + ILocationMetaItem createLocation(UUID id, String key, String name, String description); + + /** + * List Location entries + * + * @param page The page number of the result + * @param pageSize The amount of Location entries on the page + * @return The specified page with the specified number of Location entries + */ + List listLocations(int page, int pageSize); + + /** + * Return the specific Location entry + * + * @param locationId The uuid of the Location + * @return The Meta Info of the searched Location + */ + ILocationMetaItem findLocationByUUID(UUID locationId); + + /** + * Delete the specified Location entry + * + * @param locationId The uuid of the Location + */ + void deleteLocation(UUID locationId); + + /** + * Updates an existing location + * + * @param locationId The uuid of the existing Location + * @param key The key of the updated Location + * @param name The name of the updated Location + * @param description The description of the updated Location + */ + ILocationMetaItem updateLocation(UUID locationId, String key, String name, String description); + + /** + * Assigns a resource to the specified location, if a resource is already assigned to a location, the previous assignment is removed + * + * @param locationId The uuid of the Location + * @param resourceId The uuid of the Resource + */ + void assignResourceToLocation(UUID locationId, UUID resourceId); + + /** + * Removes the resource assignment from the specified location + * + * @param locationId The uuid of the Location + * @param resourceId The uuid of the Resource + */ + void unassignResourceFromLocation(UUID locationId, UUID resourceId); } diff --git a/service/pom.xml b/service/pom.xml index a9d11023..af566ef1 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -23,7 +23,7 @@ SPDX-License-Identifier: Apache-2.0 org.glassfish.jaxb jaxb-runtime - provƒided + provided 4.0.5 diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 85057e34..01239dcd 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -10,9 +10,7 @@ import org.lfenergy.compas.core.commons.exception.CompasException; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; -import org.lfenergy.compas.scl.data.model.ChangeSetType; -import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; -import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; import org.lfenergy.compas.scl.data.util.SclElementProcessor; import org.lfenergy.compas.scl.data.xml.HistoryItem; @@ -467,4 +465,86 @@ public List listHistory(SclFileType type, String name, String public List listHistoryVersionsByUUID(UUID id) { return repository.listHistoryVersionsByUUID(id); } + + /** + * Find the Location with the specified uuid + * + * @param id uuid of the Location + * @return The Location entry with the matching id + */ + @Transactional(SUPPORTS) + public ILocationMetaItem findLocationByUUID(UUID id) { + return repository.findLocationByUUID(id); + } + + /** + * List all Locations + * + * @param page The current page number to be displayed + * @param pageSize The amount of entries per page + * @return All Location entries in a paginated list + */ + @Transactional(SUPPORTS) + public List listLocations(int page, int pageSize) { + return repository.listLocations(page, pageSize); + } + + /** + * Create a Location entry according to the supplied parameters + * + * @param key The key value of the Location entry + * @param name The name value of the Location entry + * @param description The description value of the Location entry + * @return The created Location entry + */ + @Transactional(REQUIRED) + public ILocationMetaItem createLocation(String key, String name, String description) { + return repository.createLocation(UUID.randomUUID(), key, name, description); + } + + /** + * Delete the Location entry with the supplied id + * + * @param id The id of the Location entry to be deleted + */ + @Transactional(REQUIRED) + public void deleteLocation(UUID id) { + repository.deleteLocation(id); + } + + /** + * Update a Location entry with the supplied parameter value + * + * @param id The id of the Location to be updated + * @param key The updated key of the Location entry + * @param name The updated name of the Location entry + * @param description The updated description of the Location entry + * @return The updated Location entry + */ + @Transactional(REQUIRED) + public ILocationMetaItem updateLocation(UUID id, String key, String name, String description) { + return repository.updateLocation(id, key, name, description); + } + + /** + * Assigns a Resource id to a Location entry + * + * @param locationId The id of the Location entry + * @param resourceId The id of the Resource + */ + @Transactional(REQUIRED) + public void assignResourceToLocation(UUID locationId, UUID resourceId) { + repository.assignResourceToLocation(locationId, resourceId); + } + + /** + * Unassigns a Resource id from a Location entry + * + * @param locationId The id of the Location entry + * @param resourceId The id of the Resource entry + */ + @Transactional(REQUIRED) + public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { + repository.unassignResourceFromLocation(locationId, resourceId); + } } From bdd0a2c54edb33152116b9e9847d9dc88c53091b Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 19 Nov 2024 14:46:40 +0100 Subject: [PATCH 05/23] feat: implement archiving api search endpoint --- app/pom.xml | 2 +- .../rest/api/archive/ArchiveResource.java | 103 +++++ .../data/rest/api/archive/ArchivingApi.java | 34 ++ .../api/archive/model/ArchivedResource.java | 357 ++++++++++++++++++ .../api/archive/model/ArchivedResources.java | 94 +++++ .../model/ArchivedResourcesSearch.java | 252 +++++++++++++ .../rest/api/archive/model/ResourceTag.java | 98 +++++ .../rest/api/locations/LocationsResource.java | 9 - repository-postgresql/pom.xml | 8 + .../data/model/ArchivedResourceMetaItem.java | 82 ++++ .../data/model/ArchivedResourcesMetaItem.java | 17 + .../scl/data/model/ResourceTagItem.java | 28 ++ .../CompasSclDataPostgreSQLRepository.java | 140 +++++++ .../V1_10__fill_archived_resource.sql | 12 + .../db/migration/V1_11__fill_resource_tag.sql | 12 + .../V1_8__create_archived_resource.sql | 36 ++ .../migration/V1_9__create_resource_tag.sql | 17 + .../CompasSclDataServiceErrorCode.java | 3 +- .../data/model/IArchivedResourceMetaItem.java | 26 ++ .../model/IArchivedResourcesMetaItem.java | 7 + .../scl/data/model/IResourceTagItem.java | 8 + .../repository/CompasSclDataRepository.java | 10 + .../data/service/CompasSclDataService.java | 56 ++- 23 files changed, 1394 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ResourceTagItem.java create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IResourceTagItem.java diff --git a/app/pom.xml b/app/pom.xml index 2c7853ba..fc676c14 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -223,7 +223,7 @@ SPDX-License-Identifier: Apache-2.0 - + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java new file mode 100644 index 00000000..9d06c076 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -0,0 +1,103 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import org.lfenergy.compas.scl.data.model.IArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.IArchivedResourcesMetaItem; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesSearch; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ResourceTag; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; + +import java.io.File; +import java.time.OffsetDateTime; +import java.util.UUID; + +@RequestScoped +public class ArchiveResource implements ArchivingApi { + + private final CompasSclDataService compasSclDataService; + + @Inject + public ArchiveResource(CompasSclDataService compasSclDataService) { + this.compasSclDataService = compasSclDataService; + } + + @Override + public Uni archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { + return Uni.createFrom() + .item(() -> compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToArchivedResource); + } + + @Override + public Uni archiveSclResource(UUID id, String version) { + return Uni.createFrom() + .item(() -> compasSclDataService.archiveSclResource(id, version)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToArchivedResource); + } + + @Override + public Uni searchArchivedResources(ArchivedResourcesSearch archivedResourcesSearch) { + return Uni.createFrom() + .item(() -> getArchivedResourcesMetaItem(archivedResourcesSearch)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToArchivedResources); + } + + private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResourcesSearch archivedResourcesSearch) { + String uuid = archivedResourcesSearch.getUuid(); + if (uuid != null && !uuid.isBlank()) { + return compasSclDataService.searchArchivedResources(UUID.fromString(uuid)); + } + + String location = archivedResourcesSearch.getLocation(); + String name = archivedResourcesSearch.getName(); + String approver = archivedResourcesSearch.getApprover(); + String contentType = archivedResourcesSearch.getContentType(); + String type = archivedResourcesSearch.getType(); + String voltage = archivedResourcesSearch.getVoltage(); + OffsetDateTime from = archivedResourcesSearch.getFrom(); + OffsetDateTime to = archivedResourcesSearch.getTo(); + return compasSclDataService.searchArchivedResources(location, name, approver, contentType, type, voltage, from, to); + } + + private ArchivedResource mapToArchivedResource(IArchivedResourceMetaItem archivedResource) { + return new ArchivedResource() + .uuid(archivedResource.getId()) + .location(archivedResource.getLocation()) + .name(archivedResource.getName()) + .note(archivedResource.getNote()) + .author(archivedResource.getAuthor()) + .approver(archivedResource.getApprover()) + .type(archivedResource.getType()) + .contentType(archivedResource.getContentType()) + .voltage(archivedResource.getVoltage()) + .version(archivedResource.getVersion()) + .modifiedAt(archivedResource.getModifiedAt()) + .archivedAt(archivedResource.getArchivedAt()) + .fields( + archivedResource.getFields() + .stream() + .map(item -> new ResourceTag().key(item.getKey()).value(item.getValue())).toList() + ); + } + + private ArchivedResources mapToArchivedResources(IArchivedResourcesMetaItem archivedResources) { + return new ArchivedResources() + .resources( + archivedResources.getResources() + .stream() + .map(this::mapToArchivedResource) + .toList() + ); + } +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java new file mode 100644 index 00000000..49a3ba56 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java @@ -0,0 +1,34 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import io.smallrye.mutiny.Uni; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesSearch; + +import java.io.File; +import java.util.UUID; + + +@Path("/api/archive") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T15:39:21.464141400+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public interface ArchivingApi { + + @POST + @Path("/referenced-resource/{id}/versions/{version}") + @Consumes({ "application/octet-stream" }) + @Produces({ "application/json" }) + Uni archiveResource(@PathParam("id") UUID id, @PathParam("version") String version, @HeaderParam("X-author") String xAuthor, @HeaderParam("X-approver") String xApprover, @HeaderParam("Content-Type") String contentType, @HeaderParam("X-filename") String xFilename, @Valid File body); + + @POST + @Path("/scl/{id}/versions/{version}") + @Produces({ "application/json" }) + Uni archiveSclResource(@PathParam("id") UUID id,@PathParam("version") String version); + + @POST + @Path("/resources/search") + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + Uni searchArchivedResources(@Valid ArchivedResourcesSearch archivedResourcesSearch); +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java new file mode 100644 index 00000000..e5e507ad --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java @@ -0,0 +1,357 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + + +@JsonTypeName("ArchivedResource") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T15:39:21.464141400+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ArchivedResource { + private String uuid; + private String location; + private String name; + private String note; + private String author; + private String approver; + private String type; + private String contentType; + private String voltage; + private String version; + private OffsetDateTime modifiedAt; + private OffsetDateTime archivedAt; + private @Valid List<@Valid ResourceTag> fields = new ArrayList<>(); + + /** + * Unique resource identifier + **/ + public ArchivedResource uuid(String uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + @NotNull public String getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Location of the resource, might be empty + **/ + public ArchivedResource location(String location) { + this.location = location; + return this; + } + + + @JsonProperty("location") + public String getLocation() { + return location; + } + + @JsonProperty("location") + public void setLocation(String location) { + this.location = location; + } + + /** + * Resource name + **/ + public ArchivedResource name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + * Versioning note + **/ + public ArchivedResource note(String note) { + this.note = note; + return this; + } + + + @JsonProperty("note") + public String getNote() { + return note; + } + + @JsonProperty("note") + public void setNote(String note) { + this.note = note; + } + + /** + * Modifying author + **/ + public ArchivedResource author(String author) { + this.author = author; + return this; + } + + + @JsonProperty("author") + @NotNull public String getAuthor() { + return author; + } + + @JsonProperty("author") + public void setAuthor(String author) { + this.author = author; + } + + /** + * Name of the approver + **/ + public ArchivedResource approver(String approver) { + this.approver = approver; + return this; + } + + + @JsonProperty("approver") + public String getApprover() { + return approver; + } + + @JsonProperty("approver") + public void setApprover(String approver) { + this.approver = approver; + } + + /** + * Content type + **/ + public ArchivedResource type(String type) { + this.type = type; + return this; + } + + + @JsonProperty("type") + public String getType() { + return type; + } + + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + + /** + * Content type + **/ + public ArchivedResource contentType(String contentType) { + this.contentType = contentType; + return this; + } + + + @JsonProperty("contentType") + @NotNull public String getContentType() { + return contentType; + } + + @JsonProperty("contentType") + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Content type + **/ + public ArchivedResource voltage(String voltage) { + this.voltage = voltage; + return this; + } + + + @JsonProperty("voltage") + public String getVoltage() { + return voltage; + } + + @JsonProperty("voltage") + public void setVoltage(String voltage) { + this.voltage = voltage; + } + + /** + * Version + **/ + public ArchivedResource version(String version) { + this.version = version; + return this; + } + + + @JsonProperty("version") + @NotNull public String getVersion() { + return version; + } + + @JsonProperty("version") + public void setVersion(String version) { + this.version = version; + } + + /** + **/ + public ArchivedResource modifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + return this; + } + + + @JsonProperty("modifiedAt") + @NotNull public OffsetDateTime getModifiedAt() { + return modifiedAt; + } + + @JsonProperty("modifiedAt") + public void setModifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + } + + /** + **/ + public ArchivedResource archivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + return this; + } + + + @JsonProperty("archivedAt") + @NotNull public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + @JsonProperty("archivedAt") + public void setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + } + + /** + **/ + public ArchivedResource fields(List<@Valid ResourceTag> fields) { + this.fields = fields; + return this; + } + + + @JsonProperty("fields") + @NotNull @Valid public List<@Valid ResourceTag> getFields() { + return fields; + } + + @JsonProperty("fields") + public void setFields(List<@Valid ResourceTag> fields) { + this.fields = fields; + } + + public ArchivedResource addFieldsItem(ResourceTag fieldsItem) { + if (this.fields == null) { + this.fields = new ArrayList<>(); + } + + this.fields.add(fieldsItem); + return this; + } + + public ArchivedResource removeFieldsItem(ResourceTag fieldsItem) { + if (fieldsItem != null && this.fields != null) { + this.fields.remove(fieldsItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArchivedResource archivedResource = (ArchivedResource) o; + return Objects.equals(this.uuid, archivedResource.uuid) && + Objects.equals(this.location, archivedResource.location) && + Objects.equals(this.name, archivedResource.name) && + Objects.equals(this.note, archivedResource.note) && + Objects.equals(this.author, archivedResource.author) && + Objects.equals(this.approver, archivedResource.approver) && + Objects.equals(this.type, archivedResource.type) && + Objects.equals(this.contentType, archivedResource.contentType) && + Objects.equals(this.voltage, archivedResource.voltage) && + Objects.equals(this.version, archivedResource.version) && + Objects.equals(this.modifiedAt, archivedResource.modifiedAt) && + Objects.equals(this.archivedAt, archivedResource.archivedAt) && + Objects.equals(this.fields, archivedResource.fields); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, location, name, note, author, approver, type, contentType, voltage, version, modifiedAt, archivedAt, fields); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ArchivedResource {\n"); + + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" note: ").append(toIndentedString(note)).append("\n"); + sb.append(" author: ").append(toIndentedString(author)).append("\n"); + sb.append(" approver: ").append(toIndentedString(approver)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); + sb.append(" voltage: ").append(toIndentedString(voltage)).append("\n"); + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" modifiedAt: ").append(toIndentedString(modifiedAt)).append("\n"); + sb.append(" archivedAt: ").append(toIndentedString(archivedAt)).append("\n"); + sb.append(" fields: ").append(toIndentedString(fields)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java new file mode 100644 index 00000000..b39c290a --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java @@ -0,0 +1,94 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + + +@JsonTypeName("ArchivedResources") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T15:39:21.464141400+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ArchivedResources { + private @Valid List<@Valid ArchivedResource> resources = new ArrayList<>(); + + /** + **/ + public ArchivedResources resources(List<@Valid ArchivedResource> resources) { + this.resources = resources; + return this; + } + + + @JsonProperty("resources") + @NotNull @Valid public List<@Valid ArchivedResource> getResources() { + return resources; + } + + @JsonProperty("resources") + public void setResources(List<@Valid ArchivedResource> resources) { + this.resources = resources; + } + + public ArchivedResources addResourcesItem(ArchivedResource resourcesItem) { + if (this.resources == null) { + this.resources = new ArrayList<>(); + } + + this.resources.add(resourcesItem); + return this; + } + + public ArchivedResources removeResourcesItem(ArchivedResource resourcesItem) { + if (resourcesItem != null && this.resources != null) { + this.resources.remove(resourcesItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArchivedResources archivedResources = (ArchivedResources) o; + return Objects.equals(this.resources, archivedResources.resources); + } + + @Override + public int hashCode() { + return Objects.hash(resources); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ArchivedResources {\n"); + + sb.append(" resources: ").append(toIndentedString(resources)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java new file mode 100644 index 00000000..ed6e7442 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java @@ -0,0 +1,252 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.time.OffsetDateTime; +import java.util.Objects; + + + +@JsonTypeName("ArchivedResourcesSearch") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T15:39:21.464141400+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ArchivedResourcesSearch { + private String uuid; + private String location; + private String name; + private String approver; + private String contentType; + private String type; + private String voltage; + private OffsetDateTime from; + private OffsetDateTime to; + + /** + * If uuid is set no other filter must be set + **/ + public ArchivedResourcesSearch uuid(String uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + public String getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Exact match of a location + **/ + public ArchivedResourcesSearch location(String location) { + this.location = location; + return this; + } + + + @JsonProperty("location") + public String getLocation() { + return location; + } + + @JsonProperty("location") + public void setLocation(String location) { + this.location = location; + } + + /** + * Partially match allowed + **/ + public ArchivedResourcesSearch name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + * Fulltext match which can be retrieved via extra endpoint + **/ + public ArchivedResourcesSearch approver(String approver) { + this.approver = approver; + return this; + } + + + @JsonProperty("approver") + public String getApprover() { + return approver; + } + + @JsonProperty("approver") + public void setApprover(String approver) { + this.approver = approver; + } + + /** + * Fulltext match set to one of the supported scl types: SSD, IID, ICD, SCD, CID, SED, ISD, STD, etc. + **/ + public ArchivedResourcesSearch contentType(String contentType) { + this.contentType = contentType; + return this; + } + + + @JsonProperty("contentType") + public String getContentType() { + return contentType; + } + + @JsonProperty("contentType") + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Type of the documented entity eg. Schütz, Leittechnik, etc + **/ + public ArchivedResourcesSearch type(String type) { + this.type = type; + return this; + } + + + @JsonProperty("type") + public String getType() { + return type; + } + + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + + /** + * Voltage of the documented entity eg. 110, 220, 380 + **/ + public ArchivedResourcesSearch voltage(String voltage) { + this.voltage = voltage; + return this; + } + + + @JsonProperty("voltage") + public String getVoltage() { + return voltage; + } + + @JsonProperty("voltage") + public void setVoltage(String voltage) { + this.voltage = voltage; + } + + /** + * Starting date from where resources have been archived. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z). + **/ + public ArchivedResourcesSearch from(OffsetDateTime from) { + this.from = from; + return this; + } + + + @JsonProperty("from") + public OffsetDateTime getFrom() { + return from; + } + + @JsonProperty("from") + public void setFrom(OffsetDateTime from) { + this.from = from; + } + + /** + * Ending date from where resources have been archived. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z). + **/ + public ArchivedResourcesSearch to(OffsetDateTime to) { + this.to = to; + return this; + } + + + @JsonProperty("to") + public OffsetDateTime getTo() { + return to; + } + + @JsonProperty("to") + public void setTo(OffsetDateTime to) { + this.to = to; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArchivedResourcesSearch archivedResourcesSearch = (ArchivedResourcesSearch) o; + return Objects.equals(this.uuid, archivedResourcesSearch.uuid) && + Objects.equals(this.location, archivedResourcesSearch.location) && + Objects.equals(this.name, archivedResourcesSearch.name) && + Objects.equals(this.approver, archivedResourcesSearch.approver) && + Objects.equals(this.contentType, archivedResourcesSearch.contentType) && + Objects.equals(this.type, archivedResourcesSearch.type) && + Objects.equals(this.voltage, archivedResourcesSearch.voltage) && + Objects.equals(this.from, archivedResourcesSearch.from) && + Objects.equals(this.to, archivedResourcesSearch.to); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, location, name, approver, contentType, type, voltage, from, to); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ArchivedResourcesSearch {\n"); + + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" approver: ").append(toIndentedString(approver)).append("\n"); + sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" voltage: ").append(toIndentedString(voltage)).append("\n"); + sb.append(" from: ").append(toIndentedString(from)).append("\n"); + sb.append(" to: ").append(toIndentedString(to)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java new file mode 100644 index 00000000..fd6b736b --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java @@ -0,0 +1,98 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.util.Objects; + + + +@JsonTypeName("ResourceTag") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T07:52:46.875467800+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ResourceTag { + private String key; + private String value; + + /** + * Tag key + **/ + public ResourceTag key(String key) { + this.key = key; + return this; + } + + + @JsonProperty("key") + @NotNull public String getKey() { + return key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + * Tag value + **/ + public ResourceTag value(String value) { + this.value = value; + return this; + } + + + @JsonProperty("value") + @NotNull public String getValue() { + return value; + } + + @JsonProperty("value") + public void setValue(String value) { + this.value = value; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceTag resourceTag = (ResourceTag) o; + return Objects.equals(this.key, resourceTag.key) && + Objects.equals(this.value, resourceTag.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ResourceTag {\n"); + + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" value: ").append(toIndentedString(value)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java index f53cf35a..c1f75862 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -1,13 +1,11 @@ package org.lfenergy.compas.scl.data.rest.api.locations; -import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.infrastructure.Infrastructure; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; import org.lfenergy.compas.scl.data.rest.api.locations.model.Location; import org.lfenergy.compas.scl.data.service.CompasSclDataService; @@ -15,8 +13,6 @@ import java.util.List; import java.util.UUID; -import static org.lfenergy.compas.scl.data.exception.CompasSclDataServiceErrorCode.LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE; - @RequestScoped public class LocationsResource implements LocationsApi { private static final Logger LOGGER = LogManager.getLogger(LocationsResource.class); @@ -49,11 +45,6 @@ public Uni createLocation(Location location) { @Override public Uni deleteLocation(UUID locationId) { - int assignedResourceCount = compasSclDataService.findLocationByUUID(locationId).getAssignedResources(); - if (assignedResourceCount > 0) { - return Uni.createFrom().failure(new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, - String.format("Deletion of Location %s not allowed, unassign resources before deletion", locationId))); - } compasSclDataService.deleteLocation(locationId); return Uni.createFrom().nullItem(); } diff --git a/repository-postgresql/pom.xml b/repository-postgresql/pom.xml index 7e8b9f98..addc60e5 100644 --- a/repository-postgresql/pom.xml +++ b/repository-postgresql/pom.xml @@ -85,6 +85,14 @@ SPDX-License-Identifier: Apache-2.0 org.jboss.jandex jandex-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + 15 + 15 + + diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java new file mode 100644 index 00000000..4c3e943c --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java @@ -0,0 +1,82 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public class ArchivedResourceMetaItem extends AbstractItem implements IArchivedResourceMetaItem { + + String location; + String note; + String author; + String approver; + String type; + String contentType; + String voltage; + OffsetDateTime modifiedAt; + OffsetDateTime archivedAt; + List fields; + + public ArchivedResourceMetaItem(String id, String name, String version, String location, String note, String author, String approver, String type, String contentType, String voltage, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, List fields) { + super(id, name, version); + this.location = location; + this.note = note; + this.author = author; + this.approver = approver; + this.type = type; + this.contentType = contentType; + this.voltage = voltage; + this.modifiedAt = modifiedAt; + this.archivedAt = archivedAt; + this.fields = fields; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public String getNote() { + return note; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public String getApprover() { + return approver; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getVoltage() { + return voltage; + } + + @Override + public OffsetDateTime getModifiedAt() { + return modifiedAt; + } + + @Override + public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + @Override + public List getFields() { + return fields; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java new file mode 100644 index 00000000..8afd3870 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java @@ -0,0 +1,17 @@ +package org.lfenergy.compas.scl.data.model; + +import java.util.List; + +public class ArchivedResourcesMetaItem implements IArchivedResourcesMetaItem { + + List archivedResources; + + public ArchivedResourcesMetaItem(List archivedResources) { + this.archivedResources = archivedResources; + } + + @Override + public List getResources() { + return archivedResources; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ResourceTagItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ResourceTagItem.java new file mode 100644 index 00000000..8b2fcbd2 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ResourceTagItem.java @@ -0,0 +1,28 @@ +package org.lfenergy.compas.scl.data.model; + +public class ResourceTagItem implements IResourceTagItem { + String id; + String key; + String value; + + public ResourceTagItem(String id, String key, String value) { + this.id = id; + this.key = key; + this.value = value; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 1168f42f..fe5b7fa0 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -14,6 +14,7 @@ import org.lfenergy.compas.scl.extensions.model.SclFileType; import javax.sql.DataSource; +import java.io.File; import java.sql.*; import java.time.OffsetDateTime; import java.time.ZoneId; @@ -44,6 +45,15 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String HISTORYMETAITEM_AVAILABLE_FIELD = "available"; private static final String HISTORYMETAITEM_ARCHIVED_FIELD = "archived"; private static final String HISTORYMETAITEM_IS_DELETED_FIELD = "is_deleted"; + private static final String ARCHIVEMETAITEM_LOCATION_FIELD = "location"; + private static final String ARCHIVEMETAITEM_NOTE_FIELD = "note"; + private static final String ARCHIVEMETAITEM_AUTHOR_FIELD = "author"; + private static final String ARCHIVEMETAITEM_APPROVER_FIELD = "approver"; + private static final String ARCHIVEMETAITEM_TYPE_FIELD = "type"; + private static final String ARCHIVEMETAITEM_CONTENT_TYPE_FIELD = "content_type"; + private static final String ARCHIVEMETAITEM_VOLTAGE_FIELD = "voltage"; + private static final String ARCHIVEMETAITEM_MODIFIED_AT_FIELD = "modified_at"; + private static final String ARCHIVEMETAITEM_ARCHIVED_AT_FIELD = "archived_at"; private final DataSource dataSource; @@ -746,6 +756,78 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { } } + @Override + public IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { + return null; + } + + @Override + public IArchivedResourceMetaItem archiveSclResource(UUID id, String version) { + return null; + } + + @Override + public IArchivedResourcesMetaItem searchArchivedResource(UUID id) { + String archivedResourcesSql = """ + SELECT ar.*, ARRAY_AGG (rt.id || ';' || rt.key || ';' || rt.value) AS tags + FROM archived_resource ar LEFT OUTER JOIN resource_tag rt + ON ar.id=rt.archived_resource_id + WHERE ar.id = ? + GROUP BY ar.id, ar.name, ar.major_version, ar.minor_version, ar.patch_version, ar.location, ar.note, ar.approver, ar.type, ar.content_type, ar.voltage, ar.modified_at, ar.archived_at; + """; + List result = executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(id)); + return new ArchivedResourcesMetaItem(result); + } + + @Override + public IArchivedResourcesMetaItem searchArchivedResource(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to) { + List parameters = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + sb.append(""" + SELECT ar.*, ARRAY_AGG (rt.id || ';' || rt.key || ';' || rt.value) AS tags + FROM archived_resource ar LEFT OUTER JOIN resource_tag rt + ON ar.id=rt.archived_resource_id + WHERE 1=1 + """); + + if (location != null && !location.isBlank()) { + parameters.add(location); + sb.append(" AND ar.location = ?"); + } + if (name != null && !name.isBlank()) { + parameters.add("%"+name+"%"); + sb.append(" AND ar.name ILIKE ?"); + } + if (approver != null && !approver.isBlank()) { + parameters.add(approver); + sb.append(" AND ar.approver = ?"); + } + if (contentType != null && !contentType.isBlank()) { + parameters.add(contentType); + sb.append(" AND ar.content_type = ?"); + } + if (type != null && !type.isBlank()) { + parameters.add(type); + sb.append(" AND ar.type = ?"); + } + if (voltage != null && !voltage.isBlank()) { + parameters.add(voltage); + sb.append(" AND ar.voltage = ?"); + } + if (from != null) { + parameters.add(from); + sb.append(" AND ar.archived_at >= ?"); + } + if (to != null) { + parameters.add(to); + sb.append(" AND ar.archived_at <= ?"); + } + sb.append(System.lineSeparator()); + sb.append("GROUP BY ar.id, ar.name, ar.major_version, ar.minor_version, ar.patch_version, ar.location, ar.note, ar.approver, ar.type, ar.content_type, ar.voltage, ar.modified_at, ar.archived_at;"); + return new ArchivedResourcesMetaItem(executeArchivedResourceQuery(sb.toString(), parameters)); + } + + private List executeHistoryQuery(String sql, List parameters) { List items = new ArrayList<>(); try ( @@ -819,4 +901,62 @@ private LocationMetaItem mapResultSetToLocationMetaItem(ResultSet resultSet) thr resourceCount ); } + + private List executeArchivedResourceQuery(String sql, List parameters) { + List items = new ArrayList<>(); + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + for (int i = 0; i < parameters.size(); i++) { + stmt.setObject(i + 1, parameters.get(i)); + } + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + List resourceTags = getResourceTagItems(resultSet); + items.add(mapResultSetToArchivedResourceMetaItem(resultSet, resourceTags)); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException( + POSTGRES_SELECT_ERROR_CODE, + "Error listing Archived Resource entries from database!", exp + ); + } + return items; + } + + private List getResourceTagItems(ResultSet resultSet) throws SQLException { + Array tags = resultSet.getArray("tags"); + List resourceTags = new ArrayList<>(); + if (tags != null) { + Arrays.stream((String[]) tags.getArray()) + .filter(Objects::nonNull) + .map(entry -> + new ResourceTagItem( + entry.split(";")[0], + entry.split(";")[1], + entry.split(";")[2] + ) + ).forEach(resourceTags::add); + } + return resourceTags; + } + + private ArchivedResourceMetaItem mapResultSetToArchivedResourceMetaItem(ResultSet resultSet, List resourceTags) throws SQLException { + return new ArchivedResourceMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + createVersion(resultSet), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + resultSet.getString(ARCHIVEMETAITEM_NOTE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), + resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), + resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_CONTENT_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_VOLTAGE_FIELD), + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_MODIFIED_AT_FIELD)), + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_ARCHIVED_AT_FIELD)), + resourceTags + ); + } } \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql new file mode 100644 index 00000000..f12ec256 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql @@ -0,0 +1,12 @@ +--clear values on startup + +TRUNCATE TABLE archived_resource; + +insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) +values ('af71f378-5a13-47af-bbfc-aa668af14e25', 'archived_resource_1', '0', '0', '1', 'test1', 'test note', 'user1', 'user2', 'Schütz', 'SSD', '110', '2019-08-24T14:15:22Z', '2019-08-24T14:15:22Z'); + +insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) +values ('af71f378-5a13-47af-bbfc-aa668af14e26', 'archived_resource_2', '0', '1', '0', 'test2', 'test note', 'user2', 'user1', 'Leittechnik', 'IID', '220', '2020-08-24T14:15:22Z', '2021-08-24T14:15:22Z'); + +insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) +values ('af71f378-5a13-47af-bbfc-aa668af14e27', 'archived_resource_3', '1', '0', '0', 'test3', 'test note', 'user3', 'user1', 'Schütz', 'ICD', '380', '2022-08-24T14:15:22Z', '2023-08-24T14:15:22Z'); \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql new file mode 100644 index 00000000..4833765c --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql @@ -0,0 +1,12 @@ +--clear values on startup + +TRUNCATE TABLE resource_tag; + +insert into resource_tag (id, key, value, archived_resource_id) +values ('bf71f378-5a13-47af-bbfc-aa668af14e25', 'test_tag', 'test_value', 'af71f378-5a13-47af-bbfc-aa668af14e25'); + +insert into resource_tag (id, key, value, archived_resource_id) +values ('bf71f378-5a13-47af-bbfc-aa668af15e25', 'test_tag_1', 'test_value_1', 'af71f378-5a13-47af-bbfc-aa668af14e25'); + +insert into resource_tag (id, key, value, archived_resource_id) +values ('bf71f378-5a13-47af-bbfc-aa668af74e25', 'test_tag', 'test_value', 'af71f378-5a13-47af-bbfc-aa668af14e27'); \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql new file mode 100644 index 00000000..4d58dee1 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql @@ -0,0 +1,36 @@ +-- +-- Creating table to hold Archived Resource Data. A Archived Resource is identified by its ID, major-, minor- and patch version. +-- + +create table archived_resource ( + id uuid not null, + name varchar(255) not null, + major_version smallint not null, + minor_version smallint not null, + patch_version smallint not null, + location varchar(255), + note varchar(255), + author varchar(255), + approver varchar(255), + type varchar(255), + content_type varchar(3), + voltage varchar(255), + modified_at TIMESTAMP WITH TIME ZONE not null, + archived_at TIMESTAMP WITH TIME ZONE not null, + primary key (id, major_version, minor_version, patch_version) +); + +comment on table archived_resource is 'Table holding all the Archived Resource Data and its location assignments. The id, the major-, minor- and patch versions are unique (pk).'; +comment on column archived_resource.id is 'Unique ID generated according to standards'; +comment on column archived_resource.name is 'The name of the Location'; +comment on column archived_resource.major_version is 'The major version of the Archived Resource'; +comment on column archived_resource.minor_version is 'The minor version of the Archived Resource'; +comment on column archived_resource.patch_version is 'The patch version of the Archived Resource'; +comment on column archived_resource.location is 'The assigned Location of the Archived Resource'; +comment on column archived_resource.note is 'The note of Archived Resource'; +comment on column archived_resource.approver is 'The approver of the Archived Resource'; +comment on column archived_resource.type is 'The type of the Archived Resource'; +comment on column archived_resource.content_type is 'The content type of the Archived Resource'; +comment on column archived_resource.voltage is 'The voltage of the Archived Resource'; +comment on column archived_resource.modified_at is 'The modified timestamp of the Archived Resource'; +comment on column archived_resource.archived_at is 'The archivedAt timestamp of the Archived Resource'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql new file mode 100644 index 00000000..96ecdd8b --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql @@ -0,0 +1,17 @@ +-- +-- Creating table to hold Resource Tag Data. The Resource Tag is identified by its ID. +-- + +create table resource_tag ( + id uuid not null, + key varchar(255) not null, + value varchar(255), + archived_resource_id uuid, + primary key (id) +); + +comment on table resource_tag is 'Table holding all the Resource Tag data. The id is unique (pk).'; +comment on column resource_tag.id is 'Unique ID generated according to standards'; +comment on column resource_tag.key is 'The key of the Resource Tag'; +comment on column resource_tag.value is 'The value of the Resource Tag'; +comment on column resource_tag.archived_resource_id is 'The uuid of a archived resource associated to the Resource Tag'; \ No newline at end of file diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java index 2b98c400..afc59262 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java @@ -16,7 +16,8 @@ public class CompasSclDataServiceErrorCode { public static final String DUPLICATE_SCL_NAME_ERROR_CODE = "SDS-0007"; public static final String INVALID_LABEL_ERROR_CODE = "SDS-0008"; public static final String TOO_MANY_LABEL_ERROR_CODE = "SDS-0009"; - public static final String LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE = "SDS-0009"; + public static final String LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE = "SDS-0010"; + public static final String INVALID_SCL_CONTENT_TYPE_ERROR_CODE = "SDS-0011"; public static final String POSTGRES_SELECT_ERROR_CODE = "SDS-2000"; public static final String POSTGRES_INSERT_ERROR_CODE = "SDS-2001"; diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java new file mode 100644 index 00000000..8ee29c31 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java @@ -0,0 +1,26 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface IArchivedResourceMetaItem extends IAbstractItem { + String getLocation(); + + String getNote(); + + String getAuthor(); + + String getApprover(); + + String getType(); + + String getContentType(); + + String getVoltage(); + + OffsetDateTime getModifiedAt(); + + OffsetDateTime getArchivedAt(); + + List getFields(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java new file mode 100644 index 00000000..878dcaa5 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java @@ -0,0 +1,7 @@ +package org.lfenergy.compas.scl.data.model; + +import java.util.List; + +public interface IArchivedResourcesMetaItem { + List getResources(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IResourceTagItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IResourceTagItem.java new file mode 100644 index 00000000..99c8ec64 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IResourceTagItem.java @@ -0,0 +1,8 @@ +package org.lfenergy.compas.scl.data.model; + +public interface IResourceTagItem { + String getId(); + String getKey(); + String getValue(); +} + \ No newline at end of file diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index 6027a8f8..0a65ea5c 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -6,6 +6,7 @@ import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.extensions.model.SclFileType; +import java.io.File; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -220,4 +221,13 @@ public interface CompasSclDataRepository { * @param resourceId The uuid of the Resource */ void unassignResourceFromLocation(UUID locationId, UUID resourceId); + + IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body); + + IArchivedResourceMetaItem archiveSclResource(UUID id, String version); + + IArchivedResourcesMetaItem searchArchivedResource(UUID id); + + IArchivedResourcesMetaItem searchArchivedResource(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to); + } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 01239dcd..ef01381a 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -21,13 +21,11 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; +import java.io.File; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.time.OffsetDateTime; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import static jakarta.transaction.Transactional.TxType.REQUIRED; @@ -509,6 +507,11 @@ public ILocationMetaItem createLocation(String key, String name, String descript */ @Transactional(REQUIRED) public void deleteLocation(UUID id) { + int assignedResourceCount = repository.findLocationByUUID(id).getAssignedResources(); + if (assignedResourceCount > 0) { + throw new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, + String.format("Deletion of Location %s not allowed, unassign resources before deletion", id)); + } repository.deleteLocation(id); } @@ -534,7 +537,10 @@ public ILocationMetaItem updateLocation(UUID id, String key, String name, String */ @Transactional(REQUIRED) public void assignResourceToLocation(UUID locationId, UUID resourceId) { - repository.assignResourceToLocation(locationId, resourceId); + ILocationMetaItem item = repository.findLocationByUUID(locationId); + if (item != null) { + repository.assignResourceToLocation(locationId, resourceId); + } } /** @@ -545,6 +551,44 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { */ @Transactional(REQUIRED) public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { - repository.unassignResourceFromLocation(locationId, resourceId); + ILocationMetaItem item = repository.findLocationByUUID(locationId); + if (item != null) { + repository.unassignResourceFromLocation(locationId, resourceId); + } + } + + @Transactional(REQUIRED) + public IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { + return repository.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body); + } + + @Transactional(REQUIRED) + public IArchivedResourceMetaItem archiveSclResource(UUID id, String version) { + return repository.archiveSclResource(id, version); + } + + @Transactional(SUPPORTS) + public IArchivedResourcesMetaItem searchArchivedResources(UUID uuid) { + return repository.searchArchivedResource(uuid); + } + + @Transactional(SUPPORTS) + public IArchivedResourcesMetaItem searchArchivedResources(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to) { + return repository.searchArchivedResource(location, name, approver, getSclFileType(contentType), type, voltage, from, to); + } + + private String getSclFileType(String contentType) { + if (contentType != null && !contentType.isBlank()) { + boolean isInvalidSclType = Arrays.stream(SclFileType.values()) + .noneMatch(sclFileType -> + sclFileType.name().equalsIgnoreCase(contentType) + ); + if (isInvalidSclType) { + throw new CompasSclDataServiceException(INVALID_SCL_CONTENT_TYPE_ERROR_CODE, + "Content type " + contentType + " is no valid SCL file type"); + } + return SclFileType.valueOf(contentType.toUpperCase()).name(); + } + return contentType; } } From 3e2de8828a53b94c88724044eca605fc47521829 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 12 Dec 2024 16:20:04 +0100 Subject: [PATCH 06/23] feat: implement archiving api WIP disabled security --- .../rest/api/archive/ArchiveResource.java | 69 +- .../data/rest/api/archive/ArchivingApi.java | 11 +- .../api/archive/model/ArchivedResource.java | 28 +- .../model/ArchivedResourceVersion.java | 401 +++++++ .../api/archive/model/ArchivedResources.java | 4 +- .../model/ArchivedResourcesHistory.java | 94 ++ .../model/ArchivedResourcesSearch.java | 20 +- .../rest/api/archive/model/ResourceTag.java | 6 +- .../data/model/ArchivedResourceMetaItem.java | 82 -- .../data/model/ArchivedResourcesMetaItem.java | 6 +- .../scl/data/model/HistoryMetaItem.java | 9 +- .../CompasSclDataPostgreSQLRepository.java | 981 ++++++++++++++++-- .../V1_10__fill_archived_resource.sql | 12 - .../db/migration/V1_11__fill_resource_tag.sql | 12 - .../db/migration/V1_5__create_scl_history.sql | 58 +- .../db/migration/V1_7__create_location.sql | 2 - .../V1_8__create_archived_resource.sql | 36 - .../migration/V1_9__create_resource_tag.sql | 17 - .../data/model/IArchivedResourceMetaItem.java | 26 - .../model/IArchivedResourcesMetaItem.java | 2 +- .../scl/data/model/IHistoryMetaItem.java | 2 + .../repository/CompasSclDataRepository.java | 10 +- .../data/service/CompasSclDataService.java | 110 +- 23 files changed, 1643 insertions(+), 355 deletions(-) create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourceVersion.java create mode 100644 app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesHistory.java delete mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java delete mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql delete mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql delete mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql delete mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql delete mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java index 9d06c076..1eed25a2 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -4,12 +4,11 @@ import io.smallrye.mutiny.infrastructure.Infrastructure; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; -import org.lfenergy.compas.scl.data.model.IArchivedResourceMetaItem; -import org.lfenergy.compas.scl.data.model.IArchivedResourcesMetaItem; -import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; -import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; -import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesSearch; -import org.lfenergy.compas.scl.data.rest.api.archive.model.ResourceTag; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.lfenergy.compas.scl.data.model.*; +import org.lfenergy.compas.scl.data.rest.UserInfoProperties; +import org.lfenergy.compas.scl.data.rest.api.archive.model.*; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourceVersion; import org.lfenergy.compas.scl.data.service.CompasSclDataService; import java.io.File; @@ -20,10 +19,14 @@ public class ArchiveResource implements ArchivingApi { private final CompasSclDataService compasSclDataService; + private final JsonWebToken jsonWebToken; + private final UserInfoProperties userInfoProperties; @Inject - public ArchiveResource(CompasSclDataService compasSclDataService) { + public ArchiveResource(CompasSclDataService compasSclDataService, JsonWebToken jsonWebToken, UserInfoProperties userInfoProperties) { this.compasSclDataService = compasSclDataService; + this.jsonWebToken = jsonWebToken; + this.userInfoProperties = userInfoProperties; } @Override @@ -37,13 +40,23 @@ public Uni archiveResource(UUID id, String version, String xAu @Override public Uni archiveSclResource(UUID id, String version) { + String approver = jsonWebToken.getClaim(userInfoProperties.name()); return Uni.createFrom() - .item(() -> compasSclDataService.archiveSclResource(id, version)) + .item(() -> compasSclDataService.archiveSclResource(id, new Version(version), approver)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() .transform(this::mapToArchivedResource); } + @Override + public Uni retrieveArchivedResourceHistory(UUID id) { + return Uni.createFrom() + .item(() -> compasSclDataService.getArchivedResourceHistory(id)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToArchivedResourcesHistory); + } + @Override public Uni searchArchivedResources(ArchivedResourcesSearch archivedResourcesSearch) { return Uni.createFrom() @@ -58,7 +71,6 @@ private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResource if (uuid != null && !uuid.isBlank()) { return compasSclDataService.searchArchivedResources(UUID.fromString(uuid)); } - String location = archivedResourcesSearch.getLocation(); String name = archivedResourcesSearch.getName(); String approver = archivedResourcesSearch.getApprover(); @@ -70,7 +82,7 @@ private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResource return compasSclDataService.searchArchivedResources(location, name, approver, contentType, type, voltage, from, to); } - private ArchivedResource mapToArchivedResource(IArchivedResourceMetaItem archivedResource) { + private ArchivedResource mapToArchivedResource(IAbstractArchivedResourceMetaItem archivedResource) { return new ArchivedResource() .uuid(archivedResource.getId()) .location(archivedResource.getLocation()) @@ -100,4 +112,41 @@ private ArchivedResources mapToArchivedResources(IArchivedResourcesMetaItem arch .toList() ); } + + private ArchivedResourcesHistory mapToArchivedResourcesHistory(IArchivedResourcesHistoryMetaItem archivedResourcesHistoryMetaItem) { + return new ArchivedResourcesHistory() + .versions( + archivedResourcesHistoryMetaItem.getVersions() + .stream() + .map(this::mapToArchivedResourceVersion) + .toList() + ); + } + + private ArchivedResourceVersion mapToArchivedResourceVersion(IArchivedResourceVersion resourceVersion) { + return new ArchivedResourceVersion() + .uuid(resourceVersion.getId()) + .location(resourceVersion.getLocation()) + .name(resourceVersion.getName()) + .note(resourceVersion.getNote()) + .author(resourceVersion.getAuthor()) + .approver(resourceVersion.getApprover()) + .type(resourceVersion.getType()) + .contentType(resourceVersion.getContentType()) + .voltage(resourceVersion.getVoltage()) + .version(resourceVersion.getVersion()) + .modifiedAt(resourceVersion.getModifiedAt()) + .archivedAt(resourceVersion.getArchivedAt()) + .fields(resourceVersion.getFields() + .stream() + .map(field -> + new ResourceTag() + .key(field.getKey()) + .value(field.getValue()) + ) + .toList() + ) + .comment(resourceVersion.getComment()) + .archived(resourceVersion.isArchived()); + } } diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java index 49a3ba56..42b2dbfc 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java @@ -5,6 +5,7 @@ import jakarta.ws.rs.*; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesHistory; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesSearch; import java.io.File; @@ -12,19 +13,23 @@ @Path("/api/archive") -@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-11-18T15:39:21.464141400+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-12-06T09:13:22.882514600+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") public interface ArchivingApi { @POST @Path("/referenced-resource/{id}/versions/{version}") - @Consumes({ "application/octet-stream" }) @Produces({ "application/json" }) Uni archiveResource(@PathParam("id") UUID id, @PathParam("version") String version, @HeaderParam("X-author") String xAuthor, @HeaderParam("X-approver") String xApprover, @HeaderParam("Content-Type") String contentType, @HeaderParam("X-filename") String xFilename, @Valid File body); @POST @Path("/scl/{id}/versions/{version}") @Produces({ "application/json" }) - Uni archiveSclResource(@PathParam("id") UUID id,@PathParam("version") String version); + Uni archiveSclResource(@PathParam("id") UUID id, @PathParam("version") String version); + + @GET + @Path("/resources/{id}/versions") + @Produces({ "application/json" }) + Uni retrieveArchivedResourceHistory(@PathParam("id") UUID id); @POST @Path("/resources/search") diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java index e5e507ad..794f7cf8 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResource.java @@ -37,7 +37,7 @@ public ArchivedResource uuid(String uuid) { return this; } - + @JsonProperty("uuid") @NotNull public String getUuid() { return uuid; @@ -56,7 +56,7 @@ public ArchivedResource location(String location) { return this; } - + @JsonProperty("location") public String getLocation() { return location; @@ -75,7 +75,7 @@ public ArchivedResource name(String name) { return this; } - + @JsonProperty("name") @NotNull public String getName() { return name; @@ -94,7 +94,7 @@ public ArchivedResource note(String note) { return this; } - + @JsonProperty("note") public String getNote() { return note; @@ -113,7 +113,7 @@ public ArchivedResource author(String author) { return this; } - + @JsonProperty("author") @NotNull public String getAuthor() { return author; @@ -132,7 +132,7 @@ public ArchivedResource approver(String approver) { return this; } - + @JsonProperty("approver") public String getApprover() { return approver; @@ -151,7 +151,7 @@ public ArchivedResource type(String type) { return this; } - + @JsonProperty("type") public String getType() { return type; @@ -170,7 +170,7 @@ public ArchivedResource contentType(String contentType) { return this; } - + @JsonProperty("contentType") @NotNull public String getContentType() { return contentType; @@ -189,7 +189,7 @@ public ArchivedResource voltage(String voltage) { return this; } - + @JsonProperty("voltage") public String getVoltage() { return voltage; @@ -208,7 +208,7 @@ public ArchivedResource version(String version) { return this; } - + @JsonProperty("version") @NotNull public String getVersion() { return version; @@ -226,7 +226,7 @@ public ArchivedResource modifiedAt(OffsetDateTime modifiedAt) { return this; } - + @JsonProperty("modifiedAt") @NotNull public OffsetDateTime getModifiedAt() { return modifiedAt; @@ -244,7 +244,7 @@ public ArchivedResource archivedAt(OffsetDateTime archivedAt) { return this; } - + @JsonProperty("archivedAt") @NotNull public OffsetDateTime getArchivedAt() { return archivedAt; @@ -262,7 +262,7 @@ public ArchivedResource fields(List<@Valid ResourceTag> fields) { return this; } - + @JsonProperty("fields") @NotNull @Valid public List<@Valid ResourceTag> getFields() { return fields; @@ -323,7 +323,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ArchivedResource {\n"); - + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); sb.append(" location: ").append(toIndentedString(location)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourceVersion.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourceVersion.java new file mode 100644 index 00000000..9a0e70f0 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourceVersion.java @@ -0,0 +1,401 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + + +@JsonTypeName("ArchivedResourceVersion") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-12-06T09:13:22.882514600+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ArchivedResourceVersion { + private String uuid; + private String location; + private String name; + private String note; + private String author; + private String approver; + private String type; + private String contentType; + private String voltage; + private String version; + private OffsetDateTime modifiedAt; + private OffsetDateTime archivedAt; + private @Valid List<@Valid ResourceTag> fields = new ArrayList<>(); + private String comment; + private Boolean archived = false; + + /** + * Unique resource identifier + **/ + public ArchivedResourceVersion uuid(String uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + @NotNull public String getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Location of the resource, might be empty + **/ + public ArchivedResourceVersion location(String location) { + this.location = location; + return this; + } + + + @JsonProperty("location") + public String getLocation() { + return location; + } + + @JsonProperty("location") + public void setLocation(String location) { + this.location = location; + } + + /** + * Resource name + **/ + public ArchivedResourceVersion name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + * Versioning note + **/ + public ArchivedResourceVersion note(String note) { + this.note = note; + return this; + } + + + @JsonProperty("note") + public String getNote() { + return note; + } + + @JsonProperty("note") + public void setNote(String note) { + this.note = note; + } + + /** + * Modifying author + **/ + public ArchivedResourceVersion author(String author) { + this.author = author; + return this; + } + + + @JsonProperty("author") + @NotNull public String getAuthor() { + return author; + } + + @JsonProperty("author") + public void setAuthor(String author) { + this.author = author; + } + + /** + * Name of the approver + **/ + public ArchivedResourceVersion approver(String approver) { + this.approver = approver; + return this; + } + + + @JsonProperty("approver") + public String getApprover() { + return approver; + } + + @JsonProperty("approver") + public void setApprover(String approver) { + this.approver = approver; + } + + /** + * Content type + **/ + public ArchivedResourceVersion type(String type) { + this.type = type; + return this; + } + + + @JsonProperty("type") + public String getType() { + return type; + } + + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + + /** + * Content type + **/ + public ArchivedResourceVersion contentType(String contentType) { + this.contentType = contentType; + return this; + } + + + @JsonProperty("contentType") + @NotNull public String getContentType() { + return contentType; + } + + @JsonProperty("contentType") + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Content type + **/ + public ArchivedResourceVersion voltage(String voltage) { + this.voltage = voltage; + return this; + } + + + @JsonProperty("voltage") + public String getVoltage() { + return voltage; + } + + @JsonProperty("voltage") + public void setVoltage(String voltage) { + this.voltage = voltage; + } + + /** + * Version + **/ + public ArchivedResourceVersion version(String version) { + this.version = version; + return this; + } + + + @JsonProperty("version") + @NotNull public String getVersion() { + return version; + } + + @JsonProperty("version") + public void setVersion(String version) { + this.version = version; + } + + /** + **/ + public ArchivedResourceVersion modifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + return this; + } + + + @JsonProperty("modifiedAt") + @NotNull public OffsetDateTime getModifiedAt() { + return modifiedAt; + } + + @JsonProperty("modifiedAt") + public void setModifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + } + + /** + **/ + public ArchivedResourceVersion archivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + return this; + } + + + @JsonProperty("archivedAt") + @NotNull public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + @JsonProperty("archivedAt") + public void setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + } + + /** + **/ + public ArchivedResourceVersion fields(List<@Valid ResourceTag> fields) { + this.fields = fields; + return this; + } + + + @JsonProperty("fields") + @NotNull @Valid public List<@Valid ResourceTag> getFields() { + return fields; + } + + @JsonProperty("fields") + public void setFields(List<@Valid ResourceTag> fields) { + this.fields = fields; + } + + public ArchivedResourceVersion addFieldsItem(ResourceTag fieldsItem) { + if (this.fields == null) { + this.fields = new ArrayList<>(); + } + + this.fields.add(fieldsItem); + return this; + } + + public ArchivedResourceVersion removeFieldsItem(ResourceTag fieldsItem) { + if (fieldsItem != null && this.fields != null) { + this.fields.remove(fieldsItem); + } + + return this; + } + /** + * Comment given when uploading the data resource + **/ + public ArchivedResourceVersion comment(String comment) { + this.comment = comment; + return this; + } + + + @JsonProperty("comment") + public String getComment() { + return comment; + } + + @JsonProperty("comment") + public void setComment(String comment) { + this.comment = comment; + } + + /** + * Defines if given data resource is archived + **/ + public ArchivedResourceVersion archived(Boolean archived) { + this.archived = archived; + return this; + } + + + @JsonProperty("archived") + @NotNull public Boolean getArchived() { + return archived; + } + + @JsonProperty("archived") + public void setArchived(Boolean archived) { + this.archived = archived; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArchivedResourceVersion archivedResourceVersion = (ArchivedResourceVersion) o; + return Objects.equals(this.uuid, archivedResourceVersion.uuid) && + Objects.equals(this.location, archivedResourceVersion.location) && + Objects.equals(this.name, archivedResourceVersion.name) && + Objects.equals(this.note, archivedResourceVersion.note) && + Objects.equals(this.author, archivedResourceVersion.author) && + Objects.equals(this.approver, archivedResourceVersion.approver) && + Objects.equals(this.type, archivedResourceVersion.type) && + Objects.equals(this.contentType, archivedResourceVersion.contentType) && + Objects.equals(this.voltage, archivedResourceVersion.voltage) && + Objects.equals(this.version, archivedResourceVersion.version) && + Objects.equals(this.modifiedAt, archivedResourceVersion.modifiedAt) && + Objects.equals(this.archivedAt, archivedResourceVersion.archivedAt) && + Objects.equals(this.fields, archivedResourceVersion.fields) && + Objects.equals(this.comment, archivedResourceVersion.comment) && + Objects.equals(this.archived, archivedResourceVersion.archived); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, location, name, note, author, approver, type, contentType, voltage, version, modifiedAt, archivedAt, fields, comment, archived); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ArchivedResourceVersion {\n"); + + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" note: ").append(toIndentedString(note)).append("\n"); + sb.append(" author: ").append(toIndentedString(author)).append("\n"); + sb.append(" approver: ").append(toIndentedString(approver)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); + sb.append(" voltage: ").append(toIndentedString(voltage)).append("\n"); + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" modifiedAt: ").append(toIndentedString(modifiedAt)).append("\n"); + sb.append(" archivedAt: ").append(toIndentedString(archivedAt)).append("\n"); + sb.append(" fields: ").append(toIndentedString(fields)).append("\n"); + sb.append(" comment: ").append(toIndentedString(comment)).append("\n"); + sb.append(" archived: ").append(toIndentedString(archived)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java index b39c290a..f760ce27 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResources.java @@ -23,7 +23,7 @@ public ArchivedResources resources(List<@Valid ArchivedResource> resources) { return this; } - + @JsonProperty("resources") @NotNull @Valid public List<@Valid ArchivedResource> getResources() { return resources; @@ -72,7 +72,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ArchivedResources {\n"); - + sb.append(" resources: ").append(toIndentedString(resources)).append("\n"); sb.append("}"); return sb.toString(); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesHistory.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesHistory.java new file mode 100644 index 00000000..07aae09e --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesHistory.java @@ -0,0 +1,94 @@ +package org.lfenergy.compas.scl.data.rest.api.archive.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + + +@JsonTypeName("ArchivedResourcesHistory") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-12-06T09:13:22.882514600+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ArchivedResourcesHistory { + private @Valid List versions = new ArrayList<>(); + + /** + **/ + public ArchivedResourcesHistory versions(List versions) { + this.versions = versions; + return this; + } + + + @JsonProperty("versions") + @NotNull @Valid public List<@Valid ArchivedResourceVersion> getVersions() { + return versions; + } + + @JsonProperty("versions") + public void setVersions(List versions) { + this.versions = versions; + } + + public ArchivedResourcesHistory addVersionsItem(ArchivedResourceVersion versionsItem) { + if (this.versions == null) { + this.versions = new ArrayList<>(); + } + + this.versions.add(versionsItem); + return this; + } + + public ArchivedResourcesHistory removeVersionsItem(ArchivedResourceVersion versionsItem) { + if (versionsItem != null && this.versions != null) { + this.versions.remove(versionsItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArchivedResourcesHistory archivedResourcesHistory = (ArchivedResourcesHistory) o; + return Objects.equals(this.versions, archivedResourcesHistory.versions); + } + + @Override + public int hashCode() { + return Objects.hash(versions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ArchivedResourcesHistory {\n"); + + sb.append(" versions: ").append(toIndentedString(versions)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java index ed6e7442..d682cd1e 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ArchivedResourcesSearch.java @@ -29,7 +29,7 @@ public ArchivedResourcesSearch uuid(String uuid) { return this; } - + @JsonProperty("uuid") public String getUuid() { return uuid; @@ -48,7 +48,7 @@ public ArchivedResourcesSearch location(String location) { return this; } - + @JsonProperty("location") public String getLocation() { return location; @@ -67,7 +67,7 @@ public ArchivedResourcesSearch name(String name) { return this; } - + @JsonProperty("name") public String getName() { return name; @@ -86,7 +86,7 @@ public ArchivedResourcesSearch approver(String approver) { return this; } - + @JsonProperty("approver") public String getApprover() { return approver; @@ -105,7 +105,7 @@ public ArchivedResourcesSearch contentType(String contentType) { return this; } - + @JsonProperty("contentType") public String getContentType() { return contentType; @@ -124,7 +124,7 @@ public ArchivedResourcesSearch type(String type) { return this; } - + @JsonProperty("type") public String getType() { return type; @@ -143,7 +143,7 @@ public ArchivedResourcesSearch voltage(String voltage) { return this; } - + @JsonProperty("voltage") public String getVoltage() { return voltage; @@ -162,7 +162,7 @@ public ArchivedResourcesSearch from(OffsetDateTime from) { return this; } - + @JsonProperty("from") public OffsetDateTime getFrom() { return from; @@ -181,7 +181,7 @@ public ArchivedResourcesSearch to(OffsetDateTime to) { return this; } - + @JsonProperty("to") public OffsetDateTime getTo() { return to; @@ -222,7 +222,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ArchivedResourcesSearch {\n"); - + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); sb.append(" location: ").append(toIndentedString(location)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java index fd6b736b..53477aa8 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/model/ResourceTag.java @@ -22,7 +22,7 @@ public ResourceTag key(String key) { return this; } - + @JsonProperty("key") @NotNull public String getKey() { return key; @@ -41,7 +41,7 @@ public ResourceTag value(String value) { return this; } - + @JsonProperty("value") @NotNull public String getValue() { return value; @@ -75,7 +75,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ResourceTag {\n"); - + sb.append(" key: ").append(toIndentedString(key)).append("\n"); sb.append(" value: ").append(toIndentedString(value)).append("\n"); sb.append("}"); diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java deleted file mode 100644 index 4c3e943c..00000000 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceMetaItem.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.lfenergy.compas.scl.data.model; - -import java.time.OffsetDateTime; -import java.util.List; - -public class ArchivedResourceMetaItem extends AbstractItem implements IArchivedResourceMetaItem { - - String location; - String note; - String author; - String approver; - String type; - String contentType; - String voltage; - OffsetDateTime modifiedAt; - OffsetDateTime archivedAt; - List fields; - - public ArchivedResourceMetaItem(String id, String name, String version, String location, String note, String author, String approver, String type, String contentType, String voltage, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, List fields) { - super(id, name, version); - this.location = location; - this.note = note; - this.author = author; - this.approver = approver; - this.type = type; - this.contentType = contentType; - this.voltage = voltage; - this.modifiedAt = modifiedAt; - this.archivedAt = archivedAt; - this.fields = fields; - } - - @Override - public String getLocation() { - return location; - } - - @Override - public String getNote() { - return note; - } - - @Override - public String getAuthor() { - return author; - } - - @Override - public String getApprover() { - return approver; - } - - @Override - public String getType() { - return type; - } - - @Override - public String getContentType() { - return contentType; - } - - @Override - public String getVoltage() { - return voltage; - } - - @Override - public OffsetDateTime getModifiedAt() { - return modifiedAt; - } - - @Override - public OffsetDateTime getArchivedAt() { - return archivedAt; - } - - @Override - public List getFields() { - return fields; - } -} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java index 8afd3870..e4e4cc33 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java @@ -4,14 +4,14 @@ public class ArchivedResourcesMetaItem implements IArchivedResourcesMetaItem { - List archivedResources; + List archivedResources; - public ArchivedResourcesMetaItem(List archivedResources) { + public ArchivedResourcesMetaItem(List archivedResources) { this.archivedResources = archivedResources; } @Override - public List getResources() { + public List getResources() { return archivedResources; } } diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java index 8ad600d2..16eeec49 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java @@ -6,16 +6,18 @@ public class HistoryMetaItem extends AbstractItem implements IHistoryMetaItem { private final String type; private final String author; private final String comment; + private final String location; private final OffsetDateTime changedAt; private final boolean archived; private final boolean available; private final boolean deleted; - public HistoryMetaItem(String id, String name, String version, String type, String author, String comment, OffsetDateTime changedAt, boolean archived, boolean available, boolean deleted) { + public HistoryMetaItem(String id, String name, String version, String type, String author, String comment, String location,OffsetDateTime changedAt, boolean archived, boolean available, boolean deleted) { super(id, name, version); this.type = type; this.author = author; this.comment = comment; + this.location = location; this.changedAt = changedAt; this.archived = archived; this.available = available; @@ -37,6 +39,11 @@ public String getComment() { return comment; } + @Override + public String getLocation() { + return location; + } + @Override public OffsetDateTime getChangedAt() { return changedAt; diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index fe5b7fa0..7c11db9a 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -31,7 +31,8 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String MINOR_VERSION_FIELD = "minor_version"; private static final String PATCH_VERSION_FIELD = "patch_version"; private static final String NAME_FIELD = "name"; - private static final String LOCATIONMETAITEM_KEY_FIELD = "key"; + private static final String KEY_FIELD = "key"; + private static final String VALUE_FIELD = "value"; private static final String LOCATIONMETAITEM_DESCRIPTION_FIELD = "description"; private static final String LOCATIONMETAITEM_RESOURCE_ID_FIELD = "resource_id"; private static final String SCL_DATA_FIELD = "scl_data"; @@ -641,12 +642,72 @@ INSERT INTO location (id, key, name, description, resource_id) return findLocationByUUID(id); } + @Override + public void addLocationTags(ILocationMetaItem location, String author) { + String locationName = location.getName(); + ResourceTagItem locationNameTag = getResourceTag("LOCATION", locationName); + ResourceTagItem locationAuthorTag = getResourceTag("AUTHOR", author); + + if (locationNameTag == null) { + createResourceTag("LOCATION", locationName); + locationNameTag = getResourceTag("LOCATION", locationName); + } + if (locationAuthorTag == null) { + createResourceTag("AUTHOR", author); + locationAuthorTag = getResourceTag("AUTHOR", author); + } + UUID locationUuid = UUID.fromString(location.getId()); + updateTagMappingForLocation(locationUuid, List.of(locationNameTag, locationAuthorTag)); + } + + @Override + public void deleteLocationTags(ILocationMetaItem location) { + String selectLocationResourceTagQuery = """ + SELECT DISTINCT lrt.resource_tag_id as resource_tag_id + FROM location_resource_tag lrt + LEFT OUTER JOIN archived_resource_resource_tag arrt ON lrt.resource_tag_id = arrt.resource_tag_id + WHERE lrt.location_id = ? + AND COALESCE(CAST(arrt.resource_tag_id AS varchar), '' ) <> CAST(lrt.resource_tag_id AS varchar); + """; + + String deleteResourceTagQuery = """ + DELETE FROM resource_tag + WHERE id = ?; + """; + + List locationTagId = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(selectLocationResourceTagQuery)) { + sclStmt.setObject(1, UUID.fromString(location.getId())); + try (ResultSet resultSet = sclStmt.executeQuery()) { + while (resultSet.next()) { + locationTagId.add(resultSet.getString("resource_tag_id")); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_DELETE_ERROR_CODE, "Error removing Location from database", exp); + } + if (!locationTagId.isEmpty()) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(deleteResourceTagQuery)) { + for (String id : locationTagId) { + sclStmt.setObject(1, UUID.fromString(id)); + sclStmt.addBatch(); + sclStmt.clearParameters(); + } + sclStmt.executeBatch(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_DELETE_ERROR_CODE, "Error removing Location from database", exp); + } + } + } + @Override @Transactional(SUPPORTS) public List listLocations(int page, int pageSize) { String sql = """ - SELECT * - FROM location + SELECT *, (SELECT COUNT(sf.id) FROM scl_file sf WHERE sf.location_id = l.id) as assigned_resources + FROM location l ORDER BY name OFFSET ? LIMIT ?; @@ -662,12 +723,12 @@ public List listLocations(int page, int pageSize) { @Transactional(SUPPORTS) public ILocationMetaItem findLocationByUUID(UUID locationId) { String sql = """ - SELECT * - FROM location - WHERE id = ? - ORDER BY name; + SELECT *, (SELECT COUNT(sf.id) FROM scl_file sf WHERE sf.location_id = ?) as assigned_resources + FROM location l + WHERE id = ? + ORDER BY l.name; """; - List retrievedLocation = executeLocationQuery(sql, Collections.singletonList(locationId)); + List retrievedLocation = executeLocationQuery(sql, List.of(locationId, locationId)); if (retrievedLocation.isEmpty()) { throw new CompasNoDataFoundException(String.format("Unable to find Location with id %s.", locationId)); } @@ -677,12 +738,13 @@ public ILocationMetaItem findLocationByUUID(UUID locationId) { @Override @Transactional(REQUIRED) public void deleteLocation(UUID locationId) { - String sql = """ + String deleteLocationQuery = """ DELETE FROM location WHERE id = ?; """; - try (var connection = dataSource.getConnection(); - var sclStmt = connection.prepareStatement(sql)) { + + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(deleteLocationQuery)) { sclStmt.setObject(1, locationId); sclStmt.executeUpdate(); } catch (SQLException exp) { @@ -700,11 +762,11 @@ public ILocationMetaItem updateLocation(UUID locationId, String key, String name """); if (description != null && !description.isBlank()) { sqlBuilder.append(", description = ?"); - sqlBuilder.append("\n"); + sqlBuilder.append(System.lineSeparator()); } sqlBuilder.append("WHERE id = ?;"); - try (var connection = dataSource.getConnection(); - var sclStmt = connection.prepareStatement(sqlBuilder.toString())) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(sqlBuilder.toString())) { sclStmt.setString(1, key); sclStmt.setString(2, name); if (description == null || description.isBlank()) { @@ -723,60 +785,566 @@ public ILocationMetaItem updateLocation(UUID locationId, String key, String name @Override @Transactional(REQUIRED) public void assignResourceToLocation(UUID locationId, UUID resourceId) { - String sql = """ - UPDATE location - SET resource_id = ? - WHERE id = ?; - """; - try (var connection = dataSource.getConnection(); - var sclStmt = connection.prepareStatement(sql)) { - sclStmt.setObject(1, resourceId); - sclStmt.setObject(2, locationId); + String archivedResourceSql = """ + UPDATE scl_file + SET location_id = ? + WHERE id = ?; + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(archivedResourceSql)) { + sclStmt.setObject(1, locationId); + sclStmt.setObject(2, resourceId); sclStmt.executeUpdate(); } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error assigning SCL Resource to Location in database!", exp); } + String referencedResourceSql = """ + UPDATE referenced_resource + SET location_id = ? + WHERE scl_file_id = ?; + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(referencedResourceSql)) { + sclStmt.setObject(1, locationId); + sclStmt.setObject(2, resourceId); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error assigning Referenced Resource to Location in database!", exp); + } + String locationKey = findLocationByUUID(locationId).getName(); + updateLocationKeyMappingForSclResource(resourceId, locationKey); + updateLocationKeyMappingForReferencedResource(resourceId, locationKey); } @Override @Transactional(REQUIRED) public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { - String sql = """ - UPDATE location - SET resource_id = NULL - WHERE id = ? AND resource_id = ?; + String archivedResourceSql = """ + UPDATE scl_file + SET location_id = NULL + WHERE id = ? AND location_id = ?; """; - try (var connection = dataSource.getConnection(); - var sclStmt = connection.prepareStatement(sql)) { - sclStmt.setObject(1, locationId); - sclStmt.setObject(2, resourceId); + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(archivedResourceSql)) { + sclStmt.setObject(1, resourceId); + sclStmt.setObject(2, locationId); sclStmt.executeUpdate(); } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error unassigning SCL Resource from Location in database!", exp); } + String referencedResourceSql = """ + UPDATE referenced_resource + SET location_id = NULL + WHERE scl_file_id = ? AND location_id = ?; + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(referencedResourceSql)) { + sclStmt.setObject(1, resourceId); + sclStmt.setObject(2, locationId); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error unassigning Referenced Resource from Location in database!", exp); + } + updateLocationKeyMappingForSclResource(resourceId, null); + updateLocationKeyMappingForReferencedResource(resourceId, null); + } + + private void updateTagMappingForLocation(UUID locationId, List resourceTags) { + List newMappingEntries = resourceTags.stream().filter(entry -> + !existsLocationResourceTagMapping(locationId, UUID.fromString(entry.getId())) + ).toList(); + + String insertStatement = """ + INSERT INTO location_resource_tag(location_id, resource_tag_id) + VALUES (?, ?); + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement mappingStmt = connection.prepareStatement(insertStatement)) { + newMappingEntries.forEach(entry -> { + try { + mappingStmt.setObject(1, locationId); + mappingStmt.setObject(2, UUID.fromString(entry.getId())); + mappingStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding location to resource tag mapping entry to database!", exp); + } + }); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding location to resource tag mapping entries to database!", exp); + } + } + + private void updateLocationKeyMappingForSclResource(UUID resourceId, String locationKey) { + ResourceTagItem locationTag = getResourceTag("LOCATION", locationKey); + if (locationTag == null) { + createResourceTag("LOCATION", locationKey); + locationTag = getResourceTag("LOCATION", locationKey); + } + for (IAbstractArchivedResourceMetaItem resource : searchArchivedResourceBySclFile(resourceId)) { + updateLocationResourceTag(locationTag, resource); + } + } + + private void updateLocationKeyMappingForReferencedResource(UUID resourceId, String locationKey) { + ResourceTagItem locationTag = getResourceTag("LOCATION", locationKey); + if (locationTag == null) { + createResourceTag("LOCATION", locationKey); + locationTag = getResourceTag("LOCATION", locationKey); + } + for (IAbstractArchivedResourceMetaItem resource : searchArchivedReferencedResources(resourceId)) { + updateLocationResourceTag(locationTag, resource); + } + } + + private void updateLocationResourceTag(ResourceTagItem locationTag, IAbstractArchivedResourceMetaItem resource) { + List locationFieldIds = resource.getFields() + .stream() + .filter(field -> + field.getKey().equals("LOCATION") + ) + .map(IResourceTagItem::getId) + .toList(); + if (!locationFieldIds.isEmpty()) { + removeLocationTagsFromResource(resource.getId(), locationFieldIds); + } + updateArchivedResourceToResourceTagMappingTable( + UUID.fromString(resource.getId()), + List.of(locationTag) + ); + } + + private void removeLocationTagsFromResource(String resourceId, List locationFieldIds) { + String sql = String.format(""" + DELETE FROM archived_resource_resource_tag + WHERE archived_resource_id = ? AND resource_tag_id IN (%s); + """, locationFieldIds.stream() + .map(fieldIds -> "?") + .collect(Collectors.joining(","))); + try (Connection connection = dataSource.getConnection(); + PreparedStatement deleteStatement = connection.prepareStatement(sql)) { + deleteStatement.setObject(1, UUID.fromString(resourceId)); + for (int i = 1; i <= locationFieldIds.size(); i++) { + deleteStatement.setObject(i + 1, UUID.fromString(locationFieldIds.get(i - 1))); + } + deleteStatement.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_DELETE_ERROR_CODE, "Error deleting archived resource to resource tag mapping entries in database!", exp); + } } @Override - public IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { - return null; + public IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version version, String author, String approver, String contentType, String filename) { + ArchivedSclResourceMetaItem sclResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); + String location = sclResourceMetaItem.getLocation(); + + String locationIdQuery = """ + SELECT l.*, (SELECT COUNT(DISTINCT(sf.id)) FROM scl_file sf WHERE sf.location_id = l.id) as assigned_resources + FROM scl_file sf INNER JOIN location l ON sf.location_id = l.id + WHERE sf.id = ? + AND sf.major_version = ? + AND sf.minor_version = ? + AND sf.patch_version = ?; + """; + + List locationItems = executeLocationQuery(locationIdQuery, List.of(id, version.getMajorVersion(), version.getMinorVersion(), version.getPatchVersion())); + + UUID assignedResourceId = UUID.randomUUID(); + + if (!locationItems.isEmpty() && locationItems.get(0).getId() != null) { + String locationId = locationItems.get(0).getId(); + String sql = """ + INSERT INTO referenced_resource (id, content_type, filename, author, approver, location_id, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, assignedResourceId); + stmt.setObject(2, contentType); + stmt.setObject(3, filename); + stmt.setObject(4, author); + stmt.setObject(5, approver); + stmt.setObject(6, UUID.fromString(locationId)); + stmt.setObject(7, id); + stmt.setObject(8, version.getMajorVersion()); + stmt.setObject(9, version.getMinorVersion()); + stmt.setObject(10, version.getPatchVersion()); + stmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error inserting Referenced Resources!", exp); + } + } + + List resourceTags = generateFields(location, id.toString(), author, approver); + ArchivedReferencedResourceMetaItem archivedResourcesMetaItem = new ArchivedReferencedResourceMetaItem( + assignedResourceId.toString(), + filename, + version.toString(), + author, + approver, + null, + contentType, + location, + resourceTags, + null, + convertToOffsetDateTime(Timestamp.from(Instant.now())), + null + ); + UUID archivedResourceId = UUID.randomUUID(); + insertIntoArchivedResourceTable(archivedResourceId, archivedResourcesMetaItem, version); + updateArchivedResourceToResourceTagMappingTable( + archivedResourceId, + archivedResourcesMetaItem.getFields() + ); + return new ArchivedReferencedResourceMetaItem( + archivedResourceId.toString(), + archivedResourcesMetaItem.getName(), + archivedResourcesMetaItem.getVersion(), + archivedResourcesMetaItem.getAuthor(), + archivedResourcesMetaItem.getApprover(), + archivedResourcesMetaItem.getType(), + archivedResourcesMetaItem.getContentType(), + archivedResourcesMetaItem.getLocation(), + archivedResourcesMetaItem.getFields(), + archivedResourcesMetaItem.getModifiedAt(), + archivedResourcesMetaItem.getArchivedAt(), + archivedResourcesMetaItem.getComment() + ); } @Override - public IArchivedResourceMetaItem archiveSclResource(UUID id, String version) { + public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { + ArchivedSclResourceMetaItem convertedArchivedResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); + if (convertedArchivedResourceMetaItem != null) { + if (convertedArchivedResourceMetaItem.getLocation() == null) { + throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, + String.format("Unable to archive scl_file %s with version %s, no location assigned!", id, version)); + } + UUID archivedResourceId = UUID.randomUUID(); + insertIntoArchivedResourceTable(archivedResourceId, convertedArchivedResourceMetaItem, version); + updateArchivedResourceToResourceTagMappingTable( + archivedResourceId, + convertedArchivedResourceMetaItem.getFields() + ); + return new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + convertedArchivedResourceMetaItem.getName(), + convertedArchivedResourceMetaItem.getVersion(), + convertedArchivedResourceMetaItem.getAuthor(), + convertedArchivedResourceMetaItem.getApprover(), + convertedArchivedResourceMetaItem.getType(), + convertedArchivedResourceMetaItem.getContentType(), + convertedArchivedResourceMetaItem.getLocation(), + convertedArchivedResourceMetaItem.getFields(), + convertedArchivedResourceMetaItem.getModifiedAt(), + convertedArchivedResourceMetaItem.getArchivedAt(), + convertedArchivedResourceMetaItem.getNote(), + convertedArchivedResourceMetaItem.getVoltage() + ); + } return null; } + private ArchivedSclResourceMetaItem getSclFileAsArchivedSclResourceMetaItem(UUID id, Version version, String approver) { + String sql = """ + SELECT scl_file.*, + l.name as location, + (xpath('/scl:Hitem/@who', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_who, + (xpath('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_what + FROM scl_file + LEFT OUTER JOIN ( + SELECT id, major_version, minor_version, patch_version, + unnest( + xpath('(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' + , scl_data::xml + , ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']])) AS header + FROM scl_file) scl_data + ON scl_data.id = scl_file.id + AND scl_data.major_version = scl_file.major_version + AND scl_data.minor_version = scl_file.minor_version + AND scl_data.patch_version = scl_file.patch_version + INNER JOIN location l + ON scl_file.location_id = l.id + WHERE scl_file.id = ? + AND scl_file.major_version = ? + AND scl_file.minor_version = ? + AND scl_file.patch_version = ?; + """; + ArchivedSclResourceMetaItem archivedResourceMetaItem; + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, id); + stmt.setInt(2, version.getMajorVersion()); + stmt.setInt(3, version.getMinorVersion()); + stmt.setInt(4, version.getPatchVersion()); + + try (ResultSet resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + List fieldList = generateFieldsFromResultSet(resultSet, approver); + archivedResourceMetaItem = mapResultSetToArchivedSclResource(approver, resultSet, fieldList); + } else { + String message = String.format("No SCL Resource with ID '%s' and version '%s' found", id, version); + throw new CompasNoDataFoundException(message); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error select SCL Resource from database!", exp); + } + return archivedResourceMetaItem; + } + + private ArchivedSclResourceMetaItem mapResultSetToArchivedSclResource(String approver, ResultSet resultSet, List fieldList) throws SQLException { + return new ArchivedSclResourceMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + createVersion(resultSet), + resultSet.getString("created_by"), + approver, + null, + resultSet.getString("type"), + resultSet.getString("location"), + fieldList, + convertToOffsetDateTime(Timestamp.from(Instant.now())), + convertToOffsetDateTime(resultSet.getTimestamp("creation_date")), + resultSet.getString(HITEM_WHAT_FIELD), + null + ); + } + + private List generateFields(String location, String sourceResourceId, String author, String examiner) { + List fieldList = new ArrayList<>(); + + ResourceTagItem locationTag = getResourceTag("LOCATION", location); + ResourceTagItem resourceIdTag = getResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); + ResourceTagItem authorTag = getResourceTag("AUTHOR", author); + ResourceTagItem examinerTag = getResourceTag("EXAMINER", examiner); + + if (locationTag == null) { + createResourceTag("LOCATION", location); + locationTag = getResourceTag("LOCATION", location); + } + if (resourceIdTag == null) { + createResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); + resourceIdTag = getResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); + } + if (authorTag == null) { + createResourceTag("AUTHOR", author); + authorTag = getResourceTag("AUTHOR", author); + } + if (examinerTag == null) { + createResourceTag("EXAMINER", examiner); + examinerTag = getResourceTag("EXAMINER", examiner); + } + + fieldList.add(locationTag); + fieldList.add(resourceIdTag); + fieldList.add(authorTag); + fieldList.add(examinerTag); + + return fieldList; + } + + private List generateFieldsFromResultSet(ResultSet resultSet, String examiner) throws SQLException { + return generateFields( + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + resultSet.getString(ID_FIELD), + resultSet.getString("created_by"), + examiner + ); + } + + private void createResourceTag(String key, String value) { + String insertIntoResourceTagSql = """ + INSERT INTO resource_tag (id, key, value) + VALUES (?, ?, ?); + """; + + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(insertIntoResourceTagSql)) { + stmt.setObject(1, UUID.randomUUID()); + stmt.setString(2, key); + if (value == null) { + stmt.setNull(3, Types.VARCHAR); + } else { + stmt.setString(3, value); + } + stmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding SCL Resource to Archived Resources!", exp); + } + } + + private ResourceTagItem getResourceTag(String key, String value) { + StringBuilder sb = new StringBuilder(); + String sql = """ + SELECT * + FROM resource_tag + """; + sb.append(sql); + if (value == null) { + sb.append("WHERE key = ? AND value IS NULL;"); + } else { + sb.append("WHERE key = ? AND value = ?;"); + } + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sb.toString())) { + stmt.setObject(1, key); + if (value != null) { + stmt.setObject(2, value); + } + try (ResultSet resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + return new ResourceTagItem( + resultSet.getString(ID_FIELD), + resultSet.getString(KEY_FIELD), + resultSet.getString(VALUE_FIELD) + ); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error retrieving Resource Tag entry from database!", exp); + } + + return null; + } + + private void insertIntoArchivedResourceTable(UUID archivedResourceId, AbstractArchivedResourceMetaItem archivedResource, Version version) { + String insertSclResourceIntoArchiveSql = """ + INSERT INTO archived_resource(id, archived_at, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + VALUES (?, ?, ?, ?, ?, ?); + """; + + String insertReferencedResourceIntoArchiveSql = """ + INSERT INTO archived_resource(id, archived_at, referenced_resource_id, referenced_resource_major_version, referenced_resource_minor_version, referenced_resource_patch_version) + VALUES (?, ?, ?, ?, ?, ?); + """; + + if (archivedResource instanceof ArchivedSclResourceMetaItem) { + executeArchivedResourceInsertStatement(archivedResourceId, archivedResource, version, insertSclResourceIntoArchiveSql); + } else { + executeArchivedResourceInsertStatement(archivedResourceId, archivedResource, version, insertReferencedResourceIntoArchiveSql); + } + } + + private void executeArchivedResourceInsertStatement(UUID archivedResourceId, AbstractArchivedResourceMetaItem archivedResource, Version version, String query) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(query)) { + stmt.setObject(1, archivedResourceId); + stmt.setObject(2, OffsetDateTime.now()); + stmt.setObject(3, UUID.fromString(archivedResource.getId())); + stmt.setInt(4, version.getMajorVersion()); + stmt.setInt(5, version.getMinorVersion()); + stmt.setInt(6, version.getPatchVersion()); + stmt.executeUpdate(); + } catch (SQLException exp) { + String message = String.format("Error adding %s resource to archived resources!", + archivedResource instanceof ArchivedSclResourceMetaItem ? "SCL" : "Referenced"); + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, message, exp); + } + } + + private List searchArchivedReferencedResources(UUID archivedResourceUuid) { + String archivedResourcesSql = """ + SELECT ar.*, + COALESCE(sf.name, rr.filename) as name, + COALESCE(sf.created_by, rr.author) as author, + rr.approver as approver, + rr.content_type as content_type, + COALESCE(sf.type, rr.type) as type, + sf.creation_date as modified_at, + ar.archived_at as archived_at, + null as comment, + null as voltage, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.name as location + FROM archived_resource ar + INNER JOIN archived_resource_resource_tag arrt + ON ar.id = arrt.archived_resource_id + INNER JOIN resource_tag rt + ON arrt.resource_tag_id = rt.id + LEFT JOIN scl_file sf + ON sf.id = ar.scl_file_id + AND sf.major_version = ar.scl_file_major_version + AND sf.minor_version = ar.scl_file_minor_version + AND sf.patch_version = ar.scl_file_patch_version + LEFT JOIN referenced_resource rr + ON ar.referenced_resource_id = rr.id + LEFT JOIN location l + ON sf.location_id = l.id OR rr.location_id = l.id + WHERE rr.scl_file_id = ? + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; + """; + + return executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(archivedResourceUuid)); + } + + private List searchArchivedResourceBySclFile(UUID archivedResourceUuid) { + String archivedResourcesSql = """ + SELECT ar.*, + COALESCE(sf.name, rr.filename) as name, + COALESCE(sf.created_by, rr.author) as author, + rr.approver as approver, + rr.content_type as content_type, + COALESCE(sf.type, rr.type) as type, + sf.creation_date as modified_at, + ar.archived_at as archived_at, + null as comment, + null as voltage, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.name as location + FROM archived_resource ar + INNER JOIN archived_resource_resource_tag arrt + ON ar.id = arrt.archived_resource_id + INNER JOIN resource_tag rt + ON arrt.resource_tag_id = rt.id + LEFT JOIN scl_file sf + ON sf.id = ar.scl_file_id + AND sf.major_version = ar.scl_file_major_version + AND sf.minor_version = ar.scl_file_minor_version + AND sf.patch_version = ar.scl_file_patch_version + LEFT JOIN referenced_resource rr + ON ar.referenced_resource_id = rr.id + LEFT JOIN location l + ON sf.location_id = l.id OR rr.location_id = l.id + WHERE sf.id = ? + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; + """; + + return executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(archivedResourceUuid)); + } + @Override public IArchivedResourcesMetaItem searchArchivedResource(UUID id) { String archivedResourcesSql = """ - SELECT ar.*, ARRAY_AGG (rt.id || ';' || rt.key || ';' || rt.value) AS tags - FROM archived_resource ar LEFT OUTER JOIN resource_tag rt - ON ar.id=rt.archived_resource_id + SELECT ar.*, + COALESCE(sf.name, rr.filename) as name, + COALESCE(sf.created_by, rr.author) as author, + rr.approver as approver, + rr.content_type as content_type, + COALESCE(sf.type, rr.type) as type, + sf.creation_date as modified_at, + ar.archived_at as archived_at, + null as comment, + null as voltage, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.name as location + FROM archived_resource ar + INNER JOIN archived_resource_resource_tag arrt + ON ar.id = arrt.archived_resource_id + INNER JOIN resource_tag rt + ON arrt.resource_tag_id = rt.id + LEFT JOIN scl_file sf + ON sf.id = ar.scl_file_id + AND sf.major_version = ar.scl_file_major_version + AND sf.minor_version = ar.scl_file_minor_version + AND sf.patch_version = ar.scl_file_patch_version + LEFT JOIN referenced_resource rr + ON ar.referenced_resource_id = rr.id + LEFT JOIN location l + ON sf.location_id = l.id OR rr.location_id = l.id WHERE ar.id = ? - GROUP BY ar.id, ar.name, ar.major_version, ar.minor_version, ar.patch_version, ar.location, ar.note, ar.approver, ar.type, ar.content_type, ar.voltage, ar.modified_at, ar.archived_at; + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; """; - List result = executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(id)); - return new ArchivedResourcesMetaItem(result); + return new ArchivedResourcesMetaItem(executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(id))); } @Override @@ -784,35 +1352,84 @@ public IArchivedResourcesMetaItem searchArchivedResource(String location, String List parameters = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append(""" - SELECT ar.*, ARRAY_AGG (rt.id || ';' || rt.key || ';' || rt.value) AS tags - FROM archived_resource ar LEFT OUTER JOIN resource_tag rt - ON ar.id=rt.archived_resource_id - WHERE 1=1 + SELECT ar.*, + COALESCE(sf.name, rr.filename) AS name, + COALESCE(sf.created_by, rr.author) AS author, + COALESCE(xml_data.hitem_who::varchar, rr.approver) AS approver, + rr.content_type AS content_type, + COALESCE(sf.type, rr.type) AS type, + sf.creation_date AS modified_at, + ar.archived_at AS archived_at, + null AS comment, + null AS voltage, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.name AS location + FROM archived_resource ar + LEFT JOIN scl_file sf + ON sf.id = ar.scl_file_id + AND sf.major_version = ar.scl_file_major_version + AND sf.minor_version = ar.scl_file_minor_version + AND sf.patch_version = ar.scl_file_patch_version + LEFT JOIN + (SELECT scl_file.id, scl_file.major_version, scl_file.minor_version, scl_file.patch_version, + (xpath('/scl:Hitem/@who', scl_data.header, + ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_who + FROM scl_file + INNER JOIN (SELECT id, major_version, minor_version, patch_version, + unnest( + xpath( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' + , scl_data::xml + , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']] + ) + ) AS header + FROM scl_file) scl_data + ON scl_data.id = scl_file.id + AND scl_data.major_version = scl_file.major_version + AND scl_data.minor_version = scl_file.minor_version + AND scl_data.patch_version = scl_file.patch_version) xml_data + ON xml_data.id = sf.id + AND xml_data.major_version = sf.major_version + AND xml_data.minor_version = sf.minor_version + AND xml_data.patch_version = sf.patch_version + INNER JOIN archived_resource_resource_tag arrt + ON ar.id = arrt.archived_resource_id + INNER JOIN resource_tag rt + ON arrt.resource_tag_id = rt.id + LEFT JOIN referenced_resource rr + ON ar.referenced_resource_id = rr.id + LEFT JOIN location l + ON sf.location_id = l.id OR rr.location_id = l.id + WHERE 1 = 1 """); if (location != null && !location.isBlank()) { parameters.add(location); - sb.append(" AND ar.location = ?"); + sb.append(" AND l.name = ?"); } if (name != null && !name.isBlank()) { parameters.add("%"+name+"%"); - sb.append(" AND ar.name ILIKE ?"); + parameters.add("%"+name+"%"); + sb.append(" AND (rr.filename ILIKE ? OR sf.name ILIKE ?)"); } if (approver != null && !approver.isBlank()) { + parameters.add(approver); //ToDo cgutmann add xpath subselect parameters.add(approver); - sb.append(" AND ar.approver = ?"); + sb.append(" AND (rr.approver = ? OR xml_data.hitem_who::varchar = ?)"); } if (contentType != null && !contentType.isBlank()) { parameters.add(contentType); - sb.append(" AND ar.content_type = ?"); + sb.append(" AND rr.content_type = ?"); } if (type != null && !type.isBlank()) { parameters.add(type); - sb.append(" AND ar.type = ?"); + parameters.add(type); + sb.append(" AND (rr.type = ? OR sf.type = ?)"); } if (voltage != null && !voltage.isBlank()) { parameters.add(voltage); - sb.append(" AND ar.voltage = ?"); + //ToDo cgutmann: add subquery for voltage from scl data + sb.append(" AND sf.voltage = ?"); } if (from != null) { parameters.add(from); @@ -823,10 +1440,118 @@ public IArchivedResourcesMetaItem searchArchivedResource(String location, String sb.append(" AND ar.archived_at <= ?"); } sb.append(System.lineSeparator()); - sb.append("GROUP BY ar.id, ar.name, ar.major_version, ar.minor_version, ar.patch_version, ar.location, ar.note, ar.approver, ar.type, ar.content_type, ar.voltage, ar.modified_at, ar.archived_at;"); + sb.append("GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar;"); return new ArchivedResourcesMetaItem(executeArchivedResourceQuery(sb.toString(), parameters)); } + @Override + public IArchivedResourcesHistoryMetaItem searchArchivedResourceHistory(UUID uuid) { + String sql = """ + SELECT ar.*, + COALESCE(sf.name, rr.filename) AS name, + COALESCE(sf.created_by, rr.author) AS author, + COALESCE(xml_data.hitem_who::varchar, rr.approver) AS approver, + rr.content_type AS content_type, + COALESCE(sf.type, rr.type) AS type, + sf.creation_date AS modified_at, + ar.archived_at AS archived_at, + xml_data.hitem_what::varchar AS comment, + null AS voltage, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.name AS location, + ar.scl_file_id IS NOT NULL OR ar.referenced_resource_id IS NOT NULL AS is_archived + FROM archived_resource ar + LEFT JOIN scl_file sf + ON sf.id = ar.scl_file_id + AND sf.major_version = ar.scl_file_major_version + AND sf.minor_version = ar.scl_file_minor_version + AND sf.patch_version = ar.scl_file_patch_version + LEFT JOIN + (SELECT scl_file.id, scl_file.major_version, scl_file.minor_version, scl_file.patch_version + , (xpath('/scl:Hitem/@who', scl_data.header, ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_who + , (xpath('/scl:Hitem/@what', scl_data.header, ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_what + FROM scl_file + INNER JOIN (SELECT id, major_version, minor_version, patch_version, + unnest( + xpath( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' + , scl_data::xml + , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']] + ) + ) AS header + FROM scl_file) scl_data + ON scl_data.id = scl_file.id + AND scl_data.major_version = scl_file.major_version + AND scl_data.minor_version = scl_file.minor_version + AND scl_data.patch_version = scl_file.patch_version) xml_data + ON xml_data.id = sf.id + AND xml_data.major_version = sf.major_version + AND xml_data.minor_version = sf.minor_version + AND xml_data.patch_version = sf.patch_version + INNER JOIN archived_resource_resource_tag arrt + ON ar.id = arrt.archived_resource_id + INNER JOIN resource_tag rt + ON arrt.resource_tag_id = rt.id + LEFT JOIN referenced_resource rr + ON ar.referenced_resource_id = rr.id + LEFT JOIN location l + ON sf.location_id = l.id OR rr.location_id = l.id + WHERE ? in (sf.id, rr.id) + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar, xml_data.hitem_what::varchar; + """; + + List versions = new ArrayList<>(); + + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, uuid); + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + List resourceTags = getResourceTagItems(resultSet); + versions.add(mapToArchivedResourceVersion(resultSet, resourceTags)); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error retrieving Archived Resource History entries from database!", exp); + } + + return new ArchivedResourcesHistoryMetaItem(versions); + } + + private ArchivedResourceVersion mapToArchivedResourceVersion(ResultSet resultSet, List resourceTags) throws SQLException { + Version version; + if (resultSet.getObject("scl_file_id") != null) { + version = new Version( + resultSet.getInt("scl_file_major_version"), + resultSet.getInt("scl_file_minor_version"), + resultSet.getInt("scl_file_patch_version") + ); + } else { + version = new Version( + resultSet.getInt("referenced_resource_major_version"), + resultSet.getInt("referenced_resource_minor_version"), + resultSet.getInt("referenced_resource_patch_version") + ); + } + + return new ArchivedResourceVersion( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + version.toString(), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), //ToDo cgutmann: find out where to get comnment value + resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), + resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), + resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_CONTENT_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_VOLTAGE_FIELD), + resourceTags, + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_MODIFIED_AT_FIELD)), + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_ARCHIVED_AT_FIELD)), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), + resultSet.getBoolean("is_archived") + ); + } private List executeHistoryQuery(String sql, List parameters) { List items = new ArrayList<>(); @@ -851,6 +1576,81 @@ private List executeHistoryQuery(String sql, List para return items; } + private void updateArchivedResourceToResourceTagMappingTable(UUID id, List resourceTags) { + List newMappingEntries = resourceTags.stream().filter(entry -> + !existsResourceTagMapping(id, UUID.fromString(entry.getId())) + ).toList(); + + String insertStatement = """ + INSERT INTO archived_resource_resource_tag(archived_resource_id, resource_tag_id) + VALUES (?, ?); + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement mappingStmt = connection.prepareStatement(insertStatement)) { + newMappingEntries.forEach(entry -> { + try { + mappingStmt.setObject(1, id); + mappingStmt.setObject(2, UUID.fromString(entry.getId())); + mappingStmt.addBatch(); + mappingStmt.clearParameters(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding archived resource to resource tag mapping entry to database!", exp); + } + }); + mappingStmt.executeBatch(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding archived resource to resource tag mapping entries to database!", exp); + } + } + + private boolean existsLocationResourceTagMapping(UUID id, UUID tagId) { + String query = """ + SELECT * + FROM location_resource_tag + WHERE location_id = ? + AND resource_tag_id = ?; + """; + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(query) + ) { + stmt.setObject(1, id); + stmt.setObject(2, tagId); + try (ResultSet resultSet = stmt.executeQuery()) { + return resultSet.next(); + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException( + POSTGRES_SELECT_ERROR_CODE, + "Error listing scl entries from database!", exp + ); + } + } + + private boolean existsResourceTagMapping(UUID id, UUID tagId) { + String query = """ + SELECT * + FROM archived_resource_resource_tag + WHERE archived_resource_id = ? + AND resource_tag_id = ?; + """; + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(query) + ) { + stmt.setObject(1, id); + stmt.setObject(2, tagId); + try (ResultSet resultSet = stmt.executeQuery()) { + return resultSet.next(); + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException( + POSTGRES_SELECT_ERROR_CODE, + "Error listing scl entries from database!", exp + ); + } + } + private HistoryMetaItem mapResultSetToHistoryMetaItem(ResultSet resultSet) throws SQLException { return new HistoryMetaItem( resultSet.getString(ID_FIELD), @@ -891,22 +1691,19 @@ private List executeLocationQuery(String sql, List pa } private LocationMetaItem mapResultSetToLocationMetaItem(ResultSet resultSet) throws SQLException { - UUID resourceId = resultSet.getObject(LOCATIONMETAITEM_RESOURCE_ID_FIELD, UUID.class); - int resourceCount = resourceId == null ? 0 : 1; return new LocationMetaItem( resultSet.getString(ID_FIELD), - resultSet.getString(LOCATIONMETAITEM_KEY_FIELD), + resultSet.getString(KEY_FIELD), resultSet.getString(NAME_FIELD), - resultSet.getString(LOCATIONMETAITEM_DESCRIPTION_FIELD), - resourceCount + resultSet.getString(LOCATIONMETAITEM_DESCRIPTION_FIELD) == null ? "" : resultSet.getString(LOCATIONMETAITEM_DESCRIPTION_FIELD), + Integer.parseInt(resultSet.getString("assigned_resources")) ); } - private List executeArchivedResourceQuery(String sql, List parameters) { - List items = new ArrayList<>(); - try ( - Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement(sql)) { + private List executeArchivedResourceQuery(String sql, List parameters) { + List items = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { for (int i = 0; i < parameters.size(); i++) { stmt.setObject(i + 1, parameters.get(i)); } @@ -935,28 +1732,56 @@ private List getResourceTagItems(ResultSet resultSet) throws S new ResourceTagItem( entry.split(";")[0], entry.split(";")[1], - entry.split(";")[2] + entry.split(";")[2].equalsIgnoreCase("null") ? null : entry.split(";")[2] ) ).forEach(resourceTags::add); } return resourceTags; } - private ArchivedResourceMetaItem mapResultSetToArchivedResourceMetaItem(ResultSet resultSet, List resourceTags) throws SQLException { - return new ArchivedResourceMetaItem( - resultSet.getString(ID_FIELD), - resultSet.getString(NAME_FIELD), - createVersion(resultSet), - resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), - resultSet.getString(ARCHIVEMETAITEM_NOTE_FIELD), - resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), - resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), - resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), - resultSet.getString(ARCHIVEMETAITEM_CONTENT_TYPE_FIELD), - resultSet.getString(ARCHIVEMETAITEM_VOLTAGE_FIELD), - convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_MODIFIED_AT_FIELD)), - convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_ARCHIVED_AT_FIELD)), - resourceTags - ); + private AbstractArchivedResourceMetaItem mapResultSetToArchivedResourceMetaItem(ResultSet resultSet, List resourceTags) throws SQLException { + String sclFileId = resultSet.getString("scl_file_id"); + if (sclFileId != null) { + Version version = new Version( + resultSet.getInt("scl_file_major_version"), + resultSet.getInt("scl_file_minor_version"), + resultSet.getInt("scl_file_patch_version") + ); + return new ArchivedSclResourceMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + version.toString(), + resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), + resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), + resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_CONTENT_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + resourceTags, + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_MODIFIED_AT_FIELD)), + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_ARCHIVED_AT_FIELD)), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), + resultSet.getString(ARCHIVEMETAITEM_VOLTAGE_FIELD) + ); + } else { + Version version = new Version( + resultSet.getInt("referenced_resource_major_version"), + resultSet.getInt("referenced_resource_minor_version"), + resultSet.getInt("referenced_resource_patch_version") + ); + return new ArchivedReferencedResourceMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + version.toString(), + resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), + resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), + resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_CONTENT_TYPE_FIELD), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + resourceTags, + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_MODIFIED_AT_FIELD)), + convertToOffsetDateTime(resultSet.getTimestamp(ARCHIVEMETAITEM_ARCHIVED_AT_FIELD)), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD) + ); + } } } \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql deleted file mode 100644 index f12ec256..00000000 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__fill_archived_resource.sql +++ /dev/null @@ -1,12 +0,0 @@ ---clear values on startup - -TRUNCATE TABLE archived_resource; - -insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) -values ('af71f378-5a13-47af-bbfc-aa668af14e25', 'archived_resource_1', '0', '0', '1', 'test1', 'test note', 'user1', 'user2', 'Schütz', 'SSD', '110', '2019-08-24T14:15:22Z', '2019-08-24T14:15:22Z'); - -insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) -values ('af71f378-5a13-47af-bbfc-aa668af14e26', 'archived_resource_2', '0', '1', '0', 'test2', 'test note', 'user2', 'user1', 'Leittechnik', 'IID', '220', '2020-08-24T14:15:22Z', '2021-08-24T14:15:22Z'); - -insert into archived_resource (id, name, major_version, minor_version, patch_version, location, note, author, approver, type, content_type, voltage, modified_at, archived_at) -values ('af71f378-5a13-47af-bbfc-aa668af14e27', 'archived_resource_3', '1', '0', '0', 'test3', 'test note', 'user3', 'user1', 'Schütz', 'ICD', '380', '2022-08-24T14:15:22Z', '2023-08-24T14:15:22Z'); \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql deleted file mode 100644 index 4833765c..00000000 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__fill_resource_tag.sql +++ /dev/null @@ -1,12 +0,0 @@ ---clear values on startup - -TRUNCATE TABLE resource_tag; - -insert into resource_tag (id, key, value, archived_resource_id) -values ('bf71f378-5a13-47af-bbfc-aa668af14e25', 'test_tag', 'test_value', 'af71f378-5a13-47af-bbfc-aa668af14e25'); - -insert into resource_tag (id, key, value, archived_resource_id) -values ('bf71f378-5a13-47af-bbfc-aa668af15e25', 'test_tag_1', 'test_value_1', 'af71f378-5a13-47af-bbfc-aa668af14e25'); - -insert into resource_tag (id, key, value, archived_resource_id) -values ('bf71f378-5a13-47af-bbfc-aa668af74e25', 'test_tag', 'test_value', 'af71f378-5a13-47af-bbfc-aa668af14e27'); \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql index d16e5dde..302247d7 100644 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql @@ -1,32 +1,32 @@ -- -- Creating table to hold SCL History Data. The SCL is identified by it's ID, but there can be multiple versions. -- -create table scl_history ( - id uuid not null, - name varchar(255) not null, - major_version smallint not null, - minor_version smallint not null, - patch_version smallint not null, - type varchar(3) not null, - author varchar(255) not null, - comment varchar(255) not null, - changedAt TIMESTAMP WITH TIME ZONE not null, - archived boolean not null default false, - available boolean not null default true, - primary key (id, major_version, minor_version, patch_version) -); - -create index scl_history_type on scl_history(type); - -comment on table scl_history is 'Table holding all the SCL History Data. The combination id and version are unique (pk).'; -comment on column scl_history.id is 'Unique ID generated according to standards'; -comment on column scl_history.name is 'The name of the SCL File'; -comment on column scl_history.major_version is 'Versioning according to Semantic Versioning (Major Position)'; -comment on column scl_history.minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; -comment on column scl_history.patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; -comment on column scl_history.type is 'The type of SCL stored'; -comment on column scl_history.author is 'The author of the change in this version of the SCL File'; -comment on column scl_history.comment is 'The comment of the change in this version of the SCL File'; -comment on column scl_history.changedAt is 'The date of the change in this version of the SCL File'; -comment on column scl_history.archived is 'Is this version of the SCL File archived'; -comment on column scl_history.available is 'Is this version of the SCL File available'; +-- create table scl_history ( +-- id uuid not null, +-- name varchar(255) not null, +-- major_version smallint not null, +-- minor_version smallint not null, +-- patch_version smallint not null, +-- type varchar(3) not null, +-- author varchar(255) not null, +-- comment varchar(255) not null, +-- changedAt TIMESTAMP WITH TIME ZONE not null, +-- archived boolean not null default false, +-- available boolean not null default true, +-- primary key (id, major_version, minor_version, patch_version) +-- ); +-- +-- create index scl_history_type on scl_history(type); +-- +-- comment on table scl_history is 'Table holding all the SCL History Data. The combination id and version are unique (pk).'; +-- comment on column scl_history.id is 'Unique ID generated according to standards'; +-- comment on column scl_history.name is 'The name of the SCL File'; +-- comment on column scl_history.major_version is 'Versioning according to Semantic Versioning (Major Position)'; +-- comment on column scl_history.minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; +-- comment on column scl_history.patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; +-- comment on column scl_history.type is 'The type of SCL stored'; +-- comment on column scl_history.author is 'The author of the change in this version of the SCL File'; +-- comment on column scl_history.comment is 'The comment of the change in this version of the SCL File'; +-- comment on column scl_history.changedAt is 'The date of the change in this version of the SCL File'; +-- comment on column scl_history.archived is 'Is this version of the SCL File archived'; +-- comment on column scl_history.available is 'Is this version of the SCL File available'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql index fd240448..78024ebc 100644 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql @@ -7,7 +7,6 @@ create table location ( key varchar(255) not null, name varchar(255) not null, description varchar(255), - resource_id uuid default null, primary key (id) ); @@ -16,4 +15,3 @@ comment on column location.id is 'Unique ID generated according to standards'; comment on column location.key is 'The key of the Location'; comment on column location.name is 'The name of the Location'; comment on column location.description is 'The description of the Location'; -comment on column location.resource_id is 'The unique ID of the assigned resource, NULL if no resource is assigned'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql deleted file mode 100644 index 4d58dee1..00000000 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_archived_resource.sql +++ /dev/null @@ -1,36 +0,0 @@ --- --- Creating table to hold Archived Resource Data. A Archived Resource is identified by its ID, major-, minor- and patch version. --- - -create table archived_resource ( - id uuid not null, - name varchar(255) not null, - major_version smallint not null, - minor_version smallint not null, - patch_version smallint not null, - location varchar(255), - note varchar(255), - author varchar(255), - approver varchar(255), - type varchar(255), - content_type varchar(3), - voltage varchar(255), - modified_at TIMESTAMP WITH TIME ZONE not null, - archived_at TIMESTAMP WITH TIME ZONE not null, - primary key (id, major_version, minor_version, patch_version) -); - -comment on table archived_resource is 'Table holding all the Archived Resource Data and its location assignments. The id, the major-, minor- and patch versions are unique (pk).'; -comment on column archived_resource.id is 'Unique ID generated according to standards'; -comment on column archived_resource.name is 'The name of the Location'; -comment on column archived_resource.major_version is 'The major version of the Archived Resource'; -comment on column archived_resource.minor_version is 'The minor version of the Archived Resource'; -comment on column archived_resource.patch_version is 'The patch version of the Archived Resource'; -comment on column archived_resource.location is 'The assigned Location of the Archived Resource'; -comment on column archived_resource.note is 'The note of Archived Resource'; -comment on column archived_resource.approver is 'The approver of the Archived Resource'; -comment on column archived_resource.type is 'The type of the Archived Resource'; -comment on column archived_resource.content_type is 'The content type of the Archived Resource'; -comment on column archived_resource.voltage is 'The voltage of the Archived Resource'; -comment on column archived_resource.modified_at is 'The modified timestamp of the Archived Resource'; -comment on column archived_resource.archived_at is 'The archivedAt timestamp of the Archived Resource'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql deleted file mode 100644 index 96ecdd8b..00000000 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_resource_tag.sql +++ /dev/null @@ -1,17 +0,0 @@ --- --- Creating table to hold Resource Tag Data. The Resource Tag is identified by its ID. --- - -create table resource_tag ( - id uuid not null, - key varchar(255) not null, - value varchar(255), - archived_resource_id uuid, - primary key (id) -); - -comment on table resource_tag is 'Table holding all the Resource Tag data. The id is unique (pk).'; -comment on column resource_tag.id is 'Unique ID generated according to standards'; -comment on column resource_tag.key is 'The key of the Resource Tag'; -comment on column resource_tag.value is 'The value of the Resource Tag'; -comment on column resource_tag.archived_resource_id is 'The uuid of a archived resource associated to the Resource Tag'; \ No newline at end of file diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java deleted file mode 100644 index 8ee29c31..00000000 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceMetaItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.lfenergy.compas.scl.data.model; - -import java.time.OffsetDateTime; -import java.util.List; - -public interface IArchivedResourceMetaItem extends IAbstractItem { - String getLocation(); - - String getNote(); - - String getAuthor(); - - String getApprover(); - - String getType(); - - String getContentType(); - - String getVoltage(); - - OffsetDateTime getModifiedAt(); - - OffsetDateTime getArchivedAt(); - - List getFields(); -} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java index 878dcaa5..a65feaad 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java @@ -3,5 +3,5 @@ import java.util.List; public interface IArchivedResourcesMetaItem { - List getResources(); + List getResources(); } diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java index 9e21a347..ed350520 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java @@ -13,6 +13,8 @@ public interface IHistoryMetaItem extends IAbstractItem { String getComment(); + String getLocation(); + OffsetDateTime getChangedAt(); boolean isArchived(); diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index 0a65ea5c..c1938f76 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -172,6 +172,10 @@ public interface CompasSclDataRepository { */ ILocationMetaItem createLocation(UUID id, String key, String name, String description); + void addLocationTags(ILocationMetaItem location, String author); + + void deleteLocationTags(ILocationMetaItem location); + /** * List Location entries * @@ -222,12 +226,14 @@ public interface CompasSclDataRepository { */ void unassignResourceFromLocation(UUID locationId, UUID resourceId); - IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body); + IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version version, String author, String approver, String contentType, String filename); - IArchivedResourceMetaItem archiveSclResource(UUID id, String version); + IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver); IArchivedResourcesMetaItem searchArchivedResource(UUID id); IArchivedResourcesMetaItem searchArchivedResource(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to); + IArchivedResourcesHistoryMetaItem searchArchivedResourceHistory(UUID uuid); + } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index ef01381a..6c3ec3c5 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -19,9 +19,7 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.File; +import java.io.*; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.time.OffsetDateTime; @@ -44,6 +42,7 @@ public class CompasSclDataService { private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; + private final String DEFAULT_PATH = System.getProperty("user.dir") + File.separator + "locations"; @Inject public CompasSclDataService(CompasSclDataRepository repository, ElementConverter converter, @@ -496,8 +495,12 @@ public List listLocations(int page, int pageSize) { * @return The created Location entry */ @Transactional(REQUIRED) - public ILocationMetaItem createLocation(String key, String name, String description) { - return repository.createLocation(UUID.randomUUID(), key, name, description); + public ILocationMetaItem createLocation(String key, String name, String description, String author) { + ILocationMetaItem createdLocation = repository.createLocation(UUID.randomUUID(), key, name, description); + repository.addLocationTags(createdLocation, author); + File newLocationDirectory = new File(DEFAULT_PATH + File.separator + createdLocation.getName()); + newLocationDirectory.mkdirs(); + return createdLocation; } /** @@ -507,12 +510,17 @@ public ILocationMetaItem createLocation(String key, String name, String descript */ @Transactional(REQUIRED) public void deleteLocation(UUID id) { - int assignedResourceCount = repository.findLocationByUUID(id).getAssignedResources(); + ILocationMetaItem locationToDelete = repository.findLocationByUUID(id); + int assignedResourceCount = locationToDelete.getAssignedResources(); if (assignedResourceCount > 0) { throw new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, String.format("Deletion of Location %s not allowed, unassign resources before deletion", id)); } + + repository.deleteLocationTags(locationToDelete); repository.deleteLocation(id); + File directory = new File(DEFAULT_PATH + File.separator + locationToDelete.getName()); + directory.delete(); } /** @@ -530,7 +538,7 @@ public ILocationMetaItem updateLocation(UUID id, String key, String name, String } /** - * Assigns a Resource id to a Location entry + * Assign a Resource id to a Location entry * * @param locationId The id of the Location entry * @param resourceId The id of the Resource @@ -544,7 +552,7 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { } /** - * Unassigns a Resource id from a Location entry + * Unassign a Resource from a Location entry * * @param locationId The id of the Location entry * @param resourceId The id of the Resource entry @@ -553,25 +561,103 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { ILocationMetaItem item = repository.findLocationByUUID(locationId); if (item != null) { + //ToDo cgutmann: ask what should happen with resources in filesystem - should they be deleted? repository.unassignResourceFromLocation(locationId, resourceId); } } + /** + * Archive a resource and links it to the corresponding scl_file + * + * @param id The id of the scl_file + * @param version The version of the scl_file + * @param author The author of the resource + * @param approver The approver of the resource + * @param contentType The content type of the resource + * @param filename The filename of the resource + * @param body The content of the resource + * @return The created archived resource item + */ @Transactional(REQUIRED) - public IArchivedResourceMetaItem archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { - return repository.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body); + public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version, String author, String approver, String contentType, String filename, File body) { + IAbstractArchivedResourceMetaItem archivedResource = repository.archiveResource(id, new Version(version), author, approver, contentType, filename); + if (body != null) { + String absolutePath = DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); + File locationDir = new File(absolutePath); + locationDir.mkdirs(); + File f = new File(absolutePath + File.separator + filename); + try (FileOutputStream fos = new FileOutputStream(f)) { + try (FileInputStream fis = new FileInputStream(body)) { + fos.write(fis.readAllBytes()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return archivedResource; } + /** + * Archive an existing scl resource + * + * @param id The id of the resource to be archived + * @param version The version of the resource to be archived + * @param approver The approver of the archiving action + * @return The archived resource item + */ @Transactional(REQUIRED) - public IArchivedResourceMetaItem archiveSclResource(UUID id, String version) { - return repository.archiveSclResource(id, version); + public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { + IAbstractArchivedResourceMetaItem archivedResource = repository.archiveSclResource(id, version, approver); + String data = repository.findByUUID(id, version); + if (data != null) { + String absolutePath = DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); + File locationDir = new File(absolutePath); + locationDir.mkdirs(); + File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getContentType().toLowerCase()); + try (FileWriter fw = new FileWriter(f)) { + fw.write(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return archivedResource; } + /** + * Retrieve all archived resource history versions according to an archived resource id + * + * @param uuid The id of the archived resource + * @return All archived versions of the given id + */ + @Transactional(SUPPORTS) + public IArchivedResourcesHistoryMetaItem getArchivedResourceHistory(UUID uuid) { + return repository.searchArchivedResourceHistory(uuid); + } + + /** + * Retrieve all entries according to an archived resource id + * + * @param uuid The id of the archived resource + * @return All archived entries of the given id + */ @Transactional(SUPPORTS) public IArchivedResourcesMetaItem searchArchivedResources(UUID uuid) { return repository.searchArchivedResource(uuid); } + /** + * Retrieve all archived entries according to the search parameters + * + * @param location The location of the archived resource + * @param name The name of the archived resource + * @param approver The approver of the archived resource + * @param contentType The content type of the resource + * @param type The type of the resource + * @param voltage The voltage of the resource + * @param from The start timestamp of archiving (including) + * @param to The end timestamp of archiving (including) + * @return All archived entries matching the search criteria + */ @Transactional(SUPPORTS) public IArchivedResourcesMetaItem searchArchivedResources(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to) { return repository.searchArchivedResource(location, name, approver, getSclFileType(contentType), type, voltage, from, to); From 4e02a582a083d1010a153be0e07012321a1075b3 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 17 Dec 2024 10:25:02 +0100 Subject: [PATCH 07/23] feat: implement api changes locations api - add username as parameter when creating a location history api - remove history_scl table - add location to data resource object archiving api - add archive resource history endpoint --- .../rest/api/archive/ArchiveResource.java | 34 ++- .../rest/api/locations/LocationsResource.java | 11 +- .../api/locations/model/ErrorResponseDto.java | 8 +- .../rest/api/locations/model/Location.java | 12 +- .../rest/api/locations/model/Locations.java | 6 +- .../rest/api/locations/model/Pagination.java | 6 +- .../data/rest/api/scl/HistoryResource.java | 7 +- app/src/main/openapi/archiving-api.yaml | 69 ++++- app/src/main/openapi/history-api.yaml | 6 + .../rest/api/archive/ArchiveResourceTest.java | 136 +++++++++ ...ivedReferencedResourceTestDataBuilder.java | 95 ++++++ .../ArchivedSclResourceTestDataBuilder.java | 99 ++++++ .../api/locations/LocationsResourceTest.java | 6 +- .../scl/HistoryResourceTestdataBuilder.java | 3 +- .../AbstractArchivedResourceMetaItem.java | 59 ++++ .../ArchivedReferencedResourceMetaItem.java | 39 +++ .../data/model/ArchivedResourceVersion.java | 96 ++++++ .../ArchivedResourcesHistoryMetaItem.java | 21 ++ .../model/ArchivedSclResourceMetaItem.java | 45 +++ .../CompasSclDataPostgreSQLRepository.java | 281 ++++++++++-------- .../migration/V1_10__create_resource_tag.sql | 15 + ..._create_archived_resource_resource_tag.sql | 24 ++ .../V1_12__scl_file_add_location_id.sql | 8 + .../V1_13__create_location_resource_tag.sql | 24 ++ .../db/migration/V1_5__create_scl_history.sql | 32 -- .../V1_8__create_referenced_resource.sql | 42 +++ .../V1_9__create_archived_resource.sql | 38 +++ .../CompasSclDataServiceErrorCode.java | 1 + .../IAbstractArchivedResourceMetaItem.java | 31 ++ .../data/model/IArchivedResourceVersion.java | 30 ++ .../IArchivedResourcesHistoryMetaItem.java | 7 + .../repository/CompasSclDataRepository.java | 54 +++- .../data/service/CompasSclDataService.java | 6 +- 33 files changed, 1164 insertions(+), 187 deletions(-) create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedReferencedResourceTestDataBuilder.java create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedSclResourceTestDataBuilder.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesHistoryMetaItem.java create mode 100644 repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedSclResourceMetaItem.java create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__create_resource_tag.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__create_archived_resource_resource_tag.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_12__scl_file_add_location_id.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_13__create_location_resource_tag.sql delete mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_referenced_resource.sql create mode 100644 repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_archived_resource.sql create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceVersion.java create mode 100644 repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesHistoryMetaItem.java diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java index 1eed25a2..2a59ff0a 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -4,20 +4,24 @@ import io.smallrye.mutiny.infrastructure.Infrastructure; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.eclipse.microprofile.jwt.JsonWebToken; import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.data.rest.UserInfoProperties; -import org.lfenergy.compas.scl.data.rest.api.archive.model.*; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourceVersion; +import org.lfenergy.compas.scl.data.rest.api.archive.model.*; import org.lfenergy.compas.scl.data.service.CompasSclDataService; import java.io.File; +import java.rmi.UnexpectedException; import java.time.OffsetDateTime; import java.util.UUID; @RequestScoped public class ArchiveResource implements ArchivingApi { + private static final Logger LOGGER = LogManager.getLogger(ArchiveResource.class); private final CompasSclDataService compasSclDataService; private final JsonWebToken jsonWebToken; private final UserInfoProperties userInfoProperties; @@ -35,7 +39,13 @@ public Uni archiveResource(UUID id, String version, String xAu .item(() -> compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResource); + .transform(this::mapToArchivedResource) + .onFailure().transform(e -> { + LOGGER.error("Failed to archive resource {} for scl resource {} and version {}", xFilename, id, version, e); + return new UnexpectedException( + String.format("Error while archiving data resource %s for scl resource %s and version %s", xFilename, id, version), + (Exception) e); + }); } @Override @@ -45,7 +55,13 @@ public Uni archiveSclResource(UUID id, String version) { .item(() -> compasSclDataService.archiveSclResource(id, new Version(version), approver)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResource); + .transform(this::mapToArchivedResource) + .onFailure().transform(e -> { + LOGGER.error("Failed to archive SCL resource {} with version {}", id, version, e); + return new UnexpectedException( + String.format("Error while archiving data resource %s with version %s", id, version), + (Exception) e); + }); } @Override @@ -54,7 +70,11 @@ public Uni retrieveArchivedResourceHistory(UUID id) { .item(() -> compasSclDataService.getArchivedResourceHistory(id)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResourcesHistory); + .transform(this::mapToArchivedResourcesHistory) + .onFailure().transform(e -> { + LOGGER.error("Failed to retrieve archived resource history for {}", id, e); + return new UnexpectedException(String.format("Error while retrieving data resource history for %s", id), (Exception) e); + }); } @Override @@ -63,7 +83,11 @@ public Uni searchArchivedResources(ArchivedResourcesSearch ar .item(() -> getArchivedResourcesMetaItem(archivedResourcesSearch)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResources); + .transform(this::mapToArchivedResources) + .onFailure().transform(e -> { + LOGGER.error("Failed to find archived resources", e); + return new UnexpectedException("Error while finding archived data resources", (Exception) e); + }); } private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResourcesSearch archivedResourcesSearch) { diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java index c1f75862..04a5b81a 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -6,7 +6,9 @@ import jakarta.inject.Inject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; +import org.lfenergy.compas.scl.data.rest.UserInfoProperties; import org.lfenergy.compas.scl.data.rest.api.locations.model.Location; import org.lfenergy.compas.scl.data.service.CompasSclDataService; @@ -18,10 +20,14 @@ public class LocationsResource implements LocationsApi { private static final Logger LOGGER = LogManager.getLogger(LocationsResource.class); private final CompasSclDataService compasSclDataService; + private final JsonWebToken jsonWebToken; + private final UserInfoProperties userInfoProperties; @Inject - public LocationsResource(CompasSclDataService compasSclDataService) { + public LocationsResource(CompasSclDataService compasSclDataService, JsonWebToken jsonWebToken, UserInfoProperties userInfoProperties) { this.compasSclDataService = compasSclDataService; + this.jsonWebToken = jsonWebToken; + this.userInfoProperties = userInfoProperties; } @Override @@ -36,7 +42,8 @@ public Uni createLocation(Location location) { .item(() -> compasSclDataService.createLocation( location.getKey(), location.getName(), - location.getDescription() + location.getDescription(), + jsonWebToken.getClaim(userInfoProperties.name()) )) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java index 217bb663..8cf43e3b 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/ErrorResponseDto.java @@ -24,7 +24,7 @@ public ErrorResponseDto timestamp(OffsetDateTime timestamp) { return this; } - + @JsonProperty("timestamp") @NotNull public OffsetDateTime getTimestamp() { return timestamp; @@ -42,7 +42,7 @@ public ErrorResponseDto code(String code) { return this; } - + @JsonProperty("code") @NotNull public String getCode() { return code; @@ -60,7 +60,7 @@ public ErrorResponseDto message(String message) { return this; } - + @JsonProperty("message") @NotNull public String getMessage() { return message; @@ -95,7 +95,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ErrorResponseDto {\n"); - + sb.append(" timestamp: ").append(toIndentedString(timestamp)).append("\n"); sb.append(" code: ").append(toIndentedString(code)).append("\n"); sb.append(" message: ").append(toIndentedString(message)).append("\n"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java index 1ad76a58..c4a5cb59 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Location.java @@ -25,7 +25,7 @@ public Location uuid(String uuid) { return this; } - + @JsonProperty("uuid") public String getUuid() { return uuid; @@ -44,7 +44,7 @@ public Location key(String key) { return this; } - + @JsonProperty("key") @NotNull public String getKey() { return key; @@ -63,7 +63,7 @@ public Location name(String name) { return this; } - + @JsonProperty("name") @NotNull public String getName() { return name; @@ -82,7 +82,7 @@ public Location description(String description) { return this; } - + @JsonProperty("description") public String getDescription() { return description; @@ -101,7 +101,7 @@ public Location assignedResources(Integer assignedResources) { return this; } - + @JsonProperty("assignedResources") public Integer getAssignedResources() { return assignedResources; @@ -138,7 +138,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Location {\n"); - + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); sb.append(" key: ").append(toIndentedString(key)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java index cff9ca61..509099f0 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Locations.java @@ -25,7 +25,7 @@ public Locations locations(List<@Valid Location> locations) { return this; } - + @JsonProperty("locations") @NotNull @Valid public List<@Valid Location> getLocations() { return locations; @@ -59,7 +59,7 @@ public Locations pagination(Pagination pagination) { return this; } - + @JsonProperty("pagination") @Valid public Pagination getPagination() { return pagination; @@ -93,7 +93,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Locations {\n"); - + sb.append(" locations: ").append(toIndentedString(locations)).append("\n"); sb.append(" pagination: ").append(toIndentedString(pagination)).append("\n"); sb.append("}"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java index 6eb5db8f..717f7983 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/model/Pagination.java @@ -21,7 +21,7 @@ public Pagination page(Integer page) { return this; } - + @JsonProperty("page") @NotNull public Integer getPage() { return page; @@ -39,7 +39,7 @@ public Pagination pageSize(Integer pageSize) { return this; } - + @JsonProperty("pageSize") @NotNull public Integer getPageSize() { return pageSize; @@ -73,7 +73,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Pagination {\n"); - + sb.append(" page: ").append(toIndentedString(page)).append("\n"); sb.append(" pageSize: ").append(toIndentedString(pageSize)).append("\n"); sb.append("}"); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java index bba08af5..899b5f17 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java @@ -73,7 +73,8 @@ private DataResource mapToDataResource(IHistoryMetaItem e) { .changedAt(e.getChangedAt()) .version(e.getVersion()) .available(e.isAvailable()) - .deleted(e.isDeleted()); + .deleted(e.isDeleted()) + .location(e.getLocation()); } @Override @@ -96,8 +97,8 @@ private DataResourceVersion mapToDataResourceVersion(IHistoryMetaItem e) { .available(e.isAvailable()) .deleted(e.isDeleted()) .comment(e.getComment()) - .archived(e.isArchived()); - + .archived(e.isArchived()) + .location(e.getLocation()); } @Override diff --git a/app/src/main/openapi/archiving-api.yaml b/app/src/main/openapi/archiving-api.yaml index 64299b04..8034881f 100644 --- a/app/src/main/openapi/archiving-api.yaml +++ b/app/src/main/openapi/archiving-api.yaml @@ -512,7 +512,51 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponseDto' - + /api/archive/resources/{id}/versions: + get: + tags: + - archiving + description: Search for all versions for a given resource uuid + operationId: retrieveArchivedResourceHistory + parameters: + - name: id + in: path + description: Unique data resource identifier + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Succefully retrieved data resource versions + content: + application/json: + schema: + $ref: '#/components/schemas/ArchivedResourcesHistory' + '400': + description: One of the specified Parameters is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: Unable to finde data resource with given unique identifier + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + default: + description: Unexpected Error, cannot handle request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' components: securitySchemes: @@ -567,6 +611,29 @@ components: type: integer format: int32 description: "Number of resources assigned to this location" + ArchivedResourcesHistory: + type: object + required: + - versions + properties: + versions: + type: array + items: + $ref: '#/components/schemas/ArchivedResourceVersion' + ArchivedResourceVersion: + allOf: + - $ref: '#/components/schemas/ArchivedResource' + - type: object + required: + - archived + properties: + comment: + type: string + description: "Comment given when uploading the data resource" + archived: + type: boolean + description: "Defines if given data resource is archived" + default: false ArchivedResource: type: object required: diff --git a/app/src/main/openapi/history-api.yaml b/app/src/main/openapi/history-api.yaml index 2bd57e42..dc2cb5f3 100644 --- a/app/src/main/openapi/history-api.yaml +++ b/app/src/main/openapi/history-api.yaml @@ -168,6 +168,9 @@ components: name: type: string description: "Partially match allowed" + location: + type: string + description: "The location associated with the resource" author: type: string description: "Fulltext match which can be retrieved via extra endpoint" @@ -228,6 +231,9 @@ components: type: boolean description: "Defines if a resource is marked as deleted" default: false + location: + type: string + description: "The location associated with the resource" DataResourceHistory: type: object required: diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java new file mode 100644 index 00000000..6e72f238 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java @@ -0,0 +1,136 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.response.Response; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.Test; +import org.lfenergy.compas.scl.data.model.ArchivedResourcesMetaItem; +import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.IArchivedResourcesMetaItem; +import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; + +import java.util.List; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@QuarkusTest +@TestHTTPEndpoint(ArchiveResource.class) +@TestSecurity(user = "test-user") +class ArchiveResourceTest { + + @InjectMock + private CompasSclDataService compasSclDataService; + @InjectMock + private JsonWebToken jwt; + + @Test + void archiveSclResource_WhenCalled_ThenReturnsArchivedResource() { + UUID uuid = UUID.randomUUID(); + String name = "Name"; + String version = "1.0.0"; + IAbstractArchivedResourceMetaItem testData = new ArchivedSclResourceTestDataBuilder().setId(uuid.toString()).build(); + when(jwt.getClaim("name")).thenReturn(""); + when(compasSclDataService.archiveSclResource(uuid, new Version(version), "")).thenReturn(testData); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .when().post("/scl/" + uuid + "/versions/" + version) + .then() + .statusCode(200) + .extract() + .response(); + + ArchivedResource result = response.as(ArchivedResource.class); + assertEquals(uuid, UUID.fromString(result.getUuid())); + assertEquals(name, result.getName()); + assertEquals(version, result.getVersion()); + } + +// @Test +// void archiveResource_WhenCalled_ThenReturnsArchivedResource() { +// UUID uuid = UUID.randomUUID(); +// String name = "Name"; +// String version = "1.0.0"; +// IAbstractArchivedResourceMetaItem testData = new ArchivedReferencedResourceTestDataBuilder().setId(uuid.toString()).build(); +// File f = Paths.get("src","test","resources","scl", "icd_import_ied_test.scd").toFile(); +// //ToDo cgutmann: fix this test -> file not the same as in test function -> therefore request not mocked +// when(compasSclDataService.archiveResource(uuid, version, null, null, "application/json", null, f)).thenReturn(testData); +// Response response = given() +// .contentType(MediaType.APPLICATION_JSON) +// .body(f) +// .when().post("/referenced-resource/" + uuid + "/versions/" + version) +// .then() +// .statusCode(200) +// .extract() +// .response(); +// +// ArchivedResource result = response.as(ArchivedResource.class); +// assertEquals(uuid, UUID.fromString(result.getUuid())); +// assertEquals(name, result.getName()); +// assertEquals(version, result.getVersion()); +// } + + @Test + void searchArchivedResources_WhenCalledWithUuid_ThenReturnsMatchingArchivedResources() { + UUID uuid = UUID.randomUUID(); + String name = "Name"; + String version = "1.0.0"; + IAbstractArchivedResourceMetaItem testData1 = new ArchivedSclResourceTestDataBuilder().setId(uuid.toString()).setName(name).setVersion(version).build(); + IAbstractArchivedResourceMetaItem testData2 = new ArchivedSclResourceTestDataBuilder().setId(uuid.toString()).setName(name).setVersion("1.0.1").build(); + IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(testData1, testData2)); + + when(compasSclDataService.searchArchivedResources(uuid)).thenReturn(archivedResources); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body("{\"uuid\": \"" + uuid + "\"}") + .when().post("/resources/search") + .then() + .statusCode(200) + .extract() + .response(); + + ArchivedResources result = response.as(ArchivedResources.class); + assertEquals(uuid, UUID.fromString(result.getResources().get(0).getUuid())); + assertEquals(name, result.getResources().get(0).getName()); + assertEquals(version, result.getResources().get(0).getVersion()); + } + + @Test + void searchArchivedResources_WhenCalledWithoutUuid_ThenReturnsMatchingArchivedResources() { + UUID uuid = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + String name = "Name"; + String version = "1.0.0"; + IAbstractArchivedResourceMetaItem testData1 = new ArchivedSclResourceTestDataBuilder().setId(uuid.toString()).setName(name).setVersion(version).build(); + IAbstractArchivedResourceMetaItem testData2 = new ArchivedSclResourceTestDataBuilder().setId(uuid2.toString()).setName(name).setVersion(version).build(); + IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(testData1, testData2)); + + when(compasSclDataService.searchArchivedResources(null, name, null,null,null,null,null, null)).thenReturn(archivedResources); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body("{\"name\": \"Name\"}") + .when().post("/resources/search") + .then() + .statusCode(200) + .extract() + .response(); + + ArchivedResources result = response.as(ArchivedResources.class); + assertEquals(uuid, UUID.fromString(result.getResources().get(0).getUuid())); + assertEquals(name, result.getResources().get(0).getName()); + assertEquals(version, result.getResources().get(0).getVersion()); + assertEquals(uuid2, UUID.fromString(result.getResources().get(1).getUuid())); + assertEquals(name, result.getResources().get(1).getName()); + assertEquals(version, result.getResources().get(1).getVersion()); + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedReferencedResourceTestDataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedReferencedResourceTestDataBuilder.java new file mode 100644 index 00000000..d9a0fb0a --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedReferencedResourceTestDataBuilder.java @@ -0,0 +1,95 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import org.lfenergy.compas.scl.data.model.ArchivedReferencedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.IResourceTagItem; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ArchivedReferencedResourceTestDataBuilder { + // Default values + private String id = UUID.randomUUID().toString(); + private String name = "Name"; + private String version = "1.0.0"; + private String location = "some location"; + private String note = "some note"; + private String author = "user1"; + private String approver = "user2"; + private String type = "some type"; + private String contentType = "contentType1"; + private OffsetDateTime modifiedAt = null; + private OffsetDateTime archivedAt = Instant.now().atZone(ZoneId.systemDefault()).toOffsetDateTime(); + private List fields = new ArrayList<>(); + + public ArchivedReferencedResourceTestDataBuilder() { + } + + public ArchivedReferencedResourceTestDataBuilder setId(String id) { + this.id = id; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setName(String name) { + this.name = name; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setVersion(String version) { + this.version = version; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setLocation(String location) { + this.location = location; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setNote(String note) { + this.note = note; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setAuthor(String author) { + this.author = author; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setApprover(String approver) { + this.approver = approver; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setType(String type) { + this.type = type; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setModifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + return this; + } + + public ArchivedReferencedResourceTestDataBuilder setFields(List fields) { + this.fields = fields; + return this; + } + + public IAbstractArchivedResourceMetaItem build() { + return new ArchivedReferencedResourceMetaItem(id, name, version, author, approver, type, contentType, location, fields, modifiedAt, archivedAt, note); + } +} diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedSclResourceTestDataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedSclResourceTestDataBuilder.java new file mode 100644 index 00000000..07b55519 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedSclResourceTestDataBuilder.java @@ -0,0 +1,99 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import org.lfenergy.compas.scl.data.model.*; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ArchivedSclResourceTestDataBuilder { + // Default values + private String id = UUID.randomUUID().toString(); + private String name = "Name"; + private String version = "1.0.0"; + private String location = "some location"; + private String note = "some note"; + private String author = "user1"; + private String approver = "user2"; + private String type = "some type"; + private String contentType = "contentType1"; + private String voltage = "100"; + private OffsetDateTime modifiedAt = null; + private OffsetDateTime archivedAt = Instant.now().atZone(ZoneId.systemDefault()).toOffsetDateTime(); + private List fields = new ArrayList<>(); + + public ArchivedSclResourceTestDataBuilder() { + } + + public ArchivedSclResourceTestDataBuilder setId(String id) { + this.id = id; + return this; + } + + public ArchivedSclResourceTestDataBuilder setName(String name) { + this.name = name; + return this; + } + + public ArchivedSclResourceTestDataBuilder setVersion(String version) { + this.version = version; + return this; + } + + public ArchivedSclResourceTestDataBuilder setLocation(String location) { + this.location = location; + return this; + } + + public ArchivedSclResourceTestDataBuilder setNote(String note) { + this.note = note; + return this; + } + + public ArchivedSclResourceTestDataBuilder setAuthor(String author) { + this.author = author; + return this; + } + + public ArchivedSclResourceTestDataBuilder setApprover(String approver) { + this.approver = approver; + return this; + } + + public ArchivedSclResourceTestDataBuilder setType(String type) { + this.type = type; + return this; + } + + public ArchivedSclResourceTestDataBuilder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public ArchivedSclResourceTestDataBuilder setVoltage(String voltage) { + this.voltage = voltage; + return this; + } + + public ArchivedSclResourceTestDataBuilder setModifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + return this; + } + + public ArchivedSclResourceTestDataBuilder setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + return this; + } + + public ArchivedSclResourceTestDataBuilder setFields(List fields) { + this.fields = fields; + return this; + } + + public IAbstractArchivedResourceMetaItem build() { + return new ArchivedSclResourceMetaItem(id, name, version, author, approver, type, contentType, location, fields, modifiedAt, archivedAt, note, voltage); + } +} diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java index 24934b89..d743e540 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java @@ -6,6 +6,7 @@ import io.quarkus.test.security.TestSecurity; import io.restassured.response.Response; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.Test; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; @@ -30,6 +31,8 @@ class LocationsResourceTest { @InjectMock private CompasSclDataService compasSclDataService; + @InjectMock + private JsonWebToken jwt; @Test void createLocation_WhenCalled_ThenReturnsCreatedLocation() { @@ -44,7 +47,8 @@ void createLocation_WhenCalled_ThenReturnsCreatedLocation() { location.setDescription(description); ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); - when(compasSclDataService.createLocation(key, name, description)).thenReturn(testData); + when(jwt.getClaim("name")).thenReturn("test user"); + when(compasSclDataService.createLocation(key, name, description, "test user")).thenReturn(testData); Response response = given() .contentType(MediaType.APPLICATION_JSON) .body(location) diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java index 4d4d39f1..80986820 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java @@ -15,6 +15,7 @@ public class HistoryResourceTestdataBuilder { private String type = "SSD"; private String author = "Test"; private String comment = "Created"; + private String location = "some location"; private OffsetDateTime changedAt = OffsetDateTime.now(); private boolean archived = false; private boolean available = false; @@ -72,6 +73,6 @@ public HistoryResourceTestdataBuilder setDeleted(boolean deleted) { // Build method to create a new HistoryMetaItem object public IHistoryMetaItem build() { - return new HistoryMetaItem(id, name, version, type, author, comment, changedAt, archived, available, deleted); + return new HistoryMetaItem(id, name, version, type, author, comment, location, changedAt, archived, available, deleted); } } diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java new file mode 100644 index 00000000..dd78ca33 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java @@ -0,0 +1,59 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public abstract class AbstractArchivedResourceMetaItem implements IAbstractArchivedResourceMetaItem, IAbstractItem { + + private final String author; + private final String approver; + private final String type; + private final String contentType; + private final String location; + private final List fields; + private final OffsetDateTime modifiedAt; + private final OffsetDateTime archivedAt; + + public AbstractArchivedResourceMetaItem(String author, String approver, String type, String contentType, String location, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt) { + this.author = author; + this.approver = approver; + this.type = type; + this.contentType = contentType; + this.location = location; + this.fields = fields; + this.modifiedAt = modifiedAt; + this.archivedAt = archivedAt; + } + + public String getAuthor() { + return author; + } + + public String getApprover() { + return approver; + } + + public String getType() { + return type; + } + + public String getContentType() { + return contentType; + } + + public String getLocation() { + return location; + } + + public List getFields() { + return fields; + } + + public OffsetDateTime getModifiedAt() { + return modifiedAt; + } + + public OffsetDateTime getArchivedAt() { + return archivedAt; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java new file mode 100644 index 00000000..0fbcc977 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java @@ -0,0 +1,39 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public class ArchivedReferencedResourceMetaItem extends AbstractArchivedResourceMetaItem implements IAbstractArchivedResourceMetaItem { + + String id; + String name; + String version; + String comment; + + public ArchivedReferencedResourceMetaItem(String id, String name, String version, String author, String approver, String type, String contentType, String location, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment) { + super(author, approver, type, contentType, location, fields, modifiedAt, archivedAt); + this.id = id; + this.name = name; + this.version = version; + this.comment = comment; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getVersion() { + return version; + } + + public String getComment() { + return comment; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java new file mode 100644 index 00000000..97dcc482 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java @@ -0,0 +1,96 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public class ArchivedResourceVersion extends AbstractItem implements IArchivedResourceVersion { + + String location; + String note; + String author; + String approver; + String type; + String contentType; + String voltage; + OffsetDateTime modifiedAt; + OffsetDateTime archivedAt; + List fields; + String comment; + boolean archived; + + public ArchivedResourceVersion(String id, String name, String version, String location, String note, String author, String approver, String type, String contentType, String voltage, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment,boolean archived) { + super(id, name, version); + this.location = location; + this.note = note; + this.author = author; + this.approver = approver; + this.type = type; + this.contentType = contentType; + this.voltage = voltage; + this.modifiedAt = modifiedAt; + this.archivedAt = archivedAt; + this.fields = fields; + this.comment = comment; + this.archived = archived; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public String getNote() { + return note; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public String getApprover() { + return approver; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getVoltage() { + return voltage; + } + + @Override + public OffsetDateTime getModifiedAt() { + return modifiedAt; + } + + @Override + public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + @Override + public List getFields() { + return fields; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public boolean isArchived() { + return archived; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesHistoryMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesHistoryMetaItem.java new file mode 100644 index 00000000..bafd8dfe --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesHistoryMetaItem.java @@ -0,0 +1,21 @@ +package org.lfenergy.compas.scl.data.model; + +import java.util.List; + +public class ArchivedResourcesHistoryMetaItem implements IArchivedResourcesHistoryMetaItem { + + List versions; + + public ArchivedResourcesHistoryMetaItem(List versions) { + this.versions = versions; + } + + @Override + public List getVersions() { + return versions; + } + + public void setVersions(List versions) { + this.versions = versions; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedSclResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedSclResourceMetaItem.java new file mode 100644 index 00000000..5348123c --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedSclResourceMetaItem.java @@ -0,0 +1,45 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public class ArchivedSclResourceMetaItem extends AbstractArchivedResourceMetaItem implements IAbstractArchivedResourceMetaItem { + String id; + String name; + String version; + String note; + String voltage; + + public ArchivedSclResourceMetaItem(String id, String name, String version, String author, String approver, String type, String contentType, String location, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String note, String voltage) { + super(author, approver, type, contentType, location, fields, modifiedAt, archivedAt); + this.id = id; + this.name = name; + this.version = version; + this.note = note; + this.voltage = voltage; + } + + + public String getNote() { + return note; + } + + public String getVoltage() { + return voltage; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getVersion() { + return version; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 7c11db9a..a7328170 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -7,6 +7,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; @@ -14,11 +16,12 @@ import org.lfenergy.compas.scl.extensions.model.SclFileType; import javax.sql.DataSource; -import java.io.File; import java.sql.*; +import java.time.Instant; import java.time.OffsetDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.*; +import java.util.stream.Collectors; import static jakarta.transaction.Transactional.TxType.REQUIRED; import static jakarta.transaction.Transactional.TxType.SUPPORTS; @@ -34,7 +37,6 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String KEY_FIELD = "key"; private static final String VALUE_FIELD = "value"; private static final String LOCATIONMETAITEM_DESCRIPTION_FIELD = "description"; - private static final String LOCATIONMETAITEM_RESOURCE_ID_FIELD = "resource_id"; private static final String SCL_DATA_FIELD = "scl_data"; private static final String HITEM_WHO_FIELD = "hitem_who"; private static final String HITEM_WHEN_FIELD = "hitem_when"; @@ -47,7 +49,6 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String HISTORYMETAITEM_ARCHIVED_FIELD = "archived"; private static final String HISTORYMETAITEM_IS_DELETED_FIELD = "is_deleted"; private static final String ARCHIVEMETAITEM_LOCATION_FIELD = "location"; - private static final String ARCHIVEMETAITEM_NOTE_FIELD = "note"; private static final String ARCHIVEMETAITEM_AUTHOR_FIELD = "author"; private static final String ARCHIVEMETAITEM_APPROVER_FIELD = "approver"; private static final String ARCHIVEMETAITEM_TYPE_FIELD = "type"; @@ -461,78 +462,92 @@ private List createLabelList(Array sqlArray) throws SQLException { return labelsList; } - @Override - @Transactional(REQUIRED) - public void createHistoryVersion(UUID id, String name, Version version, SclFileType type, String author, String comment, OffsetDateTime changedAt, Boolean archived, Boolean available) { - var sql = """ - INSERT INTO scl_history(id, name, major_version, minor_version, patch_version, type, author, comment, changedAt, archived, available) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - """; - - try (var connection = dataSource.getConnection(); - var sclStmt = connection.prepareStatement(sql)) { - sclStmt.setObject(1, id); - sclStmt.setString(2, name); - sclStmt.setInt(3, version.getMajorVersion()); - sclStmt.setInt(4, version.getMinorVersion()); - sclStmt.setInt(5, version.getPatchVersion()); - sclStmt.setString(6, type.toString()); - sclStmt.setString(7, author); - sclStmt.setString(8, comment); - sclStmt.setObject(9, changedAt); - sclStmt.setBoolean(10, archived); - sclStmt.setBoolean(11, available); - sclStmt.executeUpdate(); - } catch (SQLException exp) { - throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding SCL History to database!", exp); - } - } - @Override @Transactional(SUPPORTS) public List listHistory() { - var sql = """ - SELECT * - FROM ( - SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted - FROM scl_history - JOIN scl_file ON scl_history.id = scl_file.id - AND scl_history.major_version = scl_file.major_version - AND scl_history.minor_version = scl_file.minor_version - AND scl_history.patch_version = scl_file.patch_version - ORDER BY - scl_history.id, - scl_history.major_version DESC, - scl_history.minor_version DESC, - scl_history.patch_version DESC - ) subquery - ORDER BY subquery.name - """; + String sql = """ + SELECT subquery.id + , subquery.major_version + , subquery.minor_version + , subquery.patch_version + , subquery.type + , subquery.name + , subquery.creation_date as changedAt + , subquery.created_by as author + , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived + , true as available + , subquery.is_deleted + , l.name as location + , (XPATH('/scl:Hitem/@what', subquery.header, + ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment + FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + sf.major_version || '.' || sf.minor_version || '.' || + sf.patch_version || '"])[1]' + , sf.scl_data::xml + , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) + as header + FROM scl_file + JOIN scl_file sf + ON scl_file.id = sf.id + AND scl_file.major_version = sf.major_version + AND scl_file.minor_version = sf.minor_version + AND scl_file.patch_version = sf.patch_version + ORDER BY scl_file.id, + scl_file.major_version DESC, + scl_file.minor_version DESC, + scl_file.patch_version DESC) subquery + LEFT JOIN location l + ON location_id = l.id + ORDER BY subquery.name; + """; return executeHistoryQuery(sql, Collections.emptyList()); } @Override @Transactional(SUPPORTS) public List listHistory(UUID id) { - var sql = """ - SELECT * - FROM ( - SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted - FROM scl_history - JOIN scl_file - ON scl_history.id = scl_file.id - AND scl_history.major_version = scl_file.major_version - AND scl_history.minor_version = scl_file.minor_version - AND scl_history.patch_version = scl_file.patch_version - WHERE scl_history.id = ? - ORDER BY - scl_history.id, - scl_history.major_version DESC, - scl_history.minor_version DESC, - scl_history.patch_version DESC - ) subquery - ORDER BY subquery.name - """; + String sql = """ + SELECT subquery.id + , subquery.major_version + , subquery.minor_version + , subquery.patch_version + , subquery.type + , subquery.name + , subquery.creation_date as changedAt + , subquery.created_by as author + , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived + , true as available + , subquery.is_deleted + , l.name as location + , (XPATH('/scl:Hitem/@what', subquery.header, + ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment + FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + sf.major_version || '.' || sf.minor_version || '.' || + sf.patch_version || '"])[1]' + , sf.scl_data::xml + , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) + as header + FROM scl_file + JOIN scl_file sf + ON scl_file.id = sf.id + AND scl_file.major_version = sf.major_version + AND scl_file.minor_version = sf.minor_version + AND scl_file.patch_version = sf.patch_version + WHERE sf.id = ? + ORDER BY scl_file.id, + scl_file.major_version DESC, + scl_file.minor_version DESC, + scl_file.patch_version DESC) subquery + LEFT JOIN location l + ON location_id = l.id + ORDER BY subquery.name; + """; return executeHistoryQuery(sql, Collections.singletonList(id)); } @@ -540,24 +555,43 @@ SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted @Transactional(SUPPORTS) public List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { StringBuilder sqlBuilder = new StringBuilder(""" - SELECT * - FROM ( - SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted - FROM scl_history - JOIN scl_file - ON scl_history.id = scl_file.id - AND scl_history.major_version = scl_file.major_version - AND scl_history.minor_version = scl_file.minor_version - AND scl_history.patch_version = scl_file.patch_version - ORDER BY - scl_history.id, - scl_history.major_version DESC, - scl_history.minor_version DESC, - scl_history.patch_version DESC - ) subquery - ORDER BY subquery.name - WHERE 1=1 - """); + SELECT subquery.id + , subquery.major_version + , subquery.minor_version + , subquery.patch_version + , subquery.type + , subquery.name + , subquery.creation_date as changedAt + , subquery.created_by as author + , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived + , true as available + , subquery.is_deleted + , l.name as location + , (XPATH('/scl:Hitem/@what', subquery.header, + ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment + FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + sf.major_version || '.' || sf.minor_version || '.' || + sf.patch_version || '"])[1]' + , sf.scl_data::xml + , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) + as header + FROM scl_file + JOIN scl_file sf + ON scl_file.id = sf.id + AND scl_file.major_version = sf.major_version + AND scl_file.minor_version = sf.minor_version + AND scl_file.patch_version = sf.patch_version + ORDER BY scl_file.id, + scl_file.major_version DESC, + scl_file.minor_version DESC, + scl_file.patch_version DESC) subquery + LEFT JOIN location l + ON location_id = l.id + WHERE 1=1 + """); List parameters = new ArrayList<>(); @@ -572,28 +606,22 @@ SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted } if (author != null) { - sqlBuilder.append(" AND subquery.author = ?"); + sqlBuilder.append(" AND subquery.created_by = ?"); parameters.add(author); } if (from != null) { - sqlBuilder.append(" AND subquery.changedAt >= ?"); + sqlBuilder.append(" AND subquery.creation_date >= ?"); parameters.add(from); } if (to != null) { - sqlBuilder.append(" AND subquery.changedAt <= ?"); + sqlBuilder.append(" AND subquery.creation_date <= ?"); parameters.add(to); } sqlBuilder.append(System.lineSeparator()); - sqlBuilder.append(""" - ORDER BY - scl_history.name, - scl_history.major_version, - scl_history.minor_version, - scl_history.patch_version - """); + sqlBuilder.append("ORDER BY subquery.name;"); return executeHistoryQuery(sqlBuilder.toString(), parameters); } @@ -602,20 +630,40 @@ SELECT DISTINCT ON (scl_history.id) scl_history.*, scl_file.is_deleted @Override @Transactional(SUPPORTS) public List listHistoryVersionsByUUID(UUID id) { - var sql = """ - SELECT scl_history.*, scl_file.is_deleted - FROM scl_history - JOIN scl_file - ON scl_history.id = scl_file.id - AND scl_history.major_version = scl_file.major_version - AND scl_history.minor_version = scl_file.minor_version - AND scl_history.patch_version = scl_file.patch_version - WHERE scl_history.id = ? + String sql = """ + SELECT sf.id + , sf.major_version + , sf.minor_version + , sf.patch_version + , sf.type + , sf.name + , sf.creation_date as changedAt + , sf.created_by as author + , sf.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived + , true as available + , sf.is_deleted + , l.name as location + , (XPATH('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment + FROM scl_file sf + INNER JOIN ( + SELECT id, major_version, minor_version, patch_version, + UNNEST( + XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' + , scl_data::xml + , ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header + FROM scl_file) scl_data + ON scl_data.id = sf.id + AND scl_data.major_version = sf.major_version + AND scl_data.minor_version = sf.minor_version + AND scl_data.patch_version = sf.patch_version + LEFT JOIN location l + ON sf.location_id = l.id + WHERE sf.id = ? ORDER BY - scl_history.name, - scl_history.major_version, - scl_history.minor_version, - scl_history.patch_version + sf.name, + sf.major_version, + sf.minor_version, + sf.patch_version """; return executeHistoryQuery(sql, Collections.singletonList(id)); } @@ -624,8 +672,8 @@ public List listHistoryVersionsByUUID(UUID id) { @Transactional(REQUIRED) public ILocationMetaItem createLocation(UUID id, String key, String name, String description) { String sql = """ - INSERT INTO location (id, key, name, description, resource_id) - VALUES (?, ?, ?, ?, null); + INSERT INTO location (id, key, name, description) + VALUES (?, ?, ?, ?); """; try (var connection = dataSource.getConnection(); @@ -633,7 +681,7 @@ INSERT INTO location (id, key, name, description, resource_id) sclStmt.setObject(1, id); sclStmt.setString(2, key); sclStmt.setString(3, name); - sclStmt.setString(4, description == null ? "" : description); + sclStmt.setString(4, description); sclStmt.executeUpdate(); } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding Location to database!", exp); @@ -1413,7 +1461,7 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, sb.append(" AND (rr.filename ILIKE ? OR sf.name ILIKE ?)"); } if (approver != null && !approver.isBlank()) { - parameters.add(approver); //ToDo cgutmann add xpath subselect + parameters.add(approver); parameters.add(approver); sb.append(" AND (rr.approver = ? OR xml_data.hitem_who::varchar = ?)"); } @@ -1427,9 +1475,7 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, sb.append(" AND (rr.type = ? OR sf.type = ?)"); } if (voltage != null && !voltage.isBlank()) { - parameters.add(voltage); - //ToDo cgutmann: add subquery for voltage from scl data - sb.append(" AND sf.voltage = ?"); +// ToDo cgutmann: find out how to retrieve voltage } if (from != null) { parameters.add(from); @@ -1539,7 +1585,7 @@ private ArchivedResourceVersion mapToArchivedResourceVersion(ResultSet resultSet resultSet.getString(NAME_FIELD), version.toString(), resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), - resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), //ToDo cgutmann: find out where to get comnment value + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), resultSet.getString(ARCHIVEMETAITEM_AUTHOR_FIELD), resultSet.getString(ARCHIVEMETAITEM_APPROVER_FIELD), resultSet.getString(ARCHIVEMETAITEM_TYPE_FIELD), @@ -1659,6 +1705,7 @@ private HistoryMetaItem mapResultSetToHistoryMetaItem(ResultSet resultSet) throw resultSet.getString(HISTORYMETAITEM_TYPE_FIELD), resultSet.getString(HISTORYMETAITEM_AUTHOR_FIELD), resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), convertToOffsetDateTime(resultSet.getTimestamp(HISTORYMETAITEM_CHANGEDAT_FIELD)), resultSet.getBoolean(HISTORYMETAITEM_ARCHIVED_FIELD), resultSet.getBoolean(HISTORYMETAITEM_AVAILABLE_FIELD), @@ -1668,7 +1715,7 @@ private HistoryMetaItem mapResultSetToHistoryMetaItem(ResultSet resultSet) throw private OffsetDateTime convertToOffsetDateTime(Timestamp sqlTimestamp) { return sqlTimestamp != null - ? sqlTimestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() + ? sqlTimestamp.toInstant().atOffset(ZoneOffset.UTC) : null; } diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__create_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__create_resource_tag.sql new file mode 100644 index 00000000..e42febd2 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_10__create_resource_tag.sql @@ -0,0 +1,15 @@ +-- +-- Creating table to hold Resource Tag Data. The Resource Tag is identified by its ID. +-- + +create table resource_tag ( + id uuid not null, + key varchar(255) not null, + value varchar(255), + primary key (id) +); + +comment on table resource_tag is 'Table holding all the Resource Tag data. The id is unique (pk).'; +comment on column resource_tag.id is 'Unique ID generated according to standards'; +comment on column resource_tag.key is 'The key of the Resource Tag'; +comment on column resource_tag.value is 'The value of the Resource Tag'; \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__create_archived_resource_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__create_archived_resource_resource_tag.sql new file mode 100644 index 00000000..8c5d0e28 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_11__create_archived_resource_resource_tag.sql @@ -0,0 +1,24 @@ +-- +-- Creating many-to-many reference table to hold references to Archived Resource Data and Resource Tag Data. +-- + +create table archived_resource_resource_tag +( + archived_resource_id uuid, + resource_tag_id uuid, + primary key (archived_resource_id, resource_tag_id), + constraint fk_archived_resource + foreign key (archived_resource_id) + REFERENCES archived_resource(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + constraint fk_resource_tag + foreign key (resource_tag_id) + REFERENCES resource_tag(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +comment on table archived_resource_resource_tag is 'Table holding all the references for the many-to-many relation between the archived_resource table and the resource_tag table'; +comment on column archived_resource_resource_tag.archived_resource_id is 'Unique archived_resource ID generated according to standards'; +comment on column archived_resource_resource_tag.resource_tag_id is 'Unique resource_tag ID generated according to standards'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_12__scl_file_add_location_id.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_12__scl_file_add_location_id.sql new file mode 100644 index 00000000..eda6acff --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_12__scl_file_add_location_id.sql @@ -0,0 +1,8 @@ +-- +-- Add 'location_id' column to the scl_file table. +-- +ALTER TABLE scl_file ADD COLUMN location_id uuid DEFAULT NULL; +ALTER TABLE scl_file ADD CONSTRAINT fk_scl_file_location + foreign key (location_id) + REFERENCES location(id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_13__create_location_resource_tag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_13__create_location_resource_tag.sql new file mode 100644 index 00000000..0b3852bf --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_13__create_location_resource_tag.sql @@ -0,0 +1,24 @@ +-- +-- Creating many-to-many reference table to hold references to Location Data and Resource Tag Data. +-- + +create table location_resource_tag +( + location_id uuid not null, + resource_tag_id uuid not null, + primary key (location_id, resource_tag_id), + constraint fk_location + foreign key (location_id) + REFERENCES location(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + constraint fk_resource_tag + foreign key (resource_tag_id) + REFERENCES resource_tag(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +comment on table location_resource_tag is 'Table holding all the references for the many-to-many relation between the location table and the resource_tag table'; +comment on column location_resource_tag.location_id is 'Unique location ID generated according to standards'; +comment on column location_resource_tag.resource_tag_id is 'Unique resource_tag ID generated according to standards'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql deleted file mode 100644 index 302247d7..00000000 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__create_scl_history.sql +++ /dev/null @@ -1,32 +0,0 @@ --- --- Creating table to hold SCL History Data. The SCL is identified by it's ID, but there can be multiple versions. --- --- create table scl_history ( --- id uuid not null, --- name varchar(255) not null, --- major_version smallint not null, --- minor_version smallint not null, --- patch_version smallint not null, --- type varchar(3) not null, --- author varchar(255) not null, --- comment varchar(255) not null, --- changedAt TIMESTAMP WITH TIME ZONE not null, --- archived boolean not null default false, --- available boolean not null default true, --- primary key (id, major_version, minor_version, patch_version) --- ); --- --- create index scl_history_type on scl_history(type); --- --- comment on table scl_history is 'Table holding all the SCL History Data. The combination id and version are unique (pk).'; --- comment on column scl_history.id is 'Unique ID generated according to standards'; --- comment on column scl_history.name is 'The name of the SCL File'; --- comment on column scl_history.major_version is 'Versioning according to Semantic Versioning (Major Position)'; --- comment on column scl_history.minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; --- comment on column scl_history.patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; --- comment on column scl_history.type is 'The type of SCL stored'; --- comment on column scl_history.author is 'The author of the change in this version of the SCL File'; --- comment on column scl_history.comment is 'The comment of the change in this version of the SCL File'; --- comment on column scl_history.changedAt is 'The date of the change in this version of the SCL File'; --- comment on column scl_history.archived is 'Is this version of the SCL File archived'; --- comment on column scl_history.available is 'Is this version of the SCL File available'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_referenced_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_referenced_resource.sql new file mode 100644 index 00000000..f147636f --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_8__create_referenced_resource.sql @@ -0,0 +1,42 @@ +-- +-- Create table to hold Referenced Resource Data. A Referenced Resource is identified by its ID. +-- + +create table referenced_resource +( + id uuid not null, + type varchar(255), + content_type varchar(255) not null, + filename varchar(255) not null, + author varchar(255) not null, + approver varchar(255), + location_id uuid, + scl_file_id uuid not null, + scl_file_major_version smallint not null, + scl_file_minor_version smallint not null, + scl_file_patch_version smallint not null, + primary key (id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version), + constraint fk_scl_file + foreign key (scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + REFERENCES scl_file(id, major_version, minor_version, patch_version) + ON UPDATE CASCADE + ON DELETE CASCADE, + constraint fk_location + foreign key (location_id) + REFERENCES location(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +comment on table referenced_resource is 'Table holding all referenced resources. The id is unique (pk)'; +comment on column referenced_resource.id is 'Unique referenced resource ID generated according to standards'; +comment on column referenced_resource.content_type is 'The type of content stored'; +comment on column referenced_resource.type is 'The type of content stored'; +comment on column referenced_resource.filename is 'The name of the uploaded file'; +comment on column referenced_resource.author is 'The name of the author of the file'; +comment on column referenced_resource.approver is 'The name of the approver of the file'; +comment on column referenced_resource.location_id is 'Unique location ID generated according to standards'; +comment on column referenced_resource.scl_file_id is 'Unique resource_tag ID generated according to standards'; +comment on column referenced_resource.scl_file_major_version is 'Versioning according to Semantic Versioning (Major Position)'; +comment on column referenced_resource.scl_file_minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; +comment on column referenced_resource.scl_file_patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_archived_resource.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_archived_resource.sql new file mode 100644 index 00000000..bb8d3d06 --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_9__create_archived_resource.sql @@ -0,0 +1,38 @@ +-- +-- Creating table to hold Archived Resource Data. An Archived Resource is identified by its ID. +-- + +create table archived_resource ( + id uuid not null, + scl_file_id uuid, + archived_at TIMESTAMP WITH TIME ZONE not null, + scl_file_major_version smallint, + scl_file_minor_version smallint, + scl_file_patch_version smallint, + referenced_resource_id uuid, + referenced_resource_major_version smallint, + referenced_resource_minor_version smallint, + referenced_resource_patch_version smallint, + primary key (id), + constraint fk_scl_file + foreign key (scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + references scl_file(id, major_version, minor_version, patch_version) + on update cascade + on delete cascade, + constraint fk_referenced_resource + foreign key (referenced_resource_id, referenced_resource_major_version, referenced_resource_minor_version, referenced_resource_patch_version) + references referenced_resource(id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + on update cascade + on delete cascade +); + +comment on table archived_resource is 'Table holding all the Archived Resource Data and its location assignments. The id is unique (pk).'; +comment on column archived_resource.id is 'Unique ID generated according to standards'; +comment on column archived_resource.scl_file_id is 'Unique ID generated according to standards'; +comment on column archived_resource.scl_file_major_version is 'Versioning according to Semantic Versioning (Major Position)'; +comment on column archived_resource.scl_file_minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; +comment on column archived_resource.scl_file_patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; +comment on column archived_resource.referenced_resource_id is 'Unique ID generated according to standards'; +comment on column archived_resource.referenced_resource_major_version is 'Versioning according to Semantic Versioning (Major Position)'; +comment on column archived_resource.referenced_resource_minor_version is 'Versioning according to Semantic Versioning (Minor Position)'; +comment on column archived_resource.referenced_resource_patch_version is 'Versioning according to Semantic Versioning (Patch Position)'; diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java index afc59262..bae3fa2c 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java @@ -18,6 +18,7 @@ public class CompasSclDataServiceErrorCode { public static final String TOO_MANY_LABEL_ERROR_CODE = "SDS-0009"; public static final String LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE = "SDS-0010"; public static final String INVALID_SCL_CONTENT_TYPE_ERROR_CODE = "SDS-0011"; + public static final String NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE = "SDS-0012"; public static final String POSTGRES_SELECT_ERROR_CODE = "SDS-2000"; public static final String POSTGRES_INSERT_ERROR_CODE = "SDS-2001"; diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java new file mode 100644 index 00000000..6fd2fa15 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java @@ -0,0 +1,31 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface IAbstractArchivedResourceMetaItem extends IAbstractItem { + + String getAuthor(); + + String getApprover(); + + String getType(); + + String getContentType(); + + String getLocation(); + + default String getNote() { + return null; + } + + default String getVoltage() { + return null; + } + + List getFields(); + + OffsetDateTime getModifiedAt(); + + OffsetDateTime getArchivedAt(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceVersion.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceVersion.java new file mode 100644 index 00000000..9dfb97fd --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourceVersion.java @@ -0,0 +1,30 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface IArchivedResourceVersion extends IAbstractItem { + String getLocation(); + + String getNote(); + + String getAuthor(); + + String getApprover(); + + String getType(); + + String getContentType(); + + String getVoltage(); + + OffsetDateTime getModifiedAt(); + + OffsetDateTime getArchivedAt(); + + List getFields(); + + String getComment(); + + boolean isArchived(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesHistoryMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesHistoryMetaItem.java new file mode 100644 index 00000000..ae57419f --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesHistoryMetaItem.java @@ -0,0 +1,7 @@ +package org.lfenergy.compas.scl.data.model; + +import java.util.List; + +public interface IArchivedResourcesHistoryMetaItem { + List getVersions(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index c1938f76..825a0dbc 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -6,7 +6,6 @@ import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.extensions.model.SclFileType; -import java.io.File; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -159,8 +158,6 @@ public interface CompasSclDataRepository { */ List listHistoryVersionsByUUID(UUID id); - void createHistoryVersion(UUID id, String name, Version version, SclFileType type, String author, String comment, OffsetDateTime changedAt, Boolean archived, Boolean available); - /** * Create a new Location * @@ -201,17 +198,18 @@ public interface CompasSclDataRepository { void deleteLocation(UUID locationId); /** - * Updates an existing location + * Update an existing location * * @param locationId The uuid of the existing Location * @param key The key of the updated Location * @param name The name of the updated Location * @param description The description of the updated Location + * @return The Meta Info of the updated Location */ ILocationMetaItem updateLocation(UUID locationId, String key, String name, String description); /** - * Assigns a resource to the specified location, if a resource is already assigned to a location, the previous assignment is removed + * Assign a resource to the specified location, if a resource is already assigned to a location, the previous assignment is removed * * @param locationId The uuid of the Location * @param resourceId The uuid of the Resource @@ -219,21 +217,65 @@ public interface CompasSclDataRepository { void assignResourceToLocation(UUID locationId, UUID resourceId); /** - * Removes the resource assignment from the specified location + * Remove the resource assignment from the specified location * * @param locationId The uuid of the Location * @param resourceId The uuid of the Resource */ void unassignResourceFromLocation(UUID locationId, UUID resourceId); + /** + * Archive a resource and link it to the corresponding scl_file entry + * + * @param id The id of the scl_file + * @param version The version of the scl_file + * @param author The author of the resource + * @param approver The approver of the resource + * @param contentType The content type of the resource + * @param filename The filename of the resource + * @return The created archived resource item + */ IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version version, String author, String approver, String contentType, String filename); + /** + * Archive an existing scl resource + * + * @param id The id of the resource to be archived + * @param version The version of the resource to be archived + * @param approver The approver of the archiving action + * @return The archived resource item + */ IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver); + /** + * Retrieve all entries according to an archived resource id + * + * @param id The id of the archived resource + * @return All archived entries of the given id + */ IArchivedResourcesMetaItem searchArchivedResource(UUID id); + /** + * Retrieve all archived entries according to the search parameters + * + * @param location The location of the archived resource + * @param name The name of the archived resource + * @param approver The approver of the archived resource + * @param contentType The content type of the resource + * @param type The type of the resource + * @param voltage The voltage of the resource + * @param from The start timestamp of archiving (including) + * @param to The end timestamp of archiving (including) + * @return All archived entries matching the search criteria + */ IArchivedResourcesMetaItem searchArchivedResource(String location, String name, String approver, String contentType, String type, String voltage, OffsetDateTime from, OffsetDateTime to); + /** + * Retrieve all archived resource history versions according to an archived resource id + * + * @param uuid The id of the archived resource + * @return All archived versions of the given id + */ IArchivedResourcesHistoryMetaItem searchArchivedResourceHistory(UUID uuid); } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 6c3ec3c5..81131888 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -182,7 +182,7 @@ public String create(SclFileType type, String name, String who, String comment, repository.create(type, id, name, newSclData, version, who, labels); if (isHistoryEnabled) { - repository.createHistoryVersion(id, name, version, type, who, what, OffsetDateTime.parse(when), false, true); + //ToDo cgutmann: check what has to be done here when history table is not available anymore } return newSclData; @@ -260,7 +260,7 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str repository.create(type, id, newSclName, newSclData, version, who, labels); if (isHistoryEnabled) { - repository.createHistoryVersion(id, newSclName, version, type, who, what, OffsetDateTime.parse(when), false, true); + //ToDo cgutmann: check what needs to be done when history table is not available anymore } return newSclData; @@ -567,7 +567,7 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { } /** - * Archive a resource and links it to the corresponding scl_file + * Archive a resource and link it to the corresponding scl_file entry * * @param id The id of the scl_file * @param version The version of the scl_file From 09a7f9b7a577b399be91b0bf36e8c599a24ec9e5 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 17 Dec 2024 11:49:56 +0100 Subject: [PATCH 08/23] fix: Fix listHistoryVersionsByUUID query --- .../CompasSclDataPostgreSQLRepository.java | 73 ++++++++++--------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index a7328170..47c6bbd7 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -631,40 +631,45 @@ public List listHistory(SclFileType type, String name, String @Transactional(SUPPORTS) public List listHistoryVersionsByUUID(UUID id) { String sql = """ - SELECT sf.id - , sf.major_version - , sf.minor_version - , sf.patch_version - , sf.type - , sf.name - , sf.creation_date as changedAt - , sf.created_by as author - , sf.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived - , true as available - , sf.is_deleted - , l.name as location - , (XPATH('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment - FROM scl_file sf - INNER JOIN ( - SELECT id, major_version, minor_version, patch_version, - UNNEST( - XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' - , scl_data::xml - , ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header - FROM scl_file) scl_data - ON scl_data.id = sf.id - AND scl_data.major_version = sf.major_version - AND scl_data.minor_version = sf.minor_version - AND scl_data.patch_version = sf.patch_version - LEFT JOIN location l - ON sf.location_id = l.id - WHERE sf.id = ? - ORDER BY - sf.name, - sf.major_version, - sf.minor_version, - sf.patch_version - """; + SELECT sf.id + , sf.major_version + , sf.minor_version + , sf.patch_version + , sf.type + , sf.name + , sf.creation_date as changedAt + , sf.created_by as author + , sf.id IN (SELECT ar.scl_file_id + FROM archived_resource ar + WHERE ar.scl_file_id = sf.id + AND ar.scl_file_major_version = sf.major_version + AND ar.scl_file_minor_version = sf.minor_version + AND ar.scl_file_patch_version = sf.patch_version) as archived + , true as available + , sf.is_deleted + , l.name as location + , (XPATH('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment + FROM scl_file sf + INNER JOIN ( + SELECT id, major_version, minor_version, patch_version, + UNNEST( + XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || major_version || '.' || minor_version || '.' || patch_version || '"])[1]' + , scl_data::xml + , ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header + FROM scl_file) scl_data + ON scl_data.id = sf.id + AND scl_data.major_version = sf.major_version + AND scl_data.minor_version = sf.minor_version + AND scl_data.patch_version = sf.patch_version + LEFT JOIN location l + ON sf.location_id = l.id + WHERE sf.id = ? + ORDER BY + sf.name, + sf.major_version, + sf.minor_version, + sf.patch_version; + """; return executeHistoryQuery(sql, Collections.singletonList(id)); } From d242091049e9a86a1096c9dc6726598fa8bb08a5 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 17 Dec 2024 11:52:32 +0100 Subject: [PATCH 09/23] test: Add test case for archiving api retrieveArchivedResourceHistory endpoint --- .../rest/api/archive/ArchiveResourceTest.java | 85 +++++++++++++------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java index 6e72f238..8d12687e 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java @@ -8,19 +8,21 @@ import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.Test; -import org.lfenergy.compas.scl.data.model.ArchivedResourcesMetaItem; -import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; -import org.lfenergy.compas.scl.data.model.IArchivedResourcesMetaItem; -import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResource; import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResources; +import org.lfenergy.compas.scl.data.rest.api.archive.model.ArchivedResourcesHistory; import org.lfenergy.compas.scl.data.service.CompasSclDataService; +import java.io.File; +import java.nio.file.Paths; import java.util.List; import java.util.UUID; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @QuarkusTest @@ -55,29 +57,28 @@ void archiveSclResource_WhenCalled_ThenReturnsArchivedResource() { assertEquals(version, result.getVersion()); } -// @Test -// void archiveResource_WhenCalled_ThenReturnsArchivedResource() { -// UUID uuid = UUID.randomUUID(); -// String name = "Name"; -// String version = "1.0.0"; -// IAbstractArchivedResourceMetaItem testData = new ArchivedReferencedResourceTestDataBuilder().setId(uuid.toString()).build(); -// File f = Paths.get("src","test","resources","scl", "icd_import_ied_test.scd").toFile(); -// //ToDo cgutmann: fix this test -> file not the same as in test function -> therefore request not mocked -// when(compasSclDataService.archiveResource(uuid, version, null, null, "application/json", null, f)).thenReturn(testData); -// Response response = given() -// .contentType(MediaType.APPLICATION_JSON) -// .body(f) -// .when().post("/referenced-resource/" + uuid + "/versions/" + version) -// .then() -// .statusCode(200) -// .extract() -// .response(); -// -// ArchivedResource result = response.as(ArchivedResource.class); -// assertEquals(uuid, UUID.fromString(result.getUuid())); -// assertEquals(name, result.getName()); -// assertEquals(version, result.getVersion()); -// } + @Test + void archiveResource_WhenCalled_ThenReturnsArchivedResource() { + UUID uuid = UUID.randomUUID(); + String name = "Name"; + String version = "1.0.0"; + IAbstractArchivedResourceMetaItem testData = new ArchivedReferencedResourceTestDataBuilder().setId(uuid.toString()).build(); + File f = Paths.get("src","test","resources","scl", "icd_import_ied_test.scd").toFile(); + when(compasSclDataService.archiveResource(eq(uuid), eq(version), eq(null), eq(null), eq("application/json"), eq(null), any(File.class))).thenReturn(testData); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .body(f) + .when().post("/referenced-resource/" + uuid + "/versions/" + version) + .then() + .statusCode(200) + .extract() + .response(); + + ArchivedResource result = response.as(ArchivedResource.class); + assertEquals(uuid, UUID.fromString(result.getUuid())); + assertEquals(name, result.getName()); + assertEquals(version, result.getVersion()); + } @Test void searchArchivedResources_WhenCalledWithUuid_ThenReturnsMatchingArchivedResources() { @@ -133,4 +134,34 @@ void searchArchivedResources_WhenCalledWithoutUuid_ThenReturnsMatchingArchivedRe assertEquals(version, result.getResources().get(1).getVersion()); } + @Test + void retrieveArchivedResourceHistory_WhenCalledWithUuid_ThenReturnsMatchingArchivedResources() { + UUID resourceUuid = UUID.randomUUID(); + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + String name = "Name"; + String version1 = "1.0.0"; + String version2 = "1.0.1"; + IArchivedResourceVersion testData1 = new ArchivedResourceVersionTestDataBuilder().setId(uuid1.toString()).setName(name).setVersion(version1).build(); + IArchivedResourceVersion testData2 = new ArchivedResourceVersionTestDataBuilder().setId(uuid2.toString()).setName(name).setVersion(version2).build(); + IArchivedResourcesHistoryMetaItem archivedResources = new ArchivedResourcesHistoryMetaItem(List.of(testData1, testData2)); + + when(compasSclDataService.getArchivedResourceHistory(resourceUuid)).thenReturn(archivedResources); + Response response = given() + .contentType(MediaType.APPLICATION_JSON) + .when().get("/resources/"+ resourceUuid +"/versions") + .then() + .statusCode(200) + .extract() + .response(); + + ArchivedResourcesHistory result = response.as(ArchivedResourcesHistory.class); + assertEquals(uuid1, UUID.fromString(result.getVersions().get(0).getUuid())); + assertEquals(name, result.getVersions().get(0).getName()); + assertEquals(version1, result.getVersions().get(0).getVersion()); + assertEquals(uuid2, UUID.fromString(result.getVersions().get(1).getUuid())); + assertEquals(name, result.getVersions().get(1).getName()); + assertEquals(version2, result.getVersions().get(1).getVersion()); + } + } \ No newline at end of file From 1a29c25263bd2ca6fdd1262a59516aaf32f41593 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 17 Dec 2024 13:50:48 +0100 Subject: [PATCH 10/23] test: Add missing ArchivedResourceVersionTestDataBuilder file --- ...rchivedResourceVersionTestDataBuilder.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedResourceVersionTestDataBuilder.java diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedResourceVersionTestDataBuilder.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedResourceVersionTestDataBuilder.java new file mode 100644 index 00000000..a4db872d --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivedResourceVersionTestDataBuilder.java @@ -0,0 +1,99 @@ +package org.lfenergy.compas.scl.data.rest.api.archive; + +import org.lfenergy.compas.scl.data.model.*; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ArchivedResourceVersionTestDataBuilder { + // Default values + private String id = UUID.randomUUID().toString(); + private String name = "Name"; + private String version = "1.0.0"; + private String location = "some location"; + private String note = "some note"; + private String author = "user1"; + private String approver = "user2"; + private String type = "some type"; + private String contentType = "contentType1"; + private String voltage = "100"; + private OffsetDateTime modifiedAt = null; + private OffsetDateTime archivedAt = Instant.now().atZone(ZoneId.systemDefault()).toOffsetDateTime(); + private List fields = new ArrayList<>(); + + public ArchivedResourceVersionTestDataBuilder() { + } + + public ArchivedResourceVersionTestDataBuilder setId(String id) { + this.id = id; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setName(String name) { + this.name = name; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setVersion(String version) { + this.version = version; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setLocation(String location) { + this.location = location; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setNote(String note) { + this.note = note; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setAuthor(String author) { + this.author = author; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setApprover(String approver) { + this.approver = approver; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setType(String type) { + this.type = type; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setVoltage(String voltage) { + this.voltage = voltage; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setModifiedAt(OffsetDateTime modifiedAt) { + this.modifiedAt = modifiedAt; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + return this; + } + + public ArchivedResourceVersionTestDataBuilder setFields(List fields) { + this.fields = fields; + return this; + } + + public IArchivedResourceVersion build() { + return new ArchivedResourceVersion(id, name, version, location, note, author, approver, type, contentType, voltage, fields, modifiedAt, archivedAt, note, true); + } +} From 15a2d62f87d25f5639b566f4012de8df33d1f930 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 9 Jan 2025 07:43:21 +0100 Subject: [PATCH 11/23] feat: implement automatic location assigning on scl resource update --- .../compas/scl/data/model/AbstractItem.java | 8 +++- .../data/model/ArchivedResourceVersion.java | 4 +- .../compas/scl/data/model/HistoryItem.java | 2 +- .../scl/data/model/HistoryMetaItem.java | 2 +- .../lfenergy/compas/scl/data/model/Item.java | 2 +- .../compas/scl/data/model/SclMetaInfo.java | 4 +- .../CompasSclDataPostgreSQLRepository.java | 46 ++++++------------- .../compas/scl/data/model/IAbstractItem.java | 3 ++ .../data/service/CompasSclDataService.java | 4 ++ 9 files changed, 36 insertions(+), 39 deletions(-) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractItem.java index 2a6d267b..f6799355 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractItem.java @@ -8,10 +8,12 @@ public abstract class AbstractItem implements IAbstractItem { private final String id; private final String name; private final String version; - protected AbstractItem(final String id, final String name, final String version) { + private final String locationId; + protected AbstractItem(final String id, final String name, final String version, final String locationId) { this.id = id; this.name = name; this.version = version; + this.locationId = locationId; } public String getId() { @@ -26,4 +28,8 @@ public String getVersion() { return version; } + public String getLocationId() { + return locationId; + } + } diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java index 97dcc482..11b2c128 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourceVersion.java @@ -18,8 +18,8 @@ public class ArchivedResourceVersion extends AbstractItem implements IArchivedRe String comment; boolean archived; - public ArchivedResourceVersion(String id, String name, String version, String location, String note, String author, String approver, String type, String contentType, String voltage, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment,boolean archived) { - super(id, name, version); + public ArchivedResourceVersion(String id, String name, String version, String location, String note, String author, String approver, String type, String contentType, String voltage, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment, boolean archived) { + super(id, name, version, null); this.location = location; this.note = note; this.author = author; diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryItem.java index 73ea526d..43fb5c38 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryItem.java @@ -9,7 +9,7 @@ public class HistoryItem extends AbstractItem implements IHistoryItem { private final String what; public HistoryItem(final String id, final String name, final String version, final String who, final String when, final String what) { - super(id, name, version); + super(id, name, version, null); this.who = who; this.when = when; this.what = what; diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java index 16eeec49..65265fd5 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java @@ -13,7 +13,7 @@ public class HistoryMetaItem extends AbstractItem implements IHistoryMetaItem { private final boolean deleted; public HistoryMetaItem(String id, String name, String version, String type, String author, String comment, String location,OffsetDateTime changedAt, boolean archived, boolean available, boolean deleted) { - super(id, name, version); + super(id, name, version, null); this.type = type; this.author = author; this.comment = comment; diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/Item.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/Item.java index 97739bf8..d665a467 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/Item.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/Item.java @@ -10,7 +10,7 @@ public class Item extends AbstractItem implements IItem { private final List labels; public Item(final String id, final String name, final String version, final List labels) { - super(id, name, version); + super(id, name, version, null); this.labels = labels; } diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/SclMetaInfo.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/SclMetaInfo.java index ad25e47e..060bf722 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/SclMetaInfo.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/SclMetaInfo.java @@ -5,7 +5,7 @@ public class SclMetaInfo extends AbstractItem { - public SclMetaInfo(final String id, final String name, final String version) { - super(id, name, version); + public SclMetaInfo(final String id, final String name, final String version, final String locationId) { + super(id, name, version, locationId); } } diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 47c6bbd7..7fa3ad28 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -7,8 +7,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; @@ -268,7 +266,7 @@ select distinct on (scl_file.id) scl_file.name @Transactional(SUPPORTS) public IAbstractItem findMetaInfoByUUID(SclFileType type, UUID id) { var sql = """ - select scl_file.id, scl_file.name, scl_file.major_version, scl_file.minor_version, scl_file.patch_version + select scl_file.id, scl_file.name, scl_file.major_version, scl_file.minor_version, scl_file.patch_version, scl_file.location_id from scl_file where scl_file.id = ? and scl_file.type = ? @@ -286,7 +284,8 @@ public IAbstractItem findMetaInfoByUUID(SclFileType type, UUID id) { if (resultSet.next()) { return new SclMetaInfo(resultSet.getString(ID_FIELD), resultSet.getString(NAME_FIELD), - createVersion(resultSet)); + createVersion(resultSet), + resultSet.getString("location_id")); } var message = String.format("No meta info found for type '%s' with ID '%s'", type, id); throw new CompasNoDataFoundException(message); @@ -484,17 +483,12 @@ public List listHistory() { UNNEST( XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || - sf.major_version || '.' || sf.minor_version || '.' || - sf.patch_version || '"])[1]' - , sf.scl_data::xml + scl_file.major_version || '.' || scl_file.minor_version || '.' || + scl_file.patch_version || '"])[1]' + , scl_file.scl_data::xml , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header FROM scl_file - JOIN scl_file sf - ON scl_file.id = sf.id - AND scl_file.major_version = sf.major_version - AND scl_file.minor_version = sf.minor_version - AND scl_file.patch_version = sf.patch_version ORDER BY scl_file.id, scl_file.major_version DESC, scl_file.minor_version DESC, @@ -528,18 +522,13 @@ public List listHistory(UUID id) { UNNEST( XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || - sf.major_version || '.' || sf.minor_version || '.' || - sf.patch_version || '"])[1]' - , sf.scl_data::xml + scl_file.major_version || '.' || scl_file.minor_version || '.' || + scl_file.patch_version || '"])[1]' + , scl_file.scl_data::xml , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header FROM scl_file - JOIN scl_file sf - ON scl_file.id = sf.id - AND scl_file.major_version = sf.major_version - AND scl_file.minor_version = sf.minor_version - AND scl_file.patch_version = sf.patch_version - WHERE sf.id = ? + WHERE scl_file.id = ? ORDER BY scl_file.id, scl_file.major_version DESC, scl_file.minor_version DESC, @@ -573,17 +562,12 @@ public List listHistory(SclFileType type, String name, String UNNEST( XPATH( '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || - sf.major_version || '.' || sf.minor_version || '.' || - sf.patch_version || '"])[1]' - , sf.scl_data::xml + scl_file.major_version || '.' || scl_file.minor_version || '.' || + scl_file.patch_version || '"])[1]' + , scl_file.scl_data::xml , ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']])) as header FROM scl_file - JOIN scl_file sf - ON scl_file.id = sf.id - AND scl_file.major_version = sf.major_version - AND scl_file.minor_version = sf.minor_version - AND scl_file.patch_version = sf.patch_version ORDER BY scl_file.id, scl_file.major_version DESC, scl_file.minor_version DESC, @@ -1547,7 +1531,7 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, ON ar.referenced_resource_id = rr.id LEFT JOIN location l ON sf.location_id = l.id OR rr.location_id = l.id - WHERE ? in (sf.id, rr.id) + WHERE ar.id = ? GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar, xml_data.hitem_what::varchar; """; @@ -1608,7 +1592,7 @@ private List executeHistoryQuery(String sql, List para List items = new ArrayList<>(); try ( Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement(sql); + PreparedStatement stmt = connection.prepareStatement(sql) ) { for (int i = 0; i < parameters.size(); i++) { stmt.setObject(i + 1, parameters.get(i)); diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractItem.java index c07da230..b185d99c 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractItem.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractItem.java @@ -8,4 +8,7 @@ public interface IAbstractItem { String getId(); String getName(); String getVersion(); + default String getLocationId() { + return null; + } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 81131888..45909a83 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -259,6 +259,10 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str var newSclData = converter.convertToString(scl); repository.create(type, id, newSclName, newSclData, version, who, labels); + if (currentSclMetaInfo.getLocationId() != null) { + assignResourceToLocation(UUID.fromString(currentSclMetaInfo.getLocationId()), id); + } + if (isHistoryEnabled) { //ToDo cgutmann: check what needs to be done when history table is not available anymore } From 1224a3a369813f8d469a065c4be8a6f3efc7f939 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 14 Jan 2025 08:14:58 +0100 Subject: [PATCH 12/23] chore: split up archived scl data persistence from CompasSclDataService --- .../CompasSclDataPostgreSQLRepository.java | 3 +- .../db/migration/V1_7__create_location.sql | 4 +- .../repository/CompasSclDataRepository.java | 3 +- service/pom.xml | 5 + .../CompasSclDataArchivingServiceImpl.java | 89 +++ .../data/service/CompasSclDataService.java | 80 +-- .../ICompasSclDataArchivingService.java | 22 + .../service/CompasSclDataServiceTest.java | 548 +++++++++++++++++- 8 files changed, 711 insertions(+), 43 deletions(-) create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 7fa3ad28..2b8979dc 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -659,12 +659,13 @@ INNER JOIN ( @Override @Transactional(REQUIRED) - public ILocationMetaItem createLocation(UUID id, String key, String name, String description) { + public ILocationMetaItem createLocation(String key, String name, String description) { String sql = """ INSERT INTO location (id, key, name, description) VALUES (?, ?, ?, ?); """; + UUID id = UUID.randomUUID(); try (var connection = dataSource.getConnection(); var sclStmt = connection.prepareStatement(sql)) { sclStmt.setObject(1, id); diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql index 78024ebc..dd78d38d 100644 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql @@ -4,8 +4,8 @@ create table location ( id uuid not null, - key varchar(255) not null, - name varchar(255) not null, + key varchar(255) unique not null, + name varchar(255) unique not null, description varchar(255), primary key (id) ); diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index 825a0dbc..f8fb0391 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -161,13 +161,12 @@ public interface CompasSclDataRepository { /** * Create a new Location * - * @param id The uuid of the Location * @param key The key of the Location * @param name The name of the Location * @param description The description of the Location * @return The created Location */ - ILocationMetaItem createLocation(UUID id, String key, String name, String description); + ILocationMetaItem createLocation(String key, String name, String description); void addLocationTags(ILocationMetaItem location, String author); diff --git a/service/pom.xml b/service/pom.xml index af566ef1..9355811e 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -66,6 +66,11 @@ SPDX-License-Identifier: Apache-2.0 openpojo test + + org.lfenergy.compas.scl.data + repository-postgresql + test + diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java new file mode 100644 index 00000000..77ede8c4 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -0,0 +1,89 @@ +package org.lfenergy.compas.scl.data.service; + +import jakarta.enterprise.context.ApplicationScoped; +import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +@ApplicationScoped +public class CompasSclDataArchivingServiceImpl implements ICompasSclDataArchivingService { + + private final String DEFAULT_PATH = System.getProperty("user.dir") + File.separator + "locations"; + + @Override + public void createLocation(ILocationMetaItem location) { + File newLocationDirectory = new File(DEFAULT_PATH + File.separator + location.getName()); + newLocationDirectory.mkdirs(); + } + + @Override + public void deleteLocation(ILocationMetaItem location) { + File directory = new File(DEFAULT_PATH + File.separator + location.getName()); + directory.delete(); + } + + @Override + public void archiveSclData(String filename, File body, IAbstractArchivedResourceMetaItem archivedResource) { + String absolutePath = generateSclDataLocation(archivedResource); + File locationDir = new File(absolutePath); + locationDir.mkdirs(); + File f = new File(absolutePath + File.separator + filename); + try (FileOutputStream fos = new FileOutputStream(f)) { + try (FileInputStream fis = new FileInputStream(body)) { + fos.write(fis.readAllBytes()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void archiveSclData(IAbstractArchivedResourceMetaItem archivedResource, String data) { + String absolutePath = generateSclDataLocation(archivedResource); + File locationDir = new File(absolutePath); + locationDir.mkdirs(); + File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getContentType().toLowerCase()); + try (FileWriter fw = new FileWriter(f)) { + fw.write(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String generateSclDataLocation(IAbstractArchivedResourceMetaItem archivedResource) { + return DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); + } + + @Override + public void deleteSclDataFromLocation(ILocationMetaItem location, List resourceIds) { + File locationDir = new File(DEFAULT_PATH + File.separator + location.getName() + File.separator); + try (Stream paths = Files.walk(locationDir.toPath()).skip(1)) { + paths.sorted(Comparator.reverseOrder()).map(Path::toFile).filter(f -> + resourceIds.stream().anyMatch(id -> f.getAbsolutePath().contains(id)) + ).forEach(File::delete); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void moveArchivedResourcesToLocation(ILocationMetaItem oldLocation, ILocationMetaItem newLocation, List resourceIds) { + resourceIds.forEach(id -> { + Path oldPath = new File(DEFAULT_PATH + File.separator + oldLocation.getName() + File.separator + id).toPath(); + Path newPath = new File(DEFAULT_PATH + File.separator + newLocation.getName() + File.separator + id).toPath(); + try { + Files.move(oldPath, newPath, REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 45909a83..6fb425fd 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -19,7 +19,9 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.time.OffsetDateTime; @@ -42,14 +44,15 @@ public class CompasSclDataService { private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; - private final String DEFAULT_PATH = System.getProperty("user.dir") + File.separator + "locations"; + private final ICompasSclDataArchivingService archivingService; @Inject public CompasSclDataService(CompasSclDataRepository repository, ElementConverter converter, - SclElementProcessor sclElementProcessor) { + SclElementProcessor sclElementProcessor, ICompasSclDataArchivingService archivingService) { this.repository = repository; this.converter = converter; this.sclElementProcessor = sclElementProcessor; + this.archivingService = archivingService; } /** @@ -500,10 +503,9 @@ public List listLocations(int page, int pageSize) { */ @Transactional(REQUIRED) public ILocationMetaItem createLocation(String key, String name, String description, String author) { - ILocationMetaItem createdLocation = repository.createLocation(UUID.randomUUID(), key, name, description); + ILocationMetaItem createdLocation = repository.createLocation(key, name, description); repository.addLocationTags(createdLocation, author); - File newLocationDirectory = new File(DEFAULT_PATH + File.separator + createdLocation.getName()); - newLocationDirectory.mkdirs(); + archivingService.createLocation(createdLocation); return createdLocation; } @@ -523,8 +525,7 @@ public void deleteLocation(UUID id) { repository.deleteLocationTags(locationToDelete); repository.deleteLocation(id); - File directory = new File(DEFAULT_PATH + File.separator + locationToDelete.getName()); - directory.delete(); + archivingService.deleteLocation(locationToDelete); } /** @@ -542,19 +543,47 @@ public ILocationMetaItem updateLocation(UUID id, String key, String name, String } /** - * Assign a Resource id to a Location entry + * Assign a Resource id to a Location entry and move archived content from previous assigned Location * * @param locationId The id of the Location entry * @param resourceId The id of the Resource */ @Transactional(REQUIRED) public void assignResourceToLocation(UUID locationId, UUID resourceId) { - ILocationMetaItem item = repository.findLocationByUUID(locationId); - if (item != null) { + ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); + if (locationItem != null) { + List historyMetaItems = repository.listHistory(resourceId); + ILocationMetaItem previousLocation = null; + if (!historyMetaItems.isEmpty()) { + IAbstractItem sclMetaInfo = repository.findMetaInfoByUUID(SclFileType.valueOf(historyMetaItems.get(0).getType()), resourceId); + if (sclMetaInfo.getLocationId() != null) { + previousLocation = repository.findLocationByUUID(UUID.fromString(sclMetaInfo.getLocationId())); + } + } repository.assignResourceToLocation(locationId, resourceId); + if (previousLocation != null) { + moveArchivedItemsToNewLocation(resourceId, locationItem, previousLocation); + } } } + private void moveArchivedItemsToNewLocation(UUID resourceId, ILocationMetaItem location, ILocationMetaItem previousLocation) { + List archivedResourceIdsToMove = getResourceIdsFromAssignedArchivedResources(resourceId, location); + archivingService.moveArchivedResourcesToLocation(previousLocation, location, archivedResourceIdsToMove); + } + + private List getResourceIdsFromAssignedArchivedResources(UUID resourceId, ILocationMetaItem location) { + return repository.searchArchivedResource(location.getName(), null, null, null, null, null, null, null) + .getResources() + .stream() + .filter(ar -> + ar.getFields() + .stream() + .anyMatch(f -> + f.getKey().equals("SOURCE_RESOURCE_ID") && f.getValue().equals(resourceId.toString()))) + .map(IAbstractItem::getId).toList(); + } + /** * Unassign a Resource from a Location entry * @@ -563,9 +592,10 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { */ @Transactional(REQUIRED) public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { - ILocationMetaItem item = repository.findLocationByUUID(locationId); - if (item != null) { - //ToDo cgutmann: ask what should happen with resources in filesystem - should they be deleted? + ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); + if (locationItem != null) { + List resourceIdsToBeUnassigned = getResourceIdsFromAssignedArchivedResources(resourceId, locationItem); + archivingService.deleteSclDataFromLocation(locationItem, resourceIdsToBeUnassigned); repository.unassignResourceFromLocation(locationId, resourceId); } } @@ -586,17 +616,7 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version, String author, String approver, String contentType, String filename, File body) { IAbstractArchivedResourceMetaItem archivedResource = repository.archiveResource(id, new Version(version), author, approver, contentType, filename); if (body != null) { - String absolutePath = DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); - File locationDir = new File(absolutePath); - locationDir.mkdirs(); - File f = new File(absolutePath + File.separator + filename); - try (FileOutputStream fos = new FileOutputStream(f)) { - try (FileInputStream fis = new FileInputStream(body)) { - fos.write(fis.readAllBytes()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + archivingService.archiveSclData(filename, body, archivedResource); } return archivedResource; } @@ -614,15 +634,7 @@ public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version ver IAbstractArchivedResourceMetaItem archivedResource = repository.archiveSclResource(id, version, approver); String data = repository.findByUUID(id, version); if (data != null) { - String absolutePath = DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); - File locationDir = new File(absolutePath); - locationDir.mkdirs(); - File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getContentType().toLowerCase()); - try (FileWriter fw = new FileWriter(f)) { - fw.write(data); - } catch (IOException e) { - throw new RuntimeException(e); - } + archivingService.archiveSclData(archivedResource, data); } return archivedResource; } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java new file mode 100644 index 00000000..fbc1f51c --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -0,0 +1,22 @@ +package org.lfenergy.compas.scl.data.service; + +import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; + +import java.io.File; +import java.util.List; + +public interface ICompasSclDataArchivingService { + + void createLocation(ILocationMetaItem location); + + void deleteLocation(ILocationMetaItem location); + + void archiveSclData(String filename, File body, IAbstractArchivedResourceMetaItem archivedResource); + + void archiveSclData(IAbstractArchivedResourceMetaItem archivedResource, String data); + + void deleteSclDataFromLocation(ILocationMetaItem location, List resourceIds); + + void moveArchivedResourcesToLocation(ILocationMetaItem oldLocation, ILocationMetaItem newLocation, List resourceIds); +} diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 90205ee0..fa60bec4 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -13,9 +13,7 @@ import org.lfenergy.compas.core.commons.exception.CompasException; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; -import org.lfenergy.compas.scl.data.model.ChangeSetType; -import org.lfenergy.compas.scl.data.model.IHistoryItem; -import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.model.*; import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; import org.lfenergy.compas.scl.data.util.SclElementProcessor; import org.lfenergy.compas.scl.data.xml.SclMetaInfo; @@ -24,7 +22,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.w3c.dom.Element; +import java.io.File; import java.io.IOException; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -43,6 +43,9 @@ class CompasSclDataServiceTest { @Mock private CompasSclDataRepository compasSclDataRepository; + @Mock + private ICompasSclDataArchivingService compasSclDataArchivingService; + private CompasSclDataService compasSclDataService; private final ElementConverter converter = new ElementConverter(); @@ -50,7 +53,7 @@ class CompasSclDataServiceTest { @BeforeEach void beforeEach() { - compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor); + compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor, compasSclDataArchivingService); } @Test @@ -388,6 +391,543 @@ void validateLabel_WhenInvalidLabelPassed_TheReturnFalse() { assertFalse(compasSclDataService.validateLabel(createLabelElement("Label*"))); } + @Test + void listHistory_WhenCalled_ThenReturnHistoryItemWithLatestVersion() { + String itemId = UUID.randomUUID().toString(); + IHistoryMetaItem historyItem = new HistoryMetaItem( + itemId, + "TestItem1", + "1.0.1", + "IID", + "User1", + "Coment after update", + null, + null, + false, + true, + false + ); + when(compasSclDataRepository.listHistory()).thenReturn(List.of(historyItem)); + List items = compasSclDataService.listHistory(); + + verify(compasSclDataRepository).listHistory(); + assertEquals(1, items.size()); + } + + @Test + void listHistory_WhenCalledWithId_ThenReturnHistoryItemWithLatestVersion() { + UUID itemId = UUID.randomUUID(); + IHistoryMetaItem historyItem = new HistoryMetaItem( + itemId.toString(), + "TestItem1", + "1.0.1", + "IID", + "User1", + "Coment after update", + null, + null, + false, + true, + false + ); + when(compasSclDataRepository.listHistory(itemId)).thenReturn(List.of(historyItem)); + List items = compasSclDataService.listHistory(itemId); + + verify(compasSclDataRepository).listHistory(itemId); + assertEquals(1, items.size()); + } + + @Test + void listHistory_WhenCalledWithSearchParameters_ThenReturnHistoryItemWithLatestVersion() { + UUID itemId = UUID.randomUUID(); + IHistoryMetaItem historyItem = new HistoryMetaItem( + itemId.toString(), + "TestItem1", + "1.0.1", + "IID", + "User1", + "Coment after update", + null, + null, + false, + true, + false + ); + SclFileType searchFileType = SclFileType.IID; + when(compasSclDataRepository.listHistory(searchFileType, "TestItem1", "User1", null, null)).thenReturn(List.of(historyItem)); + List items = compasSclDataService.listHistory(searchFileType, "TestItem1", "User1", null, null); + + verify(compasSclDataRepository).listHistory(searchFileType, "TestItem1", "User1", null, null); + assertEquals(1, items.size()); + } + + @Test + void listHistoryVersionsByUUID_WhenCalledWithId_ThenReturnHistoryItems() { + UUID itemId = UUID.randomUUID(); + IHistoryMetaItem historyItem = new HistoryMetaItem( + itemId.toString(), + "TestItem1", + "1.0.0", + "IID", + "User1", + null, + null, + null, + false, + true, + false + ); + IHistoryMetaItem historyItem1 = new HistoryMetaItem( + itemId.toString(), + "TestItem1", + "1.0.1", + "IID", + "User1", + "Coment after update", + null, + OffsetDateTime.now(), + false, + true, + false + ); + when(compasSclDataRepository.listHistoryVersionsByUUID(itemId)).thenReturn(List.of(historyItem, historyItem1)); + List items = compasSclDataService.listHistoryVersionsByUUID(itemId); + + verify(compasSclDataRepository).listHistoryVersionsByUUID(itemId); + assertEquals(2, items.size()); + } + + @Test + void findLocationByUUID_WhenCalledWithId_ThenReturnLocation() { + UUID locationId = UUID.randomUUID(); + ILocationMetaItem expectedLocation = new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + "some description", + 0 + ); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(expectedLocation); + + ILocationMetaItem actualLocation = compasSclDataService.findLocationByUUID(locationId); + + verify(compasSclDataRepository).findLocationByUUID(locationId); + assertEquals(expectedLocation, actualLocation); + } + + @Test + void listLocations_WhenCalledWithPageAndPageSize_ThenReturnLocations() { + ILocationMetaItem location1 = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey1", "locationName1", "", 0); + ILocationMetaItem location2 = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey2", "locationName2", "", 0); + ILocationMetaItem location3 = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey3", "locationName3", "", 0); + + List expectedLocations = List.of(location1, location2, location3); + when(compasSclDataRepository.listLocations(0, 25)).thenReturn(expectedLocations); + + List actualLocations = compasSclDataService.listLocations(0, 25); + + verify(compasSclDataRepository).listLocations(0, 25); + assertEquals(expectedLocations.size(), actualLocations.size()); + } + + @Test + void createLocation_WhenCalled_ThenReturnCreatedLocation() { + ILocationMetaItem expectedLocation = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey", "locationName", null, 0); + when(compasSclDataRepository.createLocation("locationKey", "locationName", null)).thenReturn(expectedLocation); + + ILocationMetaItem actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null, "testUser"); + + verify(compasSclDataArchivingService).createLocation(expectedLocation); + assertEquals(expectedLocation, actualLocation); + } + + @Test + void deleteLocation_WhenCalled_ThenLocationIsDeleted() { + UUID locationId = UUID.randomUUID(); + ILocationMetaItem locationToRemove = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 0); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(locationToRemove); + + compasSclDataService.deleteLocation(locationId); + + verify(compasSclDataRepository).deleteLocationTags(locationToRemove); + verify(compasSclDataRepository).deleteLocation(locationId); + verify(compasSclDataArchivingService).deleteLocation(locationToRemove); + } + + @Test + void deleteLocation_WhenResourceIsAssigned_ThenExceptionIsThrown() { + UUID locationId = UUID.randomUUID(); + ILocationMetaItem locationToRemove = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 1); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(locationToRemove); + + assertThrows(CompasSclDataServiceException.class, () -> compasSclDataService.deleteLocation(locationId)); + + verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); + verify(compasSclDataRepository, times(0)).deleteLocationTags(locationToRemove); + verify(compasSclDataRepository, times(0)).deleteLocation(locationId); + verify(compasSclDataArchivingService, times(0)).deleteLocation(locationToRemove); + } + + @Test + void updateLocation_WhenCalled_ThenLocationIsUpdated() { + UUID locationId = UUID.randomUUID(); + ILocationMetaItem expectedLocation = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", "updatedDescription", 0); + when(compasSclDataRepository.updateLocation(locationId, "locationKey", "locationName", "updatedDescription")).thenReturn(expectedLocation); + + ILocationMetaItem actualLocation = compasSclDataService.updateLocation(locationId, "locationKey", "locationName", "updatedDescription"); + + verify(compasSclDataRepository, times(1)).updateLocation(locationId, "locationKey", "locationName", "updatedDescription"); + assertEquals(expectedLocation, actualLocation); + } + + @Test + void assignResourceToLocation_WhenCalled_ThenLocationIsAssigned() { + UUID locationId = UUID.randomUUID(); + UUID resourceId = UUID.randomUUID(); + ILocationMetaItem locationItem = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 0); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(locationItem); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of()); + + compasSclDataService.assignResourceToLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).assignResourceToLocation(locationId, resourceId); + } + + @Test + void assignResourceToLocation_WhenResourceHasLocationAssignedWithArchivedItems_ThenNewLocationIsAssignedAndArchivedItemsAreMoved() { + UUID oldLocationId = UUID.randomUUID(); + UUID resourceId = UUID.randomUUID(); + UUID newLocationId = UUID.randomUUID(); + UUID archivedResourceId = UUID.randomUUID(); + String sclDataName = "sclDataName"; + ILocationMetaItem oldLocationItem = new LocationMetaItem(oldLocationId.toString(), "oldLocationKey", "oldLocationName", null, 1); + ILocationMetaItem newLocationItem = new LocationMetaItem(newLocationId.toString(), "newLocationKey", "newLocationName", null, 0); + IHistoryMetaItem historyItem = new HistoryMetaItem(resourceId.toString(), sclDataName, "1.0.0", "IID", "someUser", null, oldLocationItem.getName(), null, true, true, false); + AbstractItem sclData = new org.lfenergy.compas.scl.data.model.SclMetaInfo(resourceId.toString(), sclDataName, "1.0.0", oldLocationId.toString()); + IAbstractArchivedResourceMetaItem archivedResourceMetaItem = new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + sclDataName, + "1.0.0", + "someUser", + null, + "IID", + null, + oldLocationItem.getName(), + List.of(new ResourceTagItem(UUID.randomUUID().toString(), "SOURCE_RESOURCE_ID", resourceId.toString())), + null, + OffsetDateTime.now(), + null, + null + ); + IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(archivedResourceMetaItem)); + + when(compasSclDataRepository.findLocationByUUID(newLocationId)).thenReturn(newLocationItem); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyItem)); + when(compasSclDataRepository.findMetaInfoByUUID(SclFileType.IID, resourceId)).thenReturn(sclData); + when(compasSclDataRepository.findLocationByUUID(oldLocationId)).thenReturn(oldLocationItem); + when(compasSclDataRepository.searchArchivedResource("newLocationName", null, null, null, null, null, null, null)) + .thenReturn(archivedResources); + + compasSclDataService.assignResourceToLocation(newLocationId, resourceId); + + verify(compasSclDataRepository, times(1)).assignResourceToLocation(newLocationId, resourceId); + verify(compasSclDataRepository, times(1)).assignResourceToLocation(newLocationId, resourceId); + verify(compasSclDataRepository, times(1)).assignResourceToLocation(newLocationId, resourceId); + } + + @Test + void unassignResourcesFromLocation_WhenCalled_ThenLocationIsUnassigned() { + UUID locationId = UUID.randomUUID(); + UUID resourceId = UUID.randomUUID(); + UUID archivedResourceId = UUID.randomUUID(); + + ILocationMetaItem assignedLocation = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 1); + IAbstractArchivedResourceMetaItem archivedResourceMetaItem = new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + "sclDataName", + "1.0.0", + "someUser", + null, + "IID", + null, + "locationName", + List.of(new ResourceTagItem(UUID.randomUUID().toString(), "SOURCE_RESOURCE_ID", resourceId.toString())), + null, + OffsetDateTime.now(), + null, + null + ); + IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(archivedResourceMetaItem)); + + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(assignedLocation); + when(compasSclDataRepository.searchArchivedResource("locationName", null, null, null, null, null, null, null)).thenReturn(archivedResources); + + compasSclDataService.unassignResourceFromLocation(locationId, resourceId); + + verify(compasSclDataRepository, times(1)).unassignResourceFromLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).searchArchivedResource("locationName", null, null, null, null, null, null, null); + verify(compasSclDataArchivingService, times(1)).deleteSclDataFromLocation(assignedLocation, List.of(archivedResourceId.toString())); + } + + @Test + void unassignResourcesFromLocation_WhenLocationNotFound_ThenExceptionIsThrown() { + UUID locationId = UUID.randomUUID(); + UUID resourceId = UUID.randomUUID(); + + when(compasSclDataRepository.findLocationByUUID(locationId)).thenThrow(new CompasNoDataFoundException(String.format("Unable to find Location with id %s.", locationId))); + + assertThrows(CompasNoDataFoundException.class, () -> compasSclDataService.unassignResourceFromLocation(locationId, resourceId)); + + verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); + verify(compasSclDataRepository, times(0)).unassignResourceFromLocation(locationId, resourceId); + verify(compasSclDataRepository, times(0)).searchArchivedResource(any(), any(), any(), any(), any(), any(), any(), any()); + verify(compasSclDataArchivingService, times(0)).deleteSclDataFromLocation(any(), any()); + } + + @Test + void archiveResource_WhenCalled_ThenResourceIsArchived() { + UUID resourceId = UUID.randomUUID(); + String version = "1.0.0"; + String author = "someUser"; + String approver = "someOtherUser"; + String contentType = "application/pdf"; + String filename = "test.pdf"; + File file = new File("test.pdf"); + try { + file.createNewFile(); + + when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) + .thenReturn( + new ArchivedSclResourceMetaItem( + UUID.randomUUID().toString(), + "someName", + version, + author, + approver, + null, + contentType, + "locationName", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ) + ); + + compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); + + verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); + verify(compasSclDataArchivingService, times(1)).archiveSclData(any(), any(), any()); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + file.delete(); + } + } + + @Test + void archiveResource_WhenFileIsNull_ThenFileIsWritten() { + UUID resourceId = UUID.randomUUID(); + String version = "1.0.0"; + String author = "someUser"; + String approver = "someOtherUser"; + String contentType = "application/pdf"; + String filename = "test.pdf"; + File file = null; + + when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) + .thenReturn( + new ArchivedSclResourceMetaItem( + UUID.randomUUID().toString(), + "someName", + version, + author, + approver, + null, + contentType, + "locationName", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ) + ); + + compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); + + verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); + verify(compasSclDataArchivingService, times(0)).archiveSclData(any(), any(), any()); + } + + @Test + void archiveSclResource_WhenCalled_ThenResourceIsArchived() { + UUID resourceId = UUID.randomUUID(); + String version = "1.0.0"; + String approver = "someOtherUser"; + ArchivedSclResourceMetaItem archivedResource = new ArchivedSclResourceMetaItem( + UUID.randomUUID().toString(), + "someName", + version, + "someAuthor", + approver, + "IID", + null, + "locationName", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ); + String sclData = "someData"; + + when(compasSclDataRepository.archiveSclResource(resourceId, new Version(version), approver)).thenReturn(archivedResource); + when(compasSclDataRepository.findByUUID(resourceId, new Version(version))).thenReturn(sclData); + + compasSclDataService.archiveSclResource(resourceId, new Version(version), approver); + + verify(compasSclDataRepository, times(1)).archiveSclResource(resourceId, new Version(version), approver); + verify(compasSclDataRepository, times(1)).findByUUID(resourceId, new Version(version)); + verify(compasSclDataArchivingService, times(1)).archiveSclData(archivedResource, sclData); + } + + @Test + void getArchivedResourceHistory_whenCalled_ThenResourceHistoryIsReturned() { + UUID archivedResourceId = UUID.randomUUID(); + IArchivedResourceVersion archivedResourceVersion = new ArchivedResourceVersion( + archivedResourceId.toString(), + "archivedResourceName", + "1.0.0", + "someLocation", + "some note", + "someAuthor", + "someApprover", + "someType", + null, + null, + List.of(), + null, + OffsetDateTime.now(), + null, + true + ); + IArchivedResourcesHistoryMetaItem archivedResourceHistory = new ArchivedResourcesHistoryMetaItem(List.of(archivedResourceVersion)); + + when(compasSclDataRepository.searchArchivedResourceHistory(archivedResourceId)).thenReturn(archivedResourceHistory); + + IArchivedResourcesHistoryMetaItem actualMetaItems = compasSclDataService.getArchivedResourceHistory(archivedResourceId); + + assertFalse(actualMetaItems.getVersions().isEmpty()); + verify(compasSclDataRepository, times(1)).searchArchivedResourceHistory(archivedResourceId); + } + + @Test + void searchArchivedResources_whenCalled_ThenResourcesAreReturned() { + UUID archivedResourceId = UUID.randomUUID(); + IAbstractArchivedResourceMetaItem archivedResourceVersion = new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + "archivedResourceName", + "1.0.0", + "someAuthor", + "someApprover", + "someType", + null, + "someLocation", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ); + IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(archivedResourceVersion)); + when(compasSclDataRepository.searchArchivedResource(archivedResourceId)).thenReturn(archivedResources); + + IArchivedResourcesMetaItem actualMetaItems = compasSclDataService.searchArchivedResources(archivedResourceId); + + assertFalse(actualMetaItems.getResources().isEmpty()); + verify(compasSclDataRepository, times(1)).searchArchivedResource(archivedResourceId); + } + + @Test + void searchArchivedResources_whenCalledWithMultipleParameters_ThenResourcesAreReturned() { + UUID archivedResourceId = UUID.randomUUID(); + UUID archivedResourceId1 = UUID.randomUUID(); + IAbstractArchivedResourceMetaItem archivedResourceVersion = new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + "archivedResourceName", + "1.0.0", + "someAuthor", + "someApprover", + "someType", + null, + "someLocation", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ); + IAbstractArchivedResourceMetaItem archivedResourceVersion1 = new ArchivedReferencedResourceMetaItem( + archivedResourceId1.toString(), + "test.pdf", + "1.0.0", + "someAuthor", + "someApprover", + "someType", + "application/pdf", + "someLocation", + List.of(), + null, + OffsetDateTime.now(), + null + ); + IArchivedResourcesMetaItem archivedResources1 = new ArchivedResourcesMetaItem(List.of(archivedResourceVersion, archivedResourceVersion1)); + + when(compasSclDataRepository.searchArchivedResource("someLocation", null, "someApprover", null, null, null, null, null)) + .thenReturn(archivedResources1); + + IArchivedResourcesMetaItem actualMetaItem = compasSclDataService.searchArchivedResources("someLocation", null, "someApprover", null, null, null, null, null); + + assertFalse(actualMetaItem.getResources().isEmpty()); + verify(compasSclDataRepository, times(1)).searchArchivedResource("someLocation", null, "someApprover", null, null, null, null, null); + } + + @Test + void searchArchivedResources_whenCalledWithWrongSclFileTypeParameter_ThenThrowsException() { + assertThrows(CompasSclDataServiceException.class, () -> compasSclDataService.searchArchivedResources("someLocation", null, "someApprover", "asdf", null, null, null, null)); + verify(compasSclDataRepository, times(0)).searchArchivedResource("someLocation", null, "someApprover", "asdf", null, null, null, null); + } + + @Test + void searchArchivedResources_whenCalledValidSclFileTypeParameter_ThenResourcesAreReturned() { + UUID archivedResourceId = UUID.randomUUID(); + IAbstractArchivedResourceMetaItem archivedResourceVersion = new ArchivedSclResourceMetaItem( + archivedResourceId.toString(), + "archivedResourceName", + "1.0.0", + "someAuthor", + "someApprover", + "IID", + null, + "someLocation", + List.of(), + null, + OffsetDateTime.now(), + null, + null + ); + IArchivedResourcesMetaItem archivedResources1 = new ArchivedResourcesMetaItem(List.of(archivedResourceVersion)); + + when(compasSclDataRepository.searchArchivedResource("someLocation", null, "someApprover", "IID", null, null, null, null)) + .thenReturn(archivedResources1); + + IArchivedResourcesMetaItem actualMetaItem = compasSclDataService.searchArchivedResources("someLocation", null, "someApprover", "IID", null, null, null, null); + + assertFalse(actualMetaItem.getResources().isEmpty()); + verify(compasSclDataRepository, times(1)).searchArchivedResource("someLocation", null, "someApprover", "IID", null, null, null, null); + } + private Element createLabelElement(String validLabel) { var element = mock(Element.class); when(element.getTextContent()).thenReturn(validLabel); From 143119e2fc4ae44bd912d8cc9f3feb305dea99af Mon Sep 17 00:00:00 2001 From: cgutmann Date: Fri, 17 Jan 2025 08:50:55 +0100 Subject: [PATCH 13/23] fix: fix unassigning resources from location minor changes: - don't delete archived resources when unassigning them from a location (in FS) - don't delete locations in FS - change assigned_resources Location attribute to only count distinct resources - return location id as location attribute in history resource search - change file structure in FS to use scl_resource id as folder name when archiving resource - fix unit tests --- .../rest/api/archive/ArchiveResource.java | 35 +-- .../rest/api/locations/LocationsResource.java | 16 +- .../AbstractArchivedResourceMetaItem.java | 11 +- .../ArchivedReferencedResourceMetaItem.java | 4 +- .../CompasSclDataPostgreSQLRepository.java | 276 +++++------------- .../db/migration/V1_7__create_location.sql | 4 +- .../IAbstractArchivedResourceMetaItem.java | 2 - .../repository/CompasSclDataRepository.java | 20 ++ .../CompasSclDataArchivingServiceImpl.java | 51 +--- .../data/service/CompasSclDataService.java | 68 ++--- .../ICompasSclDataArchivingService.java | 12 +- .../service/CompasSclDataServiceTest.java | 148 ++++++---- 12 files changed, 253 insertions(+), 394 deletions(-) diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java index 2a59ff0a..3a7276e1 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -14,7 +14,6 @@ import org.lfenergy.compas.scl.data.service.CompasSclDataService; import java.io.File; -import java.rmi.UnexpectedException; import java.time.OffsetDateTime; import java.util.UUID; @@ -35,59 +34,43 @@ public ArchiveResource(CompasSclDataService compasSclDataService, JsonWebToken j @Override public Uni archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { + LOGGER.info("Archiving resource '{}' for scl resource with id '{}' and version '{}'", xFilename, id, version); return Uni.createFrom() .item(() -> compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResource) - .onFailure().transform(e -> { - LOGGER.error("Failed to archive resource {} for scl resource {} and version {}", xFilename, id, version, e); - return new UnexpectedException( - String.format("Error while archiving data resource %s for scl resource %s and version %s", xFilename, id, version), - (Exception) e); - }); + .transform(this::mapToArchivedResource); } @Override public Uni archiveSclResource(UUID id, String version) { + LOGGER.info("Archiving scl resource with id '{}' and version '{}'", id, version); String approver = jsonWebToken.getClaim(userInfoProperties.name()); return Uni.createFrom() .item(() -> compasSclDataService.archiveSclResource(id, new Version(version), approver)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResource) - .onFailure().transform(e -> { - LOGGER.error("Failed to archive SCL resource {} with version {}", id, version, e); - return new UnexpectedException( - String.format("Error while archiving data resource %s with version %s", id, version), - (Exception) e); - }); + .transform(this::mapToArchivedResource); } @Override public Uni retrieveArchivedResourceHistory(UUID id) { + LOGGER.info("Retrieving archived resource history for id '{}'", id); return Uni.createFrom() .item(() -> compasSclDataService.getArchivedResourceHistory(id)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResourcesHistory) - .onFailure().transform(e -> { - LOGGER.error("Failed to retrieve archived resource history for {}", id, e); - return new UnexpectedException(String.format("Error while retrieving data resource history for %s", id), (Exception) e); - }); + .transform(this::mapToArchivedResourcesHistory); } @Override public Uni searchArchivedResources(ArchivedResourcesSearch archivedResourcesSearch) { + LOGGER.info("Retrieving archived resources with filter: {}", archivedResourcesSearch); return Uni.createFrom() .item(() -> getArchivedResourcesMetaItem(archivedResourcesSearch)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() - .transform(this::mapToArchivedResources) - .onFailure().transform(e -> { - LOGGER.error("Failed to find archived resources", e); - return new UnexpectedException("Error while finding archived data resources", (Exception) e); - }); + .transform(this::mapToArchivedResources); } private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResourcesSearch archivedResourcesSearch) { @@ -109,7 +92,7 @@ private IArchivedResourcesMetaItem getArchivedResourcesMetaItem(ArchivedResource private ArchivedResource mapToArchivedResource(IAbstractArchivedResourceMetaItem archivedResource) { return new ArchivedResource() .uuid(archivedResource.getId()) - .location(archivedResource.getLocation()) + .location(archivedResource.getLocationId()) .name(archivedResource.getName()) .note(archivedResource.getNote()) .author(archivedResource.getAuthor()) diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java index 04a5b81a..5d87caaa 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -13,6 +13,7 @@ import org.lfenergy.compas.scl.data.service.CompasSclDataService; import java.util.List; +import java.util.Objects; import java.util.UUID; @RequestScoped @@ -32,12 +33,14 @@ public LocationsResource(CompasSclDataService compasSclDataService, JsonWebToken @Override public Uni assignResourceToLocation(UUID locationId, UUID uuid) { + LOGGER.info("Assigning resource '{}' to location '{}'", uuid, locationId); compasSclDataService.assignResourceToLocation(locationId, uuid); return Uni.createFrom().nullItem(); } @Override public Uni createLocation(Location location) { + LOGGER.info("Creating location '{}'", location.getName()); return Uni.createFrom() .item(() -> compasSclDataService.createLocation( location.getKey(), @@ -52,13 +55,14 @@ public Uni createLocation(Location location) { @Override public Uni deleteLocation(UUID locationId) { + LOGGER.info("Deleting location with ID '{}'", locationId); compasSclDataService.deleteLocation(locationId); return Uni.createFrom().nullItem(); } @Override public Uni getLocation(UUID locationId) { - LOGGER.info("Retrieving location for ID: {}", locationId); + LOGGER.info("Retrieving location for ID '{}'", locationId); return Uni.createFrom() .item(() -> compasSclDataService.findLocationByUUID(locationId)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) @@ -68,12 +72,8 @@ public Uni getLocation(UUID locationId) { @Override public Uni> getLocations(Integer page, Integer pageSize) { - int pageLocation; - if (page != null && page > 1) { - pageLocation = page; - } else { - pageLocation = 0; - } + int pageLocation = Objects.requireNonNullElse(page, 0); + LOGGER.info("Retrieving locations for page '{}' and pageSize '{}'", pageLocation, pageSize); return Uni.createFrom() .item(() -> compasSclDataService.listLocations(pageLocation, pageSize)) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) @@ -83,12 +83,14 @@ public Uni> getLocations(Integer page, Integer pageSize) { @Override public Uni unassignResourceFromLocation(UUID locationId, UUID uuid) { + LOGGER.info("Unassigning resource '{}' from location '{}'", uuid, locationId); compasSclDataService.unassignResourceFromLocation(locationId, uuid); return Uni.createFrom().nullItem(); } @Override public Uni updateLocation(UUID locationId, Location location) { + LOGGER.info("Updating resource '{}'", locationId); return Uni.createFrom(). item(() -> compasSclDataService.updateLocation(locationId, location.getKey(), location.getName(), location.getDescription())) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java index dd78ca33..29b60917 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java @@ -9,17 +9,17 @@ public abstract class AbstractArchivedResourceMetaItem implements IAbstractArchi private final String approver; private final String type; private final String contentType; - private final String location; + private final String locationId; private final List fields; private final OffsetDateTime modifiedAt; private final OffsetDateTime archivedAt; - public AbstractArchivedResourceMetaItem(String author, String approver, String type, String contentType, String location, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt) { + public AbstractArchivedResourceMetaItem(String author, String approver, String type, String contentType, String locationId, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt) { this.author = author; this.approver = approver; this.type = type; this.contentType = contentType; - this.location = location; + this.locationId = locationId; this.fields = fields; this.modifiedAt = modifiedAt; this.archivedAt = archivedAt; @@ -41,8 +41,9 @@ public String getContentType() { return contentType; } - public String getLocation() { - return location; + @Override + public String getLocationId() { + return locationId; } public List getFields() { diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java index 0fbcc977..a9153c10 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java @@ -10,8 +10,8 @@ public class ArchivedReferencedResourceMetaItem extends AbstractArchivedResource String version; String comment; - public ArchivedReferencedResourceMetaItem(String id, String name, String version, String author, String approver, String type, String contentType, String location, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment) { - super(author, approver, type, contentType, location, fields, modifiedAt, archivedAt); + public ArchivedReferencedResourceMetaItem(String id, String name, String version, String author, String approver, String type, String contentType, String locationId, List fields, OffsetDateTime modifiedAt, OffsetDateTime archivedAt, String comment) { + super(author, approver, type, contentType, locationId, fields, modifiedAt, archivedAt); this.id = id; this.name = name; this.version = version; diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 2b8979dc..284dbeb9 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -19,7 +19,6 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.*; -import java.util.stream.Collectors; import static jakarta.transaction.Transactional.TxType.REQUIRED; import static jakarta.transaction.Transactional.TxType.SUPPORTS; @@ -476,7 +475,7 @@ public List listHistory() { , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived , true as available , subquery.is_deleted - , l.name as location + , l.id as location , (XPATH('/scl:Hitem/@what', subquery.header, ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, @@ -515,7 +514,7 @@ public List listHistory(UUID id) { , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived , true as available , subquery.is_deleted - , l.name as location + , l.id as location , (XPATH('/scl:Hitem/@what', subquery.header, ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, @@ -555,7 +554,7 @@ public List listHistory(SclFileType type, String name, String , subquery.id IN (SELECT ar.scl_file_id FROM archived_resource ar) as archived , true as available , subquery.is_deleted - , l.name as location + , l.id as location , (XPATH('/scl:Hitem/@what', subquery.header, ARRAY [ARRAY ['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment FROM (SELECT DISTINCT ON (scl_file.id) scl_file.*, @@ -631,7 +630,7 @@ public List listHistoryVersionsByUUID(UUID id) { AND ar.scl_file_patch_version = sf.patch_version) as archived , true as available , sf.is_deleted - , l.name as location + , l.id as location , (XPATH('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1]::varchar as comment FROM scl_file sf INNER JOIN ( @@ -680,6 +679,29 @@ INSERT INTO location (id, key, name, description) return findLocationByUUID(id); } + @Transactional(SUPPORTS) + @Override + public boolean hasDuplicateLocationValues(String key, String name) { + String query = """ + SELECT ? IN (SELECT key from location) as duplicate_key, + ? IN (SELECT name from location) as duplicate_name; + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(query)) { + sclStmt.setString(1, key); + sclStmt.setString(2, name); + try (ResultSet resultSet = sclStmt.executeQuery()) { + if (resultSet.next()) { + return resultSet.getBoolean("duplicate_key") || + resultSet.getBoolean("duplicate_name"); + } + return false; + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error retrieving location values!", exp); + } + } + @Override public void addLocationTags(ILocationMetaItem location, String author) { String locationName = location.getName(); @@ -744,7 +766,7 @@ public void deleteLocationTags(ILocationMetaItem location) { @Transactional(SUPPORTS) public List listLocations(int page, int pageSize) { String sql = """ - SELECT *, (SELECT COUNT(sf.id) FROM scl_file sf WHERE sf.location_id = l.id) as assigned_resources + SELECT *, (SELECT COUNT(DISTINCT(sf.id)) FROM scl_file sf WHERE sf.location_id = l.id) as assigned_resources FROM location l ORDER BY name OFFSET ? @@ -761,14 +783,14 @@ public List listLocations(int page, int pageSize) { @Transactional(SUPPORTS) public ILocationMetaItem findLocationByUUID(UUID locationId) { String sql = """ - SELECT *, (SELECT COUNT(sf.id) FROM scl_file sf WHERE sf.location_id = ?) as assigned_resources + SELECT *, (SELECT COUNT(DISTINCT(sf.id)) FROM scl_file sf WHERE sf.location_id = ?) as assigned_resources FROM location l WHERE id = ? ORDER BY l.name; """; List retrievedLocation = executeLocationQuery(sql, List.of(locationId, locationId)); if (retrievedLocation.isEmpty()) { - throw new CompasNoDataFoundException(String.format("Unable to find Location with id %s.", locationId)); + throw new CompasNoDataFoundException(String.format("Unable to find Location with id '%s'.", locationId)); } return retrievedLocation.get(0); } @@ -849,9 +871,6 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error assigning Referenced Resource to Location in database!", exp); } - String locationKey = findLocationByUUID(locationId).getName(); - updateLocationKeyMappingForSclResource(resourceId, locationKey); - updateLocationKeyMappingForReferencedResource(resourceId, locationKey); } @Override @@ -883,8 +902,6 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error unassigning Referenced Resource from Location in database!", exp); } - updateLocationKeyMappingForSclResource(resourceId, null); - updateLocationKeyMappingForReferencedResource(resourceId, null); } private void updateTagMappingForLocation(UUID locationId, List resourceTags) { @@ -912,107 +929,35 @@ INSERT INTO location_resource_tag(location_id, resource_tag_id) } } - private void updateLocationKeyMappingForSclResource(UUID resourceId, String locationKey) { - ResourceTagItem locationTag = getResourceTag("LOCATION", locationKey); - if (locationTag == null) { - createResourceTag("LOCATION", locationKey); - locationTag = getResourceTag("LOCATION", locationKey); - } - for (IAbstractArchivedResourceMetaItem resource : searchArchivedResourceBySclFile(resourceId)) { - updateLocationResourceTag(locationTag, resource); - } - } - - private void updateLocationKeyMappingForReferencedResource(UUID resourceId, String locationKey) { - ResourceTagItem locationTag = getResourceTag("LOCATION", locationKey); - if (locationTag == null) { - createResourceTag("LOCATION", locationKey); - locationTag = getResourceTag("LOCATION", locationKey); - } - for (IAbstractArchivedResourceMetaItem resource : searchArchivedReferencedResources(resourceId)) { - updateLocationResourceTag(locationTag, resource); - } - } - - private void updateLocationResourceTag(ResourceTagItem locationTag, IAbstractArchivedResourceMetaItem resource) { - List locationFieldIds = resource.getFields() - .stream() - .filter(field -> - field.getKey().equals("LOCATION") - ) - .map(IResourceTagItem::getId) - .toList(); - if (!locationFieldIds.isEmpty()) { - removeLocationTagsFromResource(resource.getId(), locationFieldIds); - } - updateArchivedResourceToResourceTagMappingTable( - UUID.fromString(resource.getId()), - List.of(locationTag) - ); - } - - private void removeLocationTagsFromResource(String resourceId, List locationFieldIds) { - String sql = String.format(""" - DELETE FROM archived_resource_resource_tag - WHERE archived_resource_id = ? AND resource_tag_id IN (%s); - """, locationFieldIds.stream() - .map(fieldIds -> "?") - .collect(Collectors.joining(","))); - try (Connection connection = dataSource.getConnection(); - PreparedStatement deleteStatement = connection.prepareStatement(sql)) { - deleteStatement.setObject(1, UUID.fromString(resourceId)); - for (int i = 1; i <= locationFieldIds.size(); i++) { - deleteStatement.setObject(i + 1, UUID.fromString(locationFieldIds.get(i - 1))); - } - deleteStatement.executeUpdate(); - } catch (SQLException exp) { - throw new CompasSclDataServiceException(POSTGRES_DELETE_ERROR_CODE, "Error deleting archived resource to resource tag mapping entries in database!", exp); - } - } - @Override public IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version version, String author, String approver, String contentType, String filename) { ArchivedSclResourceMetaItem sclResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); - String location = sclResourceMetaItem.getLocation(); - - String locationIdQuery = """ - SELECT l.*, (SELECT COUNT(DISTINCT(sf.id)) FROM scl_file sf WHERE sf.location_id = l.id) as assigned_resources - FROM scl_file sf INNER JOIN location l ON sf.location_id = l.id - WHERE sf.id = ? - AND sf.major_version = ? - AND sf.minor_version = ? - AND sf.patch_version = ?; - """; - - List locationItems = executeLocationQuery(locationIdQuery, List.of(id, version.getMajorVersion(), version.getMinorVersion(), version.getPatchVersion())); - + String locationId = sclResourceMetaItem.getLocationId(); UUID assignedResourceId = UUID.randomUUID(); - if (!locationItems.isEmpty() && locationItems.get(0).getId() != null) { - String locationId = locationItems.get(0).getId(); - String sql = """ + String sql = """ INSERT INTO referenced_resource (id, content_type, filename, author, approver, location_id, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """; - try (Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement(sql)) { - stmt.setObject(1, assignedResourceId); - stmt.setObject(2, contentType); - stmt.setObject(3, filename); - stmt.setObject(4, author); - stmt.setObject(5, approver); - stmt.setObject(6, UUID.fromString(locationId)); - stmt.setObject(7, id); - stmt.setObject(8, version.getMajorVersion()); - stmt.setObject(9, version.getMinorVersion()); - stmt.setObject(10, version.getPatchVersion()); - stmt.executeUpdate(); - } catch (SQLException exp) { - throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error inserting Referenced Resources!", exp); - } + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, assignedResourceId); + stmt.setObject(2, contentType); + stmt.setObject(3, filename); + stmt.setObject(4, author); + stmt.setObject(5, approver); + stmt.setObject(6, UUID.fromString(locationId)); + stmt.setObject(7, id); + stmt.setObject(8, version.getMajorVersion()); + stmt.setObject(9, version.getMinorVersion()); + stmt.setObject(10, version.getPatchVersion()); + stmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error inserting Referenced Resources!", exp); } - List resourceTags = generateFields(location, id.toString(), author, approver); + String locationName = findLocationByUUID(UUID.fromString(locationId)).getName(); + List resourceTags = generateFields(locationName, id.toString(), author, approver); ArchivedReferencedResourceMetaItem archivedResourcesMetaItem = new ArchivedReferencedResourceMetaItem( assignedResourceId.toString(), filename, @@ -1021,7 +966,7 @@ INSERT INTO referenced_resource (id, content_type, filename, author, approver, l approver, null, contentType, - location, + locationId, resourceTags, null, convertToOffsetDateTime(Timestamp.from(Instant.now())), @@ -1041,7 +986,7 @@ INSERT INTO referenced_resource (id, content_type, filename, author, approver, l archivedResourcesMetaItem.getApprover(), archivedResourcesMetaItem.getType(), archivedResourcesMetaItem.getContentType(), - archivedResourcesMetaItem.getLocation(), + archivedResourcesMetaItem.getLocationId(), archivedResourcesMetaItem.getFields(), archivedResourcesMetaItem.getModifiedAt(), archivedResourcesMetaItem.getArchivedAt(), @@ -1053,10 +998,6 @@ INSERT INTO referenced_resource (id, content_type, filename, author, approver, l public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { ArchivedSclResourceMetaItem convertedArchivedResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); if (convertedArchivedResourceMetaItem != null) { - if (convertedArchivedResourceMetaItem.getLocation() == null) { - throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, - String.format("Unable to archive scl_file %s with version %s, no location assigned!", id, version)); - } UUID archivedResourceId = UUID.randomUUID(); insertIntoArchivedResourceTable(archivedResourceId, convertedArchivedResourceMetaItem, version); updateArchivedResourceToResourceTagMappingTable( @@ -1071,7 +1012,7 @@ public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version ver convertedArchivedResourceMetaItem.getApprover(), convertedArchivedResourceMetaItem.getType(), convertedArchivedResourceMetaItem.getContentType(), - convertedArchivedResourceMetaItem.getLocation(), + convertedArchivedResourceMetaItem.getLocationId(), convertedArchivedResourceMetaItem.getFields(), convertedArchivedResourceMetaItem.getModifiedAt(), convertedArchivedResourceMetaItem.getArchivedAt(), @@ -1085,7 +1026,7 @@ public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version ver private ArchivedSclResourceMetaItem getSclFileAsArchivedSclResourceMetaItem(UUID id, Version version, String approver) { String sql = """ SELECT scl_file.*, - l.name as location, + l.id AS location, (xpath('/scl:Hitem/@who', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_who, (xpath('/scl:Hitem/@what', scl_data.header, ARRAY[ARRAY['scl', 'http://www.iec.ch/61850/2003/SCL']]))[1] hitem_what FROM scl_file @@ -1182,8 +1123,9 @@ private List generateFields(String location, String sourceReso } private List generateFieldsFromResultSet(ResultSet resultSet, String examiner) throws SQLException { + String locationName = findLocationByUUID(UUID.fromString(resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD))).getName(); return generateFields( - resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD), + locationName, resultSet.getString(ID_FIELD), resultSet.getString("created_by"), examiner @@ -1280,91 +1222,21 @@ private void executeArchivedResourceInsertStatement(UUID archivedResourceId, Abs } } - private List searchArchivedReferencedResources(UUID archivedResourceUuid) { - String archivedResourcesSql = """ - SELECT ar.*, - COALESCE(sf.name, rr.filename) as name, - COALESCE(sf.created_by, rr.author) as author, - rr.approver as approver, - rr.content_type as content_type, - COALESCE(sf.type, rr.type) as type, - sf.creation_date as modified_at, - ar.archived_at as archived_at, - null as comment, - null as voltage, - ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, - l.name as location - FROM archived_resource ar - INNER JOIN archived_resource_resource_tag arrt - ON ar.id = arrt.archived_resource_id - INNER JOIN resource_tag rt - ON arrt.resource_tag_id = rt.id - LEFT JOIN scl_file sf - ON sf.id = ar.scl_file_id - AND sf.major_version = ar.scl_file_major_version - AND sf.minor_version = ar.scl_file_minor_version - AND sf.patch_version = ar.scl_file_patch_version - LEFT JOIN referenced_resource rr - ON ar.referenced_resource_id = rr.id - LEFT JOIN location l - ON sf.location_id = l.id OR rr.location_id = l.id - WHERE rr.scl_file_id = ? - GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; - """; - - return executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(archivedResourceUuid)); - } - - private List searchArchivedResourceBySclFile(UUID archivedResourceUuid) { - String archivedResourcesSql = """ - SELECT ar.*, - COALESCE(sf.name, rr.filename) as name, - COALESCE(sf.created_by, rr.author) as author, - rr.approver as approver, - rr.content_type as content_type, - COALESCE(sf.type, rr.type) as type, - sf.creation_date as modified_at, - ar.archived_at as archived_at, - null as comment, - null as voltage, - ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, - l.name as location - FROM archived_resource ar - INNER JOIN archived_resource_resource_tag arrt - ON ar.id = arrt.archived_resource_id - INNER JOIN resource_tag rt - ON arrt.resource_tag_id = rt.id - LEFT JOIN scl_file sf - ON sf.id = ar.scl_file_id - AND sf.major_version = ar.scl_file_major_version - AND sf.minor_version = ar.scl_file_minor_version - AND sf.patch_version = ar.scl_file_patch_version - LEFT JOIN referenced_resource rr - ON ar.referenced_resource_id = rr.id - LEFT JOIN location l - ON sf.location_id = l.id OR rr.location_id = l.id - WHERE sf.id = ? - GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; - """; - - return executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(archivedResourceUuid)); - } - @Override public IArchivedResourcesMetaItem searchArchivedResource(UUID id) { String archivedResourcesSql = """ SELECT ar.*, - COALESCE(sf.name, rr.filename) as name, - COALESCE(sf.created_by, rr.author) as author, - rr.approver as approver, - rr.content_type as content_type, - COALESCE(sf.type, rr.type) as type, - sf.creation_date as modified_at, - ar.archived_at as archived_at, - null as comment, - null as voltage, + COALESCE(sf.name, rr.filename) AS name, + COALESCE(sf.created_by, rr.author) AS author, + rr.approver AS approver, + rr.content_type AS content_type, + COALESCE(sf.type, rr.type) AS type, + sf.creation_date AS modified_at, + ar.archived_at AS archived_at, + null AS comment, + null AS voltage, ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, - l.name as location + l.id AS location FROM archived_resource ar INNER JOIN archived_resource_resource_tag arrt ON ar.id = arrt.archived_resource_id @@ -1380,7 +1252,7 @@ public IArchivedResourcesMetaItem searchArchivedResource(UUID id) { LEFT JOIN location l ON sf.location_id = l.id OR rr.location_id = l.id WHERE ar.id = ? - GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.id, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at; """; return new ArchivedResourcesMetaItem(executeArchivedResourceQuery(archivedResourcesSql, Collections.singletonList(id))); } @@ -1401,7 +1273,7 @@ public IArchivedResourcesMetaItem searchArchivedResource(String location, String null AS comment, null AS voltage, ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, - l.name AS location + l.id AS location FROM archived_resource ar LEFT JOIN scl_file sf ON sf.id = ar.scl_file_id @@ -1476,7 +1348,7 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, sb.append(" AND ar.archived_at <= ?"); } sb.append(System.lineSeparator()); - sb.append("GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar;"); + sb.append("GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.id, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar;"); return new ArchivedResourcesMetaItem(executeArchivedResourceQuery(sb.toString(), parameters)); } @@ -1494,7 +1366,7 @@ public IArchivedResourcesHistoryMetaItem searchArchivedResourceHistory(UUID uuid xml_data.hitem_what::varchar AS comment, null AS voltage, ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, - l.name AS location, + l.id AS location, ar.scl_file_id IS NOT NULL OR ar.referenced_resource_id IS NOT NULL AS is_archived FROM archived_resource ar LEFT JOIN scl_file sf @@ -1533,7 +1405,7 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, LEFT JOIN location l ON sf.location_id = l.id OR rr.location_id = l.id WHERE ar.id = ? - GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.name, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar, xml_data.hitem_what::varchar; + GROUP BY ar.id, sf.name, rr.filename, sf.created_by, rr.author, l.id, rr.content_type, rr.approver, sf.type, rr.type, sf.creation_date, ar.archived_at, sf.scl_data, xml_data.hitem_who::varchar, xml_data.hitem_what::varchar; """; List versions = new ArrayList<>(); @@ -1550,7 +1422,9 @@ INNER JOIN (SELECT id, major_version, minor_version, patch_version, } catch (SQLException exp) { throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error retrieving Archived Resource History entries from database!", exp); } - + if (versions.isEmpty()) { + throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", uuid)); + } return new ArchivedResourcesHistoryMetaItem(versions); } @@ -1658,7 +1532,7 @@ private boolean existsLocationResourceTagMapping(UUID id, UUID tagId) { } catch (SQLException exp) { throw new CompasSclDataServiceException( POSTGRES_SELECT_ERROR_CODE, - "Error listing scl entries from database!", exp + "Error listing location resource_tag mapping entries from database!", exp ); } } @@ -1682,7 +1556,7 @@ private boolean existsResourceTagMapping(UUID id, UUID tagId) { } catch (SQLException exp) { throw new CompasSclDataServiceException( POSTGRES_SELECT_ERROR_CODE, - "Error listing scl entries from database!", exp + "Error listing archived_resource resource_tag mapping entries from database!", exp ); } } diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql index dd78d38d..78024ebc 100644 --- a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_7__create_location.sql @@ -4,8 +4,8 @@ create table location ( id uuid not null, - key varchar(255) unique not null, - name varchar(255) unique not null, + key varchar(255) not null, + name varchar(255) not null, description varchar(255), primary key (id) ); diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java index 6fd2fa15..2fb866cd 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java @@ -13,8 +13,6 @@ public interface IAbstractArchivedResourceMetaItem extends IAbstractItem { String getContentType(); - String getLocation(); - default String getNote() { return null; } diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index f8fb0391..dbf3cadf 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -168,8 +168,28 @@ public interface CompasSclDataRepository { */ ILocationMetaItem createLocation(String key, String name, String description); + /** + * Create tags that identify a Location object + * + * @param location The Location object that receives the tags + * @param author The name of the author who created the Location + */ void addLocationTags(ILocationMetaItem location, String author); + /** + * Return whether either the key or the name are already used in a location + * + * @param key The key of the Location used for checking duplicates. + * @param name The name of the Location used for checking duplicates. + * @return True if the key or the name is already used by another Location, otherwise false. + */ + boolean hasDuplicateLocationValues(String key, String name); + + /** + * Delete tags that identify a Location + * + * @param location The Location object to be deleted + */ void deleteLocationTags(ILocationMetaItem location); /** diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index 77ede8c4..b67cb833 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -5,13 +5,7 @@ import org.lfenergy.compas.scl.data.model.ILocationMetaItem; import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Stream; - -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import java.util.UUID; @ApplicationScoped public class CompasSclDataArchivingServiceImpl implements ICompasSclDataArchivingService { @@ -25,14 +19,8 @@ public void createLocation(ILocationMetaItem location) { } @Override - public void deleteLocation(ILocationMetaItem location) { - File directory = new File(DEFAULT_PATH + File.separator + location.getName()); - directory.delete(); - } - - @Override - public void archiveSclData(String filename, File body, IAbstractArchivedResourceMetaItem archivedResource) { - String absolutePath = generateSclDataLocation(archivedResource); + public void archiveSclData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { + String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName) + File.separator + "referenced_resources"; File locationDir = new File(absolutePath); locationDir.mkdirs(); File f = new File(absolutePath + File.separator + filename); @@ -46,8 +34,8 @@ public void archiveSclData(String filename, File body, IAbstractArchivedResource } @Override - public void archiveSclData(IAbstractArchivedResourceMetaItem archivedResource, String data) { - String absolutePath = generateSclDataLocation(archivedResource); + public void archiveSclData(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) { + String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName); File locationDir = new File(absolutePath); locationDir.mkdirs(); File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getContentType().toLowerCase()); @@ -58,32 +46,7 @@ public void archiveSclData(IAbstractArchivedResourceMetaItem archivedResource, S } } - private String generateSclDataLocation(IAbstractArchivedResourceMetaItem archivedResource) { - return DEFAULT_PATH + File.separator + archivedResource.getLocation() + File.separator + archivedResource.getId(); - } - - @Override - public void deleteSclDataFromLocation(ILocationMetaItem location, List resourceIds) { - File locationDir = new File(DEFAULT_PATH + File.separator + location.getName() + File.separator); - try (Stream paths = Files.walk(locationDir.toPath()).skip(1)) { - paths.sorted(Comparator.reverseOrder()).map(Path::toFile).filter(f -> - resourceIds.stream().anyMatch(id -> f.getAbsolutePath().contains(id)) - ).forEach(File::delete); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void moveArchivedResourcesToLocation(ILocationMetaItem oldLocation, ILocationMetaItem newLocation, List resourceIds) { - resourceIds.forEach(id -> { - Path oldPath = new File(DEFAULT_PATH + File.separator + oldLocation.getName() + File.separator + id).toPath(); - Path newPath = new File(DEFAULT_PATH + File.separator + newLocation.getName() + File.separator + id).toPath(); - try { - Files.move(oldPath, newPath, REPLACE_EXISTING); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + private String generateSclDataLocation(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName) { + return DEFAULT_PATH + File.separator + locationName + File.separator + resourceId + File.separator + archivedResource.getVersion(); } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 6fb425fd..c3a3d370 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -184,10 +184,6 @@ public String create(SclFileType type, String name, String who, String comment, var newSclData = converter.convertToString(scl); repository.create(type, id, name, newSclData, version, who, labels); - if (isHistoryEnabled) { - //ToDo cgutmann: check what has to be done here when history table is not available anymore - } - return newSclData; } @@ -266,10 +262,6 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str assignResourceToLocation(UUID.fromString(currentSclMetaInfo.getLocationId()), id); } - if (isHistoryEnabled) { - //ToDo cgutmann: check what needs to be done when history table is not available anymore - } - return newSclData; } @@ -503,6 +495,9 @@ public List listLocations(int page, int pageSize) { */ @Transactional(REQUIRED) public ILocationMetaItem createLocation(String key, String name, String description, String author) { + if (repository.hasDuplicateLocationValues(key, name)) { + throw new CompasSclDataServiceException(CREATION_ERROR_CODE, "Duplicate location key or name provided!"); + } ILocationMetaItem createdLocation = repository.createLocation(key, name, description); repository.addLocationTags(createdLocation, author); archivingService.createLocation(createdLocation); @@ -520,12 +515,11 @@ public void deleteLocation(UUID id) { int assignedResourceCount = locationToDelete.getAssignedResources(); if (assignedResourceCount > 0) { throw new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, - String.format("Deletion of Location %s not allowed, unassign resources before deletion", id)); + String.format("Deletion of Location '%s' not allowed, unassign resources before deletion", id)); } repository.deleteLocationTags(locationToDelete); repository.deleteLocation(id); - archivingService.deleteLocation(locationToDelete); } /** @@ -551,39 +545,14 @@ public ILocationMetaItem updateLocation(UUID id, String key, String name, String @Transactional(REQUIRED) public void assignResourceToLocation(UUID locationId, UUID resourceId) { ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); + if (repository.listHistory(resourceId).isEmpty()) { + throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", resourceId)); + } if (locationItem != null) { - List historyMetaItems = repository.listHistory(resourceId); - ILocationMetaItem previousLocation = null; - if (!historyMetaItems.isEmpty()) { - IAbstractItem sclMetaInfo = repository.findMetaInfoByUUID(SclFileType.valueOf(historyMetaItems.get(0).getType()), resourceId); - if (sclMetaInfo.getLocationId() != null) { - previousLocation = repository.findLocationByUUID(UUID.fromString(sclMetaInfo.getLocationId())); - } - } repository.assignResourceToLocation(locationId, resourceId); - if (previousLocation != null) { - moveArchivedItemsToNewLocation(resourceId, locationItem, previousLocation); - } } } - private void moveArchivedItemsToNewLocation(UUID resourceId, ILocationMetaItem location, ILocationMetaItem previousLocation) { - List archivedResourceIdsToMove = getResourceIdsFromAssignedArchivedResources(resourceId, location); - archivingService.moveArchivedResourcesToLocation(previousLocation, location, archivedResourceIdsToMove); - } - - private List getResourceIdsFromAssignedArchivedResources(UUID resourceId, ILocationMetaItem location) { - return repository.searchArchivedResource(location.getName(), null, null, null, null, null, null, null) - .getResources() - .stream() - .filter(ar -> - ar.getFields() - .stream() - .anyMatch(f -> - f.getKey().equals("SOURCE_RESOURCE_ID") && f.getValue().equals(resourceId.toString()))) - .map(IAbstractItem::getId).toList(); - } - /** * Unassign a Resource from a Location entry * @@ -593,13 +562,13 @@ private List getResourceIdsFromAssignedArchivedResources(UUID resourceId @Transactional(REQUIRED) public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); + if (repository.listHistory(resourceId).isEmpty()) { + throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", resourceId)); + } if (locationItem != null) { - List resourceIdsToBeUnassigned = getResourceIdsFromAssignedArchivedResources(resourceId, locationItem); - archivingService.deleteSclDataFromLocation(locationItem, resourceIdsToBeUnassigned); repository.unassignResourceFromLocation(locationId, resourceId); } } - /** * Archive a resource and link it to the corresponding scl_file entry * @@ -614,9 +583,17 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { */ @Transactional(REQUIRED) public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version, String author, String approver, String contentType, String filename, File body) { + List historyItem = repository.listHistoryVersionsByUUID(id); + if (!historyItem.isEmpty() && historyItem.get(0).getLocation() == null) { + throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, + String.format("Unable to archive file '%s' for scl resource '%s' with version %s, no location assigned!", filename, id, version)); + } else if (historyItem.isEmpty() || historyItem.stream().noneMatch(hi -> hi.getVersion().equals(version))) { + throw new CompasNoDataFoundException( + String.format("Unable to archive file '%s' for scl resource '%s' with version %s, unable to find scl resource '%s' with version %s!", filename, id, version, id, version)); + } IAbstractArchivedResourceMetaItem archivedResource = repository.archiveResource(id, new Version(version), author, approver, contentType, filename); if (body != null) { - archivingService.archiveSclData(filename, body, archivedResource); + archivingService.archiveSclData(repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), filename, id, body, archivedResource); } return archivedResource; } @@ -631,10 +608,15 @@ public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version */ @Transactional(REQUIRED) public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { + List historyItem = repository.listHistory(id); + if (!historyItem.isEmpty() && historyItem.get(0).getLocation() == null) { + throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, + String.format("Unable to archive scl file '%s' with version %s, no location assigned!", id, version)); + } IAbstractArchivedResourceMetaItem archivedResource = repository.archiveSclResource(id, version, approver); String data = repository.findByUUID(id, version); if (data != null) { - archivingService.archiveSclData(archivedResource, data); + archivingService.archiveSclData(id, archivedResource, repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), data); } return archivedResource; } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java index fbc1f51c..8c9c7435 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -4,19 +4,13 @@ import org.lfenergy.compas.scl.data.model.ILocationMetaItem; import java.io.File; -import java.util.List; +import java.util.UUID; public interface ICompasSclDataArchivingService { void createLocation(ILocationMetaItem location); - void deleteLocation(ILocationMetaItem location); + void archiveSclData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); - void archiveSclData(String filename, File body, IAbstractArchivedResourceMetaItem archivedResource); - - void archiveSclData(IAbstractArchivedResourceMetaItem archivedResource, String data); - - void deleteSclDataFromLocation(ILocationMetaItem location, List resourceIds); - - void moveArchivedResourcesToLocation(ILocationMetaItem oldLocation, ILocationMetaItem newLocation, List resourceIds); + void archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data); } diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index fa60bec4..49050148 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -400,7 +400,7 @@ void listHistory_WhenCalled_ThenReturnHistoryItemWithLatestVersion() { "1.0.1", "IID", "User1", - "Coment after update", + "Comment after update", null, null, false, @@ -423,7 +423,7 @@ void listHistory_WhenCalledWithId_ThenReturnHistoryItemWithLatestVersion() { "1.0.1", "IID", "User1", - "Coment after update", + "Comment after update", null, null, false, @@ -446,7 +446,7 @@ void listHistory_WhenCalledWithSearchParameters_ThenReturnHistoryItemWithLatestV "1.0.1", "IID", "User1", - "Coment after update", + "Comment after update", null, null, false, @@ -483,7 +483,7 @@ void listHistoryVersionsByUUID_WhenCalledWithId_ThenReturnHistoryItems() { "1.0.1", "IID", "User1", - "Coment after update", + "Comment after update", null, OffsetDateTime.now(), false, @@ -551,7 +551,6 @@ void deleteLocation_WhenCalled_ThenLocationIsDeleted() { verify(compasSclDataRepository).deleteLocationTags(locationToRemove); verify(compasSclDataRepository).deleteLocation(locationId); - verify(compasSclDataArchivingService).deleteLocation(locationToRemove); } @Test @@ -565,7 +564,6 @@ void deleteLocation_WhenResourceIsAssigned_ThenExceptionIsThrown() { verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); verify(compasSclDataRepository, times(0)).deleteLocationTags(locationToRemove); verify(compasSclDataRepository, times(0)).deleteLocation(locationId); - verify(compasSclDataArchivingService, times(0)).deleteLocation(locationToRemove); } @Test @@ -585,47 +583,47 @@ void assignResourceToLocation_WhenCalled_ThenLocationIsAssigned() { UUID locationId = UUID.randomUUID(); UUID resourceId = UUID.randomUUID(); ILocationMetaItem locationItem = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 0); + HistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", + "1.0.0", + null, + "someUser", + "IID", + "locationName", + null, + true, + true, + false + ); when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(locationItem); - when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of()); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyMetaItem)); compasSclDataService.assignResourceToLocation(locationId, resourceId); verify(compasSclDataRepository, times(1)).assignResourceToLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).listHistory(resourceId); } @Test void assignResourceToLocation_WhenResourceHasLocationAssignedWithArchivedItems_ThenNewLocationIsAssignedAndArchivedItemsAreMoved() { - UUID oldLocationId = UUID.randomUUID(); UUID resourceId = UUID.randomUUID(); UUID newLocationId = UUID.randomUUID(); - UUID archivedResourceId = UUID.randomUUID(); - String sclDataName = "sclDataName"; - ILocationMetaItem oldLocationItem = new LocationMetaItem(oldLocationId.toString(), "oldLocationKey", "oldLocationName", null, 1); ILocationMetaItem newLocationItem = new LocationMetaItem(newLocationId.toString(), "newLocationKey", "newLocationName", null, 0); - IHistoryMetaItem historyItem = new HistoryMetaItem(resourceId.toString(), sclDataName, "1.0.0", "IID", "someUser", null, oldLocationItem.getName(), null, true, true, false); - AbstractItem sclData = new org.lfenergy.compas.scl.data.model.SclMetaInfo(resourceId.toString(), sclDataName, "1.0.0", oldLocationId.toString()); - IAbstractArchivedResourceMetaItem archivedResourceMetaItem = new ArchivedSclResourceMetaItem( - archivedResourceId.toString(), - sclDataName, + IHistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", "1.0.0", - "someUser", null, + "someUser", "IID", + "locationName", null, - oldLocationItem.getName(), - List.of(new ResourceTagItem(UUID.randomUUID().toString(), "SOURCE_RESOURCE_ID", resourceId.toString())), - null, - OffsetDateTime.now(), - null, - null + true, + true, + false ); - IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(archivedResourceMetaItem)); - when(compasSclDataRepository.findLocationByUUID(newLocationId)).thenReturn(newLocationItem); - when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyItem)); - when(compasSclDataRepository.findMetaInfoByUUID(SclFileType.IID, resourceId)).thenReturn(sclData); - when(compasSclDataRepository.findLocationByUUID(oldLocationId)).thenReturn(oldLocationItem); - when(compasSclDataRepository.searchArchivedResource("newLocationName", null, null, null, null, null, null, null)) - .thenReturn(archivedResources); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyMetaItem)); compasSclDataService.assignResourceToLocation(newLocationId, resourceId); @@ -638,34 +636,30 @@ void assignResourceToLocation_WhenResourceHasLocationAssignedWithArchivedItems_T void unassignResourcesFromLocation_WhenCalled_ThenLocationIsUnassigned() { UUID locationId = UUID.randomUUID(); UUID resourceId = UUID.randomUUID(); - UUID archivedResourceId = UUID.randomUUID(); ILocationMetaItem assignedLocation = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 1); - IAbstractArchivedResourceMetaItem archivedResourceMetaItem = new ArchivedSclResourceMetaItem( - archivedResourceId.toString(), + HistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), "sclDataName", "1.0.0", - "someUser", null, + "someUser", "IID", - null, "locationName", - List.of(new ResourceTagItem(UUID.randomUUID().toString(), "SOURCE_RESOURCE_ID", resourceId.toString())), null, - OffsetDateTime.now(), - null, - null + true, + true, + false ); - IArchivedResourcesMetaItem archivedResources = new ArchivedResourcesMetaItem(List.of(archivedResourceMetaItem)); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyMetaItem)); when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(assignedLocation); - when(compasSclDataRepository.searchArchivedResource("locationName", null, null, null, null, null, null, null)).thenReturn(archivedResources); compasSclDataService.unassignResourceFromLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); + verify(compasSclDataRepository, times(1)).listHistory(resourceId); verify(compasSclDataRepository, times(1)).unassignResourceFromLocation(locationId, resourceId); - verify(compasSclDataRepository, times(1)).searchArchivedResource("locationName", null, null, null, null, null, null, null); - verify(compasSclDataArchivingService, times(1)).deleteSclDataFromLocation(assignedLocation, List.of(archivedResourceId.toString())); } @Test @@ -680,12 +674,12 @@ void unassignResourcesFromLocation_WhenLocationNotFound_ThenExceptionIsThrown() verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); verify(compasSclDataRepository, times(0)).unassignResourceFromLocation(locationId, resourceId); verify(compasSclDataRepository, times(0)).searchArchivedResource(any(), any(), any(), any(), any(), any(), any(), any()); - verify(compasSclDataArchivingService, times(0)).deleteSclDataFromLocation(any(), any()); } @Test void archiveResource_WhenCalled_ThenResourceIsArchived() { UUID resourceId = UUID.randomUUID(); + UUID locationId = UUID.randomUUID(); String version = "1.0.0"; String author = "someUser"; String approver = "someOtherUser"; @@ -694,7 +688,32 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { File file = new File("test.pdf"); try { file.createNewFile(); - + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn( + new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + null, + 0 + ) + ); + when(compasSclDataRepository.listHistoryVersionsByUUID(resourceId)).thenReturn( + List.of( + new HistoryMetaItem( + resourceId.toString(), + "someName", + version, + null, + author, + null, + locationId.toString(), + OffsetDateTime.now(), + false, + true, + false + ) + ) + ); when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) .thenReturn( new ArchivedSclResourceMetaItem( @@ -705,7 +724,7 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { approver, null, contentType, - "locationName", + locationId.toString(), List.of(), null, OffsetDateTime.now(), @@ -717,7 +736,7 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); - verify(compasSclDataArchivingService, times(1)).archiveSclData(any(), any(), any()); + verify(compasSclDataArchivingService, times(1)).archiveSclData(any(), any(), any(), any(), any()); } catch (IOException e) { throw new RuntimeException(e); } finally { @@ -726,15 +745,28 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { } @Test - void archiveResource_WhenFileIsNull_ThenFileIsWritten() { + void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { UUID resourceId = UUID.randomUUID(); String version = "1.0.0"; String author = "someUser"; String approver = "someOtherUser"; String contentType = "application/pdf"; String filename = "test.pdf"; - File file = null; + IHistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", + version, + null, + "someUser", + "IID", + "locationName", + null, + true, + true, + false + ); + when(compasSclDataRepository.listHistoryVersionsByUUID(resourceId)).thenReturn(List.of(historyMetaItem)); when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) .thenReturn( new ArchivedSclResourceMetaItem( @@ -754,15 +786,16 @@ void archiveResource_WhenFileIsNull_ThenFileIsWritten() { ) ); - compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); + compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, null); verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); - verify(compasSclDataArchivingService, times(0)).archiveSclData(any(), any(), any()); + verify(compasSclDataArchivingService, times(0)).archiveSclData(any(), any(), any(), any()); } @Test void archiveSclResource_WhenCalled_ThenResourceIsArchived() { UUID resourceId = UUID.randomUUID(); + UUID locationId = UUID.randomUUID(); String version = "1.0.0"; String approver = "someOtherUser"; ArchivedSclResourceMetaItem archivedResource = new ArchivedSclResourceMetaItem( @@ -773,7 +806,7 @@ void archiveSclResource_WhenCalled_ThenResourceIsArchived() { approver, "IID", null, - "locationName", + locationId.toString(), List.of(), null, OffsetDateTime.now(), @@ -784,12 +817,21 @@ void archiveSclResource_WhenCalled_ThenResourceIsArchived() { when(compasSclDataRepository.archiveSclResource(resourceId, new Version(version), approver)).thenReturn(archivedResource); when(compasSclDataRepository.findByUUID(resourceId, new Version(version))).thenReturn(sclData); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn( + new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + null, + 0 + ) + ); compasSclDataService.archiveSclResource(resourceId, new Version(version), approver); verify(compasSclDataRepository, times(1)).archiveSclResource(resourceId, new Version(version), approver); verify(compasSclDataRepository, times(1)).findByUUID(resourceId, new Version(version)); - verify(compasSclDataArchivingService, times(1)).archiveSclData(archivedResource, sclData); + verify(compasSclDataArchivingService, times(1)).archiveSclData(resourceId, archivedResource, "locationName", sclData); } @Test From f02c3a7a981b94e4fa71a8e1c00354c0e6e0a31e Mon Sep 17 00:00:00 2001 From: cgutmann Date: Tue, 21 Jan 2025 08:38:56 +0100 Subject: [PATCH 14/23] chore: rename function for saving related resources to FS --- .../scl/data/service/CompasSclDataArchivingServiceImpl.java | 2 +- .../lfenergy/compas/scl/data/service/CompasSclDataService.java | 2 +- .../compas/scl/data/service/ICompasSclDataArchivingService.java | 2 +- .../compas/scl/data/service/CompasSclDataServiceTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index b67cb833..9ce6204a 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -19,7 +19,7 @@ public void createLocation(ILocationMetaItem location) { } @Override - public void archiveSclData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { + public void archiveData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName) + File.separator + "referenced_resources"; File locationDir = new File(absolutePath); locationDir.mkdirs(); diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index c3a3d370..b48b7c4a 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -593,7 +593,7 @@ public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version } IAbstractArchivedResourceMetaItem archivedResource = repository.archiveResource(id, new Version(version), author, approver, contentType, filename); if (body != null) { - archivingService.archiveSclData(repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), filename, id, body, archivedResource); + archivingService.archiveData(repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), filename, id, body, archivedResource); } return archivedResource; } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java index 8c9c7435..762ea8fd 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -10,7 +10,7 @@ public interface ICompasSclDataArchivingService { void createLocation(ILocationMetaItem location); - void archiveSclData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); + void archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); void archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data); } diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 49050148..dd6bdb13 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -736,7 +736,7 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); - verify(compasSclDataArchivingService, times(1)).archiveSclData(any(), any(), any(), any(), any()); + verify(compasSclDataArchivingService, times(1)).archiveData(any(), any(), any(), any(), any()); } catch (IOException e) { throw new RuntimeException(e); } finally { From ce73d9f2b7e1a0b6463f34a644dcf4846babebaa Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 20 Feb 2025 07:49:12 +0100 Subject: [PATCH 15/23] feat: implement storing archived resources in elo client --- .../rest/api/archive/ArchiveResource.java | 6 +- .../data/rest/api/archive/ArchivingApi.java | 3 + app/src/main/resources/application.properties | 39 +++ .../rest/api/archive/ArchiveResourceTest.java | 5 +- .../CompasSclDataPostgreSQLRepository.java | 88 +++--- .../CompasSclDataServiceErrorCode.java | 1 + service/pom.xml | 14 + .../compas/scl/data/dto/ResourceData.java | 265 ++++++++++++++++++ .../compas/scl/data/dto/ResourceMetaData.java | 34 +++ .../compas/scl/data/dto/ResourceTag.java | 27 ++ .../compas/scl/data/dto/TypeEnum.java | 35 +++ .../CompasSclDataArchivingEloServiceImpl.java | 107 +++++++ .../CompasSclDataArchivingServiceImpl.java | 29 +- .../data/service/CompasSclDataService.java | 136 +++++++-- .../ICompasSclDataArchivingService.java | 6 +- .../data/service/IEloConnectorRestClient.java | 37 +++ .../service/CompasSclDataServiceTest.java | 73 ++++- 17 files changed, 813 insertions(+), 92 deletions(-) create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceData.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceMetaData.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceTag.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/TypeEnum.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java index 3a7276e1..abe53a0b 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -35,8 +35,7 @@ public ArchiveResource(CompasSclDataService compasSclDataService, JsonWebToken j @Override public Uni archiveResource(UUID id, String version, String xAuthor, String xApprover, String contentType, String xFilename, File body) { LOGGER.info("Archiving resource '{}' for scl resource with id '{}' and version '{}'", xFilename, id, version); - return Uni.createFrom() - .item(() -> compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body)) + return compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() .transform(this::mapToArchivedResource); @@ -46,8 +45,7 @@ public Uni archiveResource(UUID id, String version, String xAu public Uni archiveSclResource(UUID id, String version) { LOGGER.info("Archiving scl resource with id '{}' and version '{}'", id, version); String approver = jsonWebToken.getClaim(userInfoProperties.name()); - return Uni.createFrom() - .item(() -> compasSclDataService.archiveSclResource(id, new Version(version), approver)) + return compasSclDataService.archiveSclResource(id, new Version(version), approver) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() .transform(this::mapToArchivedResource); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java index 42b2dbfc..39a6fd15 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java @@ -1,5 +1,6 @@ package org.lfenergy.compas.scl.data.rest.api.archive; +import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; import jakarta.validation.Valid; import jakarta.ws.rs.*; @@ -18,11 +19,13 @@ public interface ArchivingApi { @POST @Path("/referenced-resource/{id}/versions/{version}") + @Blocking @Produces({ "application/json" }) Uni archiveResource(@PathParam("id") UUID id, @PathParam("version") String version, @HeaderParam("X-author") String xAuthor, @HeaderParam("X-approver") String xApprover, @HeaderParam("Content-Type") String contentType, @HeaderParam("X-filename") String xFilename, @Valid File body); @POST @Path("/scl/{id}/versions/{version}") + @Blocking @Produces({ "application/json" }) Uni archiveSclResource(@PathParam("id") UUID id, @PathParam("version") String version); diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 1bce8cf8..b7468034 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -260,6 +260,30 @@ quarkus.http.auth.permission.STD_CREATE_POST_WS.policy=STD_CREATE quarkus.http.auth.permission.STD_UPDATE_PUT_WS.paths=/compas-scl-data-service/scl-ws/v1/STD/update quarkus.http.auth.permission.STD_UPDATE_PUT_WS.policy=STD_UPDATE +quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.paths=/compas-scl-data-service/api/resources/* +quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.methods=GET +quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.policy=ARCHIVE_API_READ + +quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.paths=/compas-scl-data-service/api/resources/* +quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.methods=POST +quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE + +quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.paths=/compas-scl-data-service/api/locations/* +quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.methods=GET +quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.policy=ARCHIVE_API_GET + +quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.paths=/compas-scl-data-service/api/locations/* +quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.methods=POST +quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE + +quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.paths=/compas-scl-data-service/api/locations/* +quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.methods=PUT +quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.policy=ARCHIVE_API_UPDATE + +quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.paths=/compas-scl-data-service/api/locations/* +quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.methods=DELETE +quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.policy=ARCHIVE_API_DELETE + quarkus.http.auth.permission.api.paths=/compas-scl-data-service/api/* quarkus.http.auth.permission.api.policy=permit @@ -267,3 +291,18 @@ quarkus.http.auth.permission.api.policy=permit scl-data-service.features.is-history-enabled=true # Feature flag to enable or disable persistent delete mode scl-data-service.features.keep-deleted-files=true + +quarkus.log.category."io.quarkus.restclient".level=DEBUG +#scl-data-service.archiving.filesystem.location=/work/locations +scl-data-service.archiving.connector.enabled=${ELO_CONNECTOR_ENABLED:false} +#scl-data-service.archiving.connector.clientId=test +#scl-data-service.archiving.connector.clientSecret=testSecret + +quarkus.rest-client.elo-connector-client.url=${ELO_CONNECTOR_BASE_URL:http://elo-connector:8080/compas-elo-connector/api} +#quarkus.tls.trust-all=true + +#quarkus.oidc-client.client-enabled=false +# +#quarkus.oidc-client.jwt-secret.auth-server-url=${JWT_VERIFY_ISSUER:http://localhost:8089/auth/realms/compas} +#quarkus.oidc-client.jwt-secret.client-id=${JWT_VERIFY_CLIENT_ID:scl-data-service} +#quarkus.oidc-client.jwt-secret.credentials.jwt.secret=${JWT_VERIFY_CLIENT_SECRET:AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java index 8d12687e..5c687efe 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java @@ -5,6 +5,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.Test; @@ -42,7 +43,7 @@ void archiveSclResource_WhenCalled_ThenReturnsArchivedResource() { String version = "1.0.0"; IAbstractArchivedResourceMetaItem testData = new ArchivedSclResourceTestDataBuilder().setId(uuid.toString()).build(); when(jwt.getClaim("name")).thenReturn(""); - when(compasSclDataService.archiveSclResource(uuid, new Version(version), "")).thenReturn(testData); + when(compasSclDataService.archiveSclResource(uuid, new Version(version), "")).thenReturn(Uni.createFrom().item(testData)); Response response = given() .contentType(MediaType.APPLICATION_JSON) .when().post("/scl/" + uuid + "/versions/" + version) @@ -64,7 +65,7 @@ void archiveResource_WhenCalled_ThenReturnsArchivedResource() { String version = "1.0.0"; IAbstractArchivedResourceMetaItem testData = new ArchivedReferencedResourceTestDataBuilder().setId(uuid.toString()).build(); File f = Paths.get("src","test","resources","scl", "icd_import_ied_test.scd").toFile(); - when(compasSclDataService.archiveResource(eq(uuid), eq(version), eq(null), eq(null), eq("application/json"), eq(null), any(File.class))).thenReturn(testData); + when(compasSclDataService.archiveResource(eq(uuid), eq(version), eq(null), eq(null), eq("application/json"), eq(null), any(File.class))).thenReturn(Uni.createFrom().item(testData)); Response response = given() .contentType(MediaType.APPLICATION_JSON) .body(f) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 284dbeb9..a2e55a6d 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -6,7 +6,10 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.transaction.TransactionManager; import jakarta.transaction.Transactional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; @@ -54,8 +57,13 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor private static final String ARCHIVEMETAITEM_MODIFIED_AT_FIELD = "modified_at"; private static final String ARCHIVEMETAITEM_ARCHIVED_AT_FIELD = "archived_at"; + private static final Logger LOGGER = LogManager.getLogger(CompasSclDataPostgreSQLRepository.class); + private final DataSource dataSource; + @Inject + TransactionManager tm; + @Inject public CompasSclDataPostgreSQLRepository(DataSource dataSource) { this.dataSource = dataSource; @@ -93,7 +101,6 @@ left outer join ( try (var connection = dataSource.getConnection(); var stmt = connection.prepareStatement(sql)) { stmt.setString(1, type.name()); - try (var resultSet = stmt.executeQuery()) { while (resultSet.next()) { items.add(new Item(resultSet.getString(ID_FIELD), @@ -935,26 +942,7 @@ public IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version versio String locationId = sclResourceMetaItem.getLocationId(); UUID assignedResourceId = UUID.randomUUID(); - String sql = """ - INSERT INTO referenced_resource (id, content_type, filename, author, approver, location_id, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - """; - try (Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement(sql)) { - stmt.setObject(1, assignedResourceId); - stmt.setObject(2, contentType); - stmt.setObject(3, filename); - stmt.setObject(4, author); - stmt.setObject(5, approver); - stmt.setObject(6, UUID.fromString(locationId)); - stmt.setObject(7, id); - stmt.setObject(8, version.getMajorVersion()); - stmt.setObject(9, version.getMinorVersion()); - stmt.setObject(10, version.getPatchVersion()); - stmt.executeUpdate(); - } catch (SQLException exp) { - throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error inserting Referenced Resources!", exp); - } + insertRelatedResourceEntry(id, version, author, approver, contentType, filename, assignedResourceId, locationId); String locationName = findLocationByUUID(UUID.fromString(locationId)).getName(); List resourceTags = generateFields(locationName, id.toString(), author, approver); @@ -994,6 +982,29 @@ INSERT INTO referenced_resource (id, content_type, filename, author, approver, l ); } + private void insertRelatedResourceEntry(UUID id, Version version, String author, String approver, String contentType, String filename, UUID assignedResourceId, String locationId) { + String sql = """ + INSERT INTO referenced_resource (id, content_type, filename, author, approver, location_id, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """; + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setObject(1, assignedResourceId); + stmt.setObject(2, contentType); + stmt.setObject(3, filename); + stmt.setObject(4, author); + stmt.setObject(5, approver); + stmt.setObject(6, UUID.fromString(locationId)); + stmt.setObject(7, id); + stmt.setObject(8, version.getMajorVersion()); + stmt.setObject(9, version.getMinorVersion()); + stmt.setObject(10, version.getPatchVersion()); + stmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error inserting Referenced Resources!", exp); + } + } + @Override public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { ArchivedSclResourceMetaItem convertedArchivedResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); @@ -1078,8 +1089,8 @@ private ArchivedSclResourceMetaItem mapResultSetToArchivedSclResource(String app createVersion(resultSet), resultSet.getString("created_by"), approver, - null, resultSet.getString("type"), + null, resultSet.getString("location"), fieldList, convertToOffsetDateTime(Timestamp.from(Instant.now())), @@ -1097,19 +1108,19 @@ private List generateFields(String location, String sourceReso ResourceTagItem authorTag = getResourceTag("AUTHOR", author); ResourceTagItem examinerTag = getResourceTag("EXAMINER", examiner); - if (locationTag == null) { + if (locationTag == null && location != null) { createResourceTag("LOCATION", location); locationTag = getResourceTag("LOCATION", location); } - if (resourceIdTag == null) { + if (resourceIdTag == null && sourceResourceId != null) { createResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); resourceIdTag = getResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); } - if (authorTag == null) { + if (authorTag == null && author != null) { createResourceTag("AUTHOR", author); authorTag = getResourceTag("AUTHOR", author); } - if (examinerTag == null) { + if (examinerTag == null && examiner != null) { createResourceTag("EXAMINER", examiner); examinerTag = getResourceTag("EXAMINER", examiner); } @@ -1119,7 +1130,7 @@ private List generateFields(String location, String sourceReso fieldList.add(authorTag); fieldList.add(examinerTag); - return fieldList; + return fieldList.stream().filter(Objects::nonNull).toList(); } private List generateFieldsFromResultSet(ResultSet resultSet, String examiner) throws SQLException { @@ -1188,21 +1199,20 @@ private ResourceTagItem getResourceTag(String key, String value) { } private void insertIntoArchivedResourceTable(UUID archivedResourceId, AbstractArchivedResourceMetaItem archivedResource, Version version) { - String insertSclResourceIntoArchiveSql = """ - INSERT INTO archived_resource(id, archived_at, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) - VALUES (?, ?, ?, ?, ?, ?); - """; - - String insertReferencedResourceIntoArchiveSql = """ - INSERT INTO archived_resource(id, archived_at, referenced_resource_id, referenced_resource_major_version, referenced_resource_minor_version, referenced_resource_patch_version) - VALUES (?, ?, ?, ?, ?, ?); - """; - if (archivedResource instanceof ArchivedSclResourceMetaItem) { + String insertSclResourceIntoArchiveSql = """ + INSERT INTO archived_resource(id, archived_at, scl_file_id, scl_file_major_version, scl_file_minor_version, scl_file_patch_version) + VALUES (?, ?, ?, ?, ?, ?); + """; executeArchivedResourceInsertStatement(archivedResourceId, archivedResource, version, insertSclResourceIntoArchiveSql); - } else { - executeArchivedResourceInsertStatement(archivedResourceId, archivedResource, version, insertReferencedResourceIntoArchiveSql); + return; } + + String insertReferencedResourceIntoArchiveSql = """ + INSERT INTO archived_resource(id, archived_at, referenced_resource_id, referenced_resource_major_version, referenced_resource_minor_version, referenced_resource_patch_version) + VALUES (?, ?, ?, ?, ?, ?); + """; + executeArchivedResourceInsertStatement(archivedResourceId, archivedResource, version, insertReferencedResourceIntoArchiveSql); } private void executeArchivedResourceInsertStatement(UUID archivedResourceId, AbstractArchivedResourceMetaItem archivedResource, Version version, String query) { diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java index bae3fa2c..0715e4a2 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java @@ -19,6 +19,7 @@ public class CompasSclDataServiceErrorCode { public static final String LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE = "SDS-0010"; public static final String INVALID_SCL_CONTENT_TYPE_ERROR_CODE = "SDS-0011"; public static final String NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE = "SDS-0012"; + public static final String RESOURCE_ALREADY_ARCHIVED = "SDS-0013"; public static final String POSTGRES_SELECT_ERROR_CODE = "SDS-2000"; public static final String POSTGRES_INSERT_ERROR_CODE = "SDS-2001"; diff --git a/service/pom.xml b/service/pom.xml index 9355811e..11a3165d 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -49,6 +49,20 @@ SPDX-License-Identifier: Apache-2.0 log4j-core provided + + io.quarkus + quarkus-rest-client-jackson + 3.17.7 + + + io.quarkus + quarkus-narayana-jta + + + io.quarkus + quarkus-rest-client-oidc-filter + 3.18.0 + diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceData.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceData.java new file mode 100644 index 00000000..d046176e --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceData.java @@ -0,0 +1,265 @@ +package org.lfenergy.compas.scl.data.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + + + +@JsonTypeName("ResourceData") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2025-02-04T13:09:27.372199200+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class ResourceData { + + private TypeEnum type; + private UUID uuid; + private String location; + private @Valid List<@Valid ResourceTag> tags = new ArrayList<>(); + private String name; + private String contentType; + private String version; + private String extension; + private String data; + + /** + **/ + public ResourceData type(TypeEnum type) { + this.type = type; + return this; + } + + + @JsonProperty("type") + @NotNull public TypeEnum getType() { + return type; + } + + @JsonProperty("type") + public void setType(TypeEnum type) { + this.type = type; + } + + /** + **/ + public ResourceData uuid(UUID uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + @NotNull public UUID getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + /** + * required field when 'type == RESOURCE' + **/ + public ResourceData location(String location) { + this.location = location; + return this; + } + + + @JsonProperty("location") + public String getLocation() { + return location; + } + + @JsonProperty("location") + public void setLocation(String location) { + this.location = location; + } + + /** + **/ + public ResourceData tags(List<@Valid ResourceTag> tags) { + this.tags = tags; + return this; + } + + + @JsonProperty("tags") + @Valid public List<@Valid ResourceTag> getTags() { + return tags; + } + + @JsonProperty("tags") + public void setTags(List<@Valid ResourceTag> tags) { + this.tags = tags; + } + + public ResourceData addTagsItem(ResourceTag tagsItem) { + if (this.tags == null) { + this.tags = new ArrayList<>(); + } + + this.tags.add(tagsItem); + return this; + } + + public ResourceData removeTagsItem(ResourceTag tagsItem) { + if (tagsItem != null && this.tags != null) { + this.tags.remove(tagsItem); + } + + return this; + } + /** + **/ + public ResourceData name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public ResourceData contentType(String contentType) { + this.contentType = contentType; + return this; + } + + + @JsonProperty("contentType") + public String getContentType() { + return contentType; + } + + @JsonProperty("contentType") + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + **/ + public ResourceData version(String version) { + this.version = version; + return this; + } + + + @JsonProperty("version") + @NotNull public String getVersion() { + return version; + } + + @JsonProperty("version") + public void setVersion(String version) { + this.version = version; + } + + /** + **/ + public ResourceData extension(String extension) { + this.extension = extension; + return this; + } + + + @JsonProperty("extension") + public String getExtension() { + return extension; + } + + @JsonProperty("extension") + public void setExtension(String extension) { + this.extension = extension; + } + + /** + * UTF8 encoded input stream of the resource + **/ + public ResourceData data(String data) { + this.data = data; + return this; + } + + + @JsonProperty("data") + @NotNull public String getData() { + return data; + } + + @JsonProperty("data") + public void setData(String data) { + this.data = data; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceData resourceData = (ResourceData) o; + return Objects.equals(this.type, resourceData.type) && + Objects.equals(this.uuid, resourceData.uuid) && + Objects.equals(this.location, resourceData.location) && + Objects.equals(this.tags, resourceData.tags) && + Objects.equals(this.name, resourceData.name) && + Objects.equals(this.contentType, resourceData.contentType) && + Objects.equals(this.version, resourceData.version) && + Objects.equals(this.extension, resourceData.extension) && + Objects.equals(this.data, resourceData.data); + } + + @Override + public int hashCode() { + return Objects.hash(type, uuid, location, tags, name, contentType, version, extension, data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ResourceData {\n"); + + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" tags: ").append(toIndentedString(tags)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" extension: ").append(toIndentedString(extension)).append("\n"); + sb.append(" data: ").append(toIndentedString(data)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} \ No newline at end of file diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceMetaData.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceMetaData.java new file mode 100644 index 00000000..ae10f454 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceMetaData.java @@ -0,0 +1,34 @@ +package org.lfenergy.compas.scl.data.dto; + +import java.util.List; +import java.util.UUID; + +public class ResourceMetaData { + private final TypeEnum type; + private final UUID uuid; + private final String location; + private final List tags; + + public ResourceMetaData(TypeEnum type, UUID uuid, String location, List tags) { + this.type = type; + this.uuid = uuid; + this.location = location; + this.tags = tags; + } + + public TypeEnum getType() { + return type; + } + + public UUID getUuid() { + return uuid; + } + + public String getLocation() { + return location; + } + + public List getTags() { + return tags; + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceTag.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceTag.java new file mode 100644 index 00000000..6982cc98 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/ResourceTag.java @@ -0,0 +1,27 @@ +package org.lfenergy.compas.scl.data.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeName("ResourceTag") +public class ResourceTag { + private final String key; + private final String value; + + @JsonCreator + public ResourceTag(String key, String value) { + this.key = key; + this.value = value; + } + + @JsonProperty("key") + public String getKey() { + return key; + } + + @JsonProperty("value") + public String getValue() { + return value; + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/TypeEnum.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/TypeEnum.java new file mode 100644 index 00000000..a0926036 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/TypeEnum.java @@ -0,0 +1,35 @@ +package org.lfenergy.compas.scl.data.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TypeEnum { + RESOURCE("RESOURCE"), + TEMPLATE("TEMPLATE"); + + private final String value; + + TypeEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static TypeEnum fromValue(String value) { + for (TypeEnum b : TypeEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java new file mode 100644 index 00000000..6095b4be --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java @@ -0,0 +1,107 @@ +package org.lfenergy.compas.scl.data.service; + +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MediaType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.lfenergy.compas.scl.data.dto.ResourceData; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; +import org.lfenergy.compas.scl.data.dto.ResourceTag; +import org.lfenergy.compas.scl.data.dto.TypeEnum; +import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; +import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; +import org.lfenergy.compas.scl.data.model.ILocationMetaItem; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.UUID; + +import static org.lfenergy.compas.scl.data.exception.CompasSclDataServiceErrorCode.CREATION_ERROR_CODE; + +@ApplicationScoped +public class CompasSclDataArchivingEloServiceImpl implements ICompasSclDataArchivingService { + + private static final Logger LOGGER = LogManager.getLogger(CompasSclDataArchivingEloServiceImpl.class); + + @Inject + @RestClient + IEloConnectorRestClient eloClient; + + @Override + public void createLocation(ILocationMetaItem location) { + LOGGER.info("Creating a new location in ELO!"); + } + + @Override + public Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource) { + LOGGER.debug("Archiving related resource in ELO!"); + + ResourceData resourceData = new ResourceData(); + String extension = archivedResource.getName().substring(archivedResource.getName().lastIndexOf(".") + 1).toLowerCase(); + String contentType = archivedResource.getContentType(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(uuid); + resourceData.type(TypeEnum.RESOURCE); + resourceData.location(locationName); + String name = archivedResource.getName().substring(0, archivedResource.getName().lastIndexOf(".")); + resourceData.name(name); + + try (FileInputStream fis = new FileInputStream(body)) { + byte[] fileData = new byte[(int) body.length()]; + fis.read(fileData); + resourceData.data(Base64.getEncoder().encodeToString(fileData)); + return eloClient.createArchivedResource(resourceData) + .onFailure() + .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while archiving referenced resource '%s' in ELO!", uuid))) + .onItem() + .invoke(item -> + LOGGER.debug("returned archived related item {} ", item) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archivedResource, ResourceData resourceData, String extension, String contentType) { + resourceData.version(archivedResource.getVersion()); + resourceData.extension(extension); + resourceData.contentType(contentType); + archivedResource.getFields().stream() + .filter(field -> field.getValue() != null) + .map(field -> + new ResourceTag(field.getKey(), field.getValue())) + .forEach( + resourceData::addTagsItem + ); + } + + @Override + public Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) throws CompasSclDataServiceException { + LOGGER.debug("Archiving scl resource in ELO!"); + + ResourceData resourceData = new ResourceData(); + String extension = archivedResource.getType(); + String contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE.getType(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(uuid); + resourceData.type(TypeEnum.RESOURCE); + resourceData.location(locationName); + resourceData.name(archivedResource.getName()); + String encodedDataString = Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + resourceData.data(encodedDataString); + + return eloClient.createArchivedResource(resourceData) + .onFailure() + .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while archiving scl resource '%s' in ELO!", uuid))) + .onItem() + .invoke(item -> + LOGGER.debug("returned archived scl item {} ", item) + ); + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index 9ce6204a..9d6a322a 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -1,25 +1,36 @@ package org.lfenergy.compas.scl.data.service; +import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; +import org.lfenergy.compas.scl.data.dto.ResourceTag; +import org.lfenergy.compas.scl.data.dto.TypeEnum; import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; import java.io.*; +import java.util.List; import java.util.UUID; @ApplicationScoped public class CompasSclDataArchivingServiceImpl implements ICompasSclDataArchivingService { - private final String DEFAULT_PATH = System.getProperty("user.dir") + File.separator + "locations"; + private static final Logger LOGGER = LogManager.getLogger(CompasSclDataArchivingServiceImpl.class); + @ConfigProperty(name = "scl-data-service.archiving.filesystem.location", defaultValue = "/work/locations") + String locationPath; @Override public void createLocation(ILocationMetaItem location) { - File newLocationDirectory = new File(DEFAULT_PATH + File.separator + location.getName()); + LOGGER.info("locationPath: {}", locationPath); + File newLocationDirectory = new File(locationPath + File.separator + location.getName()); newLocationDirectory.mkdirs(); } @Override - public void archiveData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { + public Uni archiveData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName) + File.separator + "referenced_resources"; File locationDir = new File(absolutePath); locationDir.mkdirs(); @@ -29,24 +40,28 @@ public void archiveData(String locationName, String filename, UUID resourceId, F fos.write(fis.readAllBytes()); } } catch (IOException e) { - throw new RuntimeException(e); + return Uni.createFrom().failure(new RuntimeException(e)); } + List archivedResourceTag = archivedResource.getFields().stream().map(field -> new ResourceTag(field.getKey(), field.getValue())).toList(); + return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationName, archivedResourceTag)); } @Override - public void archiveSclData(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) { + public Uni archiveSclData(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) { String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName); File locationDir = new File(absolutePath); locationDir.mkdirs(); - File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getContentType().toLowerCase()); + File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getType().toLowerCase()); try (FileWriter fw = new FileWriter(f)) { fw.write(data); } catch (IOException e) { throw new RuntimeException(e); } + List archivedResourceTag = archivedResource.getFields().stream().map(field -> new ResourceTag(field.getKey(), field.getValue())).toList(); + return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationName, archivedResourceTag)); } private String generateSclDataLocation(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName) { - return DEFAULT_PATH + File.separator + locationName + File.separator + resourceId + File.separator + archivedResource.getVersion(); + return locationPath + File.separator + locationName + File.separator + resourceId + File.separator + archivedResource.getVersion(); } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index b48b7c4a..50ae49c2 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -3,11 +3,20 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.service; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.unchecked.Unchecked; +import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; import jakarta.transaction.Transactional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.lfenergy.compas.core.commons.ElementConverter; import org.lfenergy.compas.core.commons.exception.CompasException; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; @@ -28,8 +37,7 @@ import java.util.*; import java.util.stream.Collectors; -import static jakarta.transaction.Transactional.TxType.REQUIRED; -import static jakarta.transaction.Transactional.TxType.SUPPORTS; +import static jakarta.transaction.Transactional.TxType.*; import static org.lfenergy.compas.scl.data.SclDataServiceConstants.*; import static org.lfenergy.compas.scl.data.exception.CompasSclDataServiceErrorCode.*; import static org.lfenergy.compas.scl.extensions.commons.CompasExtensionsConstants.*; @@ -44,15 +52,37 @@ public class CompasSclDataService { private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; - private final ICompasSclDataArchivingService archivingService; + private ICompasSclDataArchivingService archivingService; + private static final Logger LOGGER = LogManager.getLogger(CompasSclDataService.class); + @ConfigProperty(name = "scl-data-service.archiving.connector.enabled", defaultValue = "false") + String isEloEnabled; + + @Inject + CompasSclDataArchivingServiceImpl fileSystemArchivingService; + + @Inject + CompasSclDataArchivingEloServiceImpl eloArchivingService; + + @Inject + TransactionManager tm; @Inject public CompasSclDataService(CompasSclDataRepository repository, ElementConverter converter, - SclElementProcessor sclElementProcessor, ICompasSclDataArchivingService archivingService) { + SclElementProcessor sclElementProcessor) { this.repository = repository; this.converter = converter; this.sclElementProcessor = sclElementProcessor; - this.archivingService = archivingService; + } + + @PostConstruct + void init() { + if (isEloEnabled.equalsIgnoreCase("true")) { + LOGGER.info("Initializing ELO archiving service"); + this.archivingService = eloArchivingService; + } else { + LOGGER.info("Initializing FileSystem archiving service"); + this.archivingService = fileSystemArchivingService; + } } /** @@ -496,7 +526,10 @@ public List listLocations(int page, int pageSize) { @Transactional(REQUIRED) public ILocationMetaItem createLocation(String key, String name, String description, String author) { if (repository.hasDuplicateLocationValues(key, name)) { - throw new CompasSclDataServiceException(CREATION_ERROR_CODE, "Duplicate location key or name provided!"); + String errorMessage = String.format("Unable to create location, duplicate location key '%s' or name '%s' provided!", key, name); + LOGGER.warn(errorMessage); + throw new CompasSclDataServiceException(CREATION_ERROR_CODE, + errorMessage); } ILocationMetaItem createdLocation = repository.createLocation(key, name, description); repository.addLocationTags(createdLocation, author); @@ -514,8 +547,10 @@ public void deleteLocation(UUID id) { ILocationMetaItem locationToDelete = repository.findLocationByUUID(id); int assignedResourceCount = locationToDelete.getAssignedResources(); if (assignedResourceCount > 0) { + String errorMessage = String.format("Deletion of Location '%s' not allowed, unassign resources before deletion", id); + LOGGER.warn(errorMessage); throw new CompasSclDataServiceException(LOCATION_DELETION_NOT_ALLOWED_ERROR_CODE, - String.format("Deletion of Location '%s' not allowed, unassign resources before deletion", id)); + errorMessage); } repository.deleteLocationTags(locationToDelete); @@ -546,7 +581,9 @@ public ILocationMetaItem updateLocation(UUID id, String key, String name, String public void assignResourceToLocation(UUID locationId, UUID resourceId) { ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); if (repository.listHistory(resourceId).isEmpty()) { - throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", resourceId)); + String errorMessage = String.format("Unable to assign resource '%s' to location '%s', cannot find resource.", resourceId, locationId); + LOGGER.warn(errorMessage); + throw new CompasNoDataFoundException(errorMessage); } if (locationItem != null) { repository.assignResourceToLocation(locationId, resourceId); @@ -563,7 +600,9 @@ public void assignResourceToLocation(UUID locationId, UUID resourceId) { public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); if (repository.listHistory(resourceId).isEmpty()) { - throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", resourceId)); + String errorMessage = String.format("Unable to unassign resource '%s' from location '%s', cannot find resource.", resourceId, locationId); + LOGGER.warn(errorMessage); + throw new CompasNoDataFoundException(errorMessage); } if (locationItem != null) { repository.unassignResourceFromLocation(locationId, resourceId); @@ -581,21 +620,33 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { * @param body The content of the resource * @return The created archived resource item */ - @Transactional(REQUIRED) - public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version, String author, String approver, String contentType, String filename, File body) { + @Transactional(REQUIRES_NEW) + public Uni archiveResource(UUID id, String version, String author, String approver, String contentType, String filename, File body) { List historyItem = repository.listHistoryVersionsByUUID(id); if (!historyItem.isEmpty() && historyItem.get(0).getLocation() == null) { - throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, - String.format("Unable to archive file '%s' for scl resource '%s' with version %s, no location assigned!", filename, id, version)); + String errorMessage = String.format("Unable to archive file '%s' for scl resource '%s' with version %s, no location assigned!", filename, id, version); + LOGGER.warn(errorMessage); + return Uni.createFrom().failure(new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, + errorMessage)); } else if (historyItem.isEmpty() || historyItem.stream().noneMatch(hi -> hi.getVersion().equals(version))) { - throw new CompasNoDataFoundException( - String.format("Unable to archive file '%s' for scl resource '%s' with version %s, unable to find scl resource '%s' with version %s!", filename, id, version, id, version)); + String errorMessage = String.format("Unable to archive file '%s' for scl resource '%s' with version %s, unable to find scl resource '%s' with version %s!", filename, id, version, id, version); + LOGGER.warn(errorMessage); + return Uni.createFrom().failure(new CompasNoDataFoundException( + errorMessage)); } - IAbstractArchivedResourceMetaItem archivedResource = repository.archiveResource(id, new Version(version), author, approver, contentType, filename); - if (body != null) { - archivingService.archiveData(repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), filename, id, body, archivedResource); - } - return archivedResource; + return Uni.createFrom() + .item(() -> repository.archiveResource(id, new Version(version), author, approver, contentType, filename)) + .onItem() + .ifNotNull() + .call(item -> + storeResourceData(archivingService.archiveData( + repository.findLocationByUUID(UUID.fromString(item.getLocationId())).getName(), + filename, + id, + body, + item + ), "Error while archiving resource: {}") + ); } /** @@ -606,19 +657,48 @@ public IAbstractArchivedResourceMetaItem archiveResource(UUID id, String version * @param approver The approver of the archiving action * @return The archived resource item */ - @Transactional(REQUIRED) - public IAbstractArchivedResourceMetaItem archiveSclResource(UUID id, Version version, String approver) { + @Transactional(REQUIRES_NEW) + public Uni archiveSclResource(UUID id, Version version, String approver) { List historyItem = repository.listHistory(id); if (!historyItem.isEmpty() && historyItem.get(0).getLocation() == null) { + String errorMessage = String.format("Unable to archive scl file '%s' with version %s, no location assigned!", id, version); + LOGGER.warn(errorMessage); throw new CompasSclDataServiceException(NO_LOCATION_ASSIGNED_TO_SCL_DATA_ERROR_CODE, - String.format("Unable to archive scl file '%s' with version %s, no location assigned!", id, version)); + errorMessage); } - IAbstractArchivedResourceMetaItem archivedResource = repository.archiveSclResource(id, version, approver); - String data = repository.findByUUID(id, version); - if (data != null) { - archivingService.archiveSclData(id, archivedResource, repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), data); + + if(!repository.listHistoryVersionsByUUID(id).isEmpty() && + repository.listHistoryVersionsByUUID(id).stream().anyMatch(hi -> hi.getVersion().equals(version.toString()) && hi.isArchived())) { + return Uni.createFrom().failure(new CompasSclDataServiceException( + RESOURCE_ALREADY_ARCHIVED, + String.format("Unable to archive version %s of scl resource '%s' because it is already archived.", version, id))); } - return archivedResource; + + return Uni.createFrom() + .item(() -> repository.archiveSclResource(id, version, approver)) + .onItem() + .ifNotNull() + .call(archivedResource -> + storeResourceData(archivingService.archiveSclData( + id, + archivedResource, + repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), + repository.findByUUID(id, version) + ), "Error while archiving scl resource: {}") + ); + } + + private Uni storeResourceData(Uni archivingRequest, String errorMessage) { + return archivingRequest + .onFailure() + .invoke(Unchecked.consumer(throwable -> { + LOGGER.warn(errorMessage, throwable.getMessage()); + try { + tm.setRollbackOnly(); + } catch (SystemException e) { + throw new RuntimeException(e); + } + })); } /** diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java index 762ea8fd..40711fbb 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -1,5 +1,7 @@ package org.lfenergy.compas.scl.data.service; +import io.smallrye.mutiny.Uni; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; @@ -10,7 +12,7 @@ public interface ICompasSclDataArchivingService { void createLocation(ILocationMetaItem location); - void archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); + Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); - void archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data); + Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data); } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java new file mode 100644 index 00000000..6316c598 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java @@ -0,0 +1,37 @@ +package org.lfenergy.compas.scl.data.service; + +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.lfenergy.compas.scl.data.dto.ResourceData; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; + +import java.util.List; +import java.util.UUID; + +@ApplicationScoped +//@OidcClientFilter("jwt-secret") +@RegisterRestClient(configKey = "elo-connector-client") +public interface IEloConnectorRestClient { + @POST + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + @Path("/archiving") + Uni createArchivedResource(@Valid @NotNull ResourceData resourceData); + + + @GET + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + @Path("/resources/projects/{project}") + Uni> retrieveAllProjectResources(@PathParam("project") UUID project); + + @GET + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + @Path("/resources/{resourceUuid}") + Uni retrieveArchivedResource(@PathParam("resourceUuid") String resourceUuid); +} diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index dd6bdb13..7c9db800 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -6,11 +6,15 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.data.service; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.lfenergy.compas.core.commons.ElementConverter; import org.lfenergy.compas.core.commons.exception.CompasException; +import org.lfenergy.compas.scl.data.dto.ResourceMetaData; +import org.lfenergy.compas.scl.data.dto.TypeEnum; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.*; @@ -24,6 +28,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -46,14 +51,20 @@ class CompasSclDataServiceTest { @Mock private ICompasSclDataArchivingService compasSclDataArchivingService; + @Mock + private CompasSclDataArchivingServiceImpl compasSclDataArchivingServiceImpl; + private CompasSclDataService compasSclDataService; private final ElementConverter converter = new ElementConverter(); private final SclElementProcessor processor = new SclElementProcessor(); @BeforeEach - void beforeEach() { - compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor, compasSclDataArchivingService); + void beforeEach() throws NoSuchFieldException, IllegalAccessException { + compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor); + Field archivingServiceField = compasSclDataService.getClass().getDeclaredField("archivingService"); + archivingServiceField.setAccessible(true); + archivingServiceField.set(compasSclDataService, compasSclDataArchivingServiceImpl); } @Test @@ -534,10 +545,11 @@ void listLocations_WhenCalledWithPageAndPageSize_ThenReturnLocations() { void createLocation_WhenCalled_ThenReturnCreatedLocation() { ILocationMetaItem expectedLocation = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey", "locationName", null, 0); when(compasSclDataRepository.createLocation("locationKey", "locationName", null)).thenReturn(expectedLocation); - + when(compasSclDataRepository.hasDuplicateLocationValues("locationKey", "locationName")).thenReturn(false); ILocationMetaItem actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null, "testUser"); - verify(compasSclDataArchivingService).createLocation(expectedLocation); + verify(compasSclDataRepository, times(1)).createLocation(any(), any(), any()); + verify(compasSclDataArchivingServiceImpl, times(1)).createLocation(any()); assertEquals(expectedLocation, actualLocation); } @@ -733,10 +745,19 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { ) ); - compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file); + when(compasSclDataArchivingServiceImpl.archiveData(any(), any(), any(), any(), any())).thenReturn(Uni.createFrom().item( + new ResourceMetaData( + TypeEnum.RESOURCE, + UUID.randomUUID(), + locationId.toString(), + List.of() + ) + )); + + UniAssertSubscriber result = compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, file).subscribe().withSubscriber(UniAssertSubscriber.create()); + result.assertCompleted(); verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); - verify(compasSclDataArchivingService, times(1)).archiveData(any(), any(), any(), any(), any()); } catch (IOException e) { throw new RuntimeException(e); } finally { @@ -766,7 +787,17 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { false ); + UUID locationId = UUID.randomUUID(); when(compasSclDataRepository.listHistoryVersionsByUUID(resourceId)).thenReturn(List.of(historyMetaItem)); + when(compasSclDataRepository.findLocationByUUID(UUID.fromString(locationId.toString()))).thenReturn( + new LocationMetaItem( + locationId.toString(), + "someKey", + "someName", + "someDescription", + 1 + ) + ); when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) .thenReturn( new ArchivedSclResourceMetaItem( @@ -777,7 +808,7 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { approver, null, contentType, - "locationName", + locationId.toString(), List.of(), null, OffsetDateTime.now(), @@ -786,7 +817,18 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { ) ); - compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, null); + when(compasSclDataArchivingServiceImpl.archiveData(any(), any(), any(), any(), any())).thenReturn(Uni.createFrom().item( + new ResourceMetaData( + TypeEnum.RESOURCE, + UUID.randomUUID(), + locationId.toString(), + List.of() + ) + )); + + UniAssertSubscriber cut = compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, null).subscribe().withSubscriber(UniAssertSubscriber.create()); + + cut.assertCompleted(); verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); verify(compasSclDataArchivingService, times(0)).archiveSclData(any(), any(), any(), any()); @@ -827,11 +869,22 @@ void archiveSclResource_WhenCalled_ThenResourceIsArchived() { ) ); - compasSclDataService.archiveSclResource(resourceId, new Version(version), approver); + when(compasSclDataArchivingServiceImpl.archiveSclData(any(), any(), any(), any())).thenReturn(Uni.createFrom().item( + new ResourceMetaData( + TypeEnum.RESOURCE, + UUID.randomUUID(), + locationId.toString(), + List.of() + ) + )); + + UniAssertSubscriber result = compasSclDataService.archiveSclResource(resourceId, new Version(version), approver).subscribe().withSubscriber(UniAssertSubscriber.create()); + + result.assertCompleted(); verify(compasSclDataRepository, times(1)).archiveSclResource(resourceId, new Version(version), approver); verify(compasSclDataRepository, times(1)).findByUUID(resourceId, new Version(version)); - verify(compasSclDataArchivingService, times(1)).archiveSclData(resourceId, archivedResource, "locationName", sclData); + verify(compasSclDataArchivingServiceImpl, times(1)).archiveSclData(resourceId, archivedResource, "locationName", sclData); } @Test From 91d3b167f89744a1daa96eb6052d52158e42d4f5 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 20 Feb 2025 07:50:15 +0100 Subject: [PATCH 16/23] chore: add elo connector and wiremock to docker compose file --- .../__files/error_response.json | 4 + _dev/wiremock_resources/__files/response.json | 93 +++++++++++++++++++ .../wiremock_resources/mappings/mappings.json | 14 +++ docker-compose.yml | 42 +++++++++ 4 files changed, 153 insertions(+) create mode 100644 _dev/wiremock_resources/__files/error_response.json create mode 100644 _dev/wiremock_resources/__files/response.json create mode 100644 _dev/wiremock_resources/mappings/mappings.json diff --git a/_dev/wiremock_resources/__files/error_response.json b/_dev/wiremock_resources/__files/error_response.json new file mode 100644 index 00000000..0c5b1c1b --- /dev/null +++ b/_dev/wiremock_resources/__files/error_response.json @@ -0,0 +1,4 @@ +{ + "result": {}, + "exception": "Error while creating doc '8fae399c-caa3-4021-8138-fd3eef3b2b1a' in ELO!" +} \ No newline at end of file diff --git a/_dev/wiremock_resources/__files/response.json b/_dev/wiremock_resources/__files/response.json new file mode 100644 index 00000000..04bb9b1a --- /dev/null +++ b/_dev/wiremock_resources/__files/response.json @@ -0,0 +1,93 @@ +{ + "result": { + "docs": [ + { + "preview": null, + "pathId2": 0, + "previewUrl": "string", + "encryptionSet": 0, + "language": "string", + "pathId": 0, + "ownerId": 0, + "relativeFilePath": "string", + "sig": null, + "accessDateIso": "string", + "ownerName": "string", + "tStamp": "string", + "physPath": "string", + "id": 123456789, + "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "deleteDateIso": "string", + "ext": "docx", + "uploadResult": "string", + "tStampSync": "string", + "fileData": { + "data": "VGVzdDEyMzQ=", + "stream": { + "streamId": "string", + "url": "string" + }, + "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }, + "workVersion": true, + "version": "1.0.0", + "url": "string", + "updateDateIso": "string", + "deleted": false, + "milestone": false, + "size": "string", + "nbOfValidSignatures": 0, + "createDateIso": "string", + "fulltextContent": null, + "guid": "string", + "comment": "string", + "md5": "string" + } + ], + "objId": "8fae399c-caa3-4021-8138-fd3eef3b2b1a", + "atts": [ + { + "preview": null, + "pathId2": 0, + "previewUrl": "string", + "encryptionSet": 0, + "language": "string", + "pathId": 0, + "ownerId": 0, + "relativeFilePath": "string", + "sig": null, + "accessDateIso": "string", + "ownerName": "string", + "tStamp": "string", + "physPath": "string", + "id": 0, + "contentType": "string", + "deleteDateIso": "string", + "ext": "string", + "uploadResult": "string", + "tStampSync": "string", + "fileData": { + "data": "string", + "stream": { + "streamId": "string", + "url": "string" + }, + "contentType": "string" + }, + "workVersion": true, + "version": "string", + "url": "string", + "updateDateIso": "string", + "deleted": true, + "milestone": true, + "size": "string", + "nbOfValidSignatures": 0, + "createDateIso": "string", + "fulltextContent": null, + "guid": "string", + "comment": "string", + "md5": "string" + } + ] + } +} \ No newline at end of file diff --git a/_dev/wiremock_resources/mappings/mappings.json b/_dev/wiremock_resources/mappings/mappings.json new file mode 100644 index 00000000..527aca83 --- /dev/null +++ b/_dev/wiremock_resources/mappings/mappings.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "POST", + "urlPattern": "/IXServicePortIF/checkinDocEnd\\?docId=.*" + }, + + "response": { + "status": 200, + "bodyFileName": "../__files/response.json", + "headers": { + "Content-Type": "application/json" + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9f3d2937..4d7a1684 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: container_name: compas-scl-data-service depends_on: - postgres + - transnetbw-elo-connector ports: - "8080:8080" environment: @@ -49,10 +50,51 @@ services: USERINFO_WHO_CLAIMNAME: name USERINFO_SESSION_WARNING: 20 USERINFO_SESSION_EXPIRES: 30 + # ELO Connector + ELO_CONNECTOR_ENABLED: true + ELO_CONNECTOR_BASE_URL: http://elo-connector:8080/compas-elo-connector/api restart: on-failure # will restart until it's success networks: - compas-network + transnetbw-elo-connector: + image: transnetbw/compas-elo-connector:1.0.0-SNAPSHOT + container_name: elo-connector + depends_on: + - keycloak + ports: + - "8181:8080" + environment: + # JWT Security Variables + JWT_VERIFY_KEY: http://localhost:8089/auth/realms/compas/protocol/openid-connect/certs + JWT_VERIFY_ISSUER: http://localhost:8089/auth/realms/compas + JWT_VERIFY_CLIENT_ID: scl-data-service + JWT_GROUPS_PATH: resource_access/scl-data-service/roles + # UserInfo variables + USERINFO_NAME_CLAIMNAME: name + USERINFO_WHO_CLAIMNAME: name + USERINFO_SESSION_WARNING: 20 + USERINFO_SESSION_EXPIRES: 30 + restart: on-failure + networks: + - compas-network + + wiremock: + image: "wiremock/wiremock:3.10.0-alpine" + container_name: wiremock-dev + ports: + - "48080:8080" + volumes: + - ./_dev/wiremock_resources/mappings:/home/wiremock/mappings + - ./_dev/wiremock_resources/__files:/home/wiremock/__files + entrypoint: [ + "/docker-entrypoint.sh", + "--global-response-templating", + "--disable-gzip" + ] + networks: + - compas-network + networks: compas-network: driver: bridge From 4e08ef87614b621d7665b7f3f46e0eb0094e527c Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 20 Feb 2025 08:19:25 +0100 Subject: [PATCH 17/23] chore: update elo tags for resources and locations --- .../rest/api/locations/LocationsResource.java | 3 +- app/src/main/resources/application.properties | 42 ++++++++++--------- .../api/locations/LocationsResourceTest.java | 2 +- .../CompasSclDataPostgreSQLRepository.java | 29 ++++++++----- .../repository/CompasSclDataRepository.java | 3 +- .../data/service/CompasSclDataService.java | 4 +- .../service/CompasSclDataServiceTest.java | 2 +- 7 files changed, 48 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java index 5d87caaa..0aa88aa8 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -45,8 +45,7 @@ public Uni createLocation(Location location) { .item(() -> compasSclDataService.createLocation( location.getKey(), location.getName(), - location.getDescription(), - jsonWebToken.getClaim(userInfoProperties.name()) + location.getDescription() )) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index b7468034..849e1a5f 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -264,25 +264,29 @@ quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.paths=/compas quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.methods=GET quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.policy=ARCHIVE_API_READ -quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.paths=/compas-scl-data-service/api/resources/* -quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.methods=POST -quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE - -quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.paths=/compas-scl-data-service/api/locations/* -quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.methods=GET -quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.policy=ARCHIVE_API_GET - -quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.paths=/compas-scl-data-service/api/locations/* -quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.methods=POST -quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE - -quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.paths=/compas-scl-data-service/api/locations/* -quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.methods=PUT -quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.policy=ARCHIVE_API_UPDATE - -quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.paths=/compas-scl-data-service/api/locations/* -quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.methods=DELETE -quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.policy=ARCHIVE_API_DELETE +#quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.paths=/compas-scl-data-service/api/resources/* +#quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.methods=GET +#quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.policy=ARCHIVE_API_READ +# +#quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.paths=/compas-scl-data-service/api/resources/* +#quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.methods=POST +#quarkus.http.auth.permission.ARCHIVE_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE +# +#quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.paths=/compas-scl-data-service/api/locations/* +#quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.methods=GET +#quarkus.http.auth.permission.LOCATIONS_API_GET_RESOURCE.policy=ARCHIVE_API_GET +# +#quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.paths=/compas-scl-data-service/api/locations/* +#quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.methods=POST +#quarkus.http.auth.permission.LOCATIONS_API_POST_RESOURCE.policy=ARCHIVE_API_CREATE +# +#quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.paths=/compas-scl-data-service/api/locations/* +#quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.methods=PUT +#quarkus.http.auth.permission.LOCATIONS_API_PUT_RESOURCE.policy=ARCHIVE_API_UPDATE +# +#quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.paths=/compas-scl-data-service/api/locations/* +#quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.methods=DELETE +#quarkus.http.auth.permission.LOCATIONS_API_DELETE_RESOURCE.policy=ARCHIVE_API_DELETE quarkus.http.auth.permission.api.paths=/compas-scl-data-service/api/* quarkus.http.auth.permission.api.policy=permit diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java index d743e540..4114aca8 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java @@ -48,7 +48,7 @@ void createLocation_WhenCalled_ThenReturnsCreatedLocation() { ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); when(jwt.getClaim("name")).thenReturn("test user"); - when(compasSclDataService.createLocation(key, name, description, "test user")).thenReturn(testData); + when(compasSclDataService.createLocation(key, name, description)).thenReturn(testData); Response response = given() .contentType(MediaType.APPLICATION_JSON) .body(location) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index a2e55a6d..5e9fdb1e 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -710,21 +710,16 @@ public boolean hasDuplicateLocationValues(String key, String name) { } @Override - public void addLocationTags(ILocationMetaItem location, String author) { + public void addLocationTags(ILocationMetaItem location) { String locationName = location.getName(); ResourceTagItem locationNameTag = getResourceTag("LOCATION", locationName); - ResourceTagItem locationAuthorTag = getResourceTag("AUTHOR", author); if (locationNameTag == null) { createResourceTag("LOCATION", locationName); locationNameTag = getResourceTag("LOCATION", locationName); } - if (locationAuthorTag == null) { - createResourceTag("AUTHOR", author); - locationAuthorTag = getResourceTag("AUTHOR", author); - } UUID locationUuid = UUID.fromString(location.getId()); - updateTagMappingForLocation(locationUuid, List.of(locationNameTag, locationAuthorTag)); + updateTagMappingForLocation(locationUuid, List.of(Objects.requireNonNull(locationNameTag))); } @Override @@ -945,7 +940,7 @@ public IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version versio insertRelatedResourceEntry(id, version, author, approver, contentType, filename, assignedResourceId, locationId); String locationName = findLocationByUUID(UUID.fromString(locationId)).getName(); - List resourceTags = generateFields(locationName, id.toString(), author, approver); + List resourceTags = generateFields(locationName, id.toString(), author, approver, filename, version.toString()); ArchivedReferencedResourceMetaItem archivedResourcesMetaItem = new ArchivedReferencedResourceMetaItem( assignedResourceId.toString(), filename, @@ -1100,13 +1095,15 @@ private ArchivedSclResourceMetaItem mapResultSetToArchivedSclResource(String app ); } - private List generateFields(String location, String sourceResourceId, String author, String examiner) { + private List generateFields(String location, String sourceResourceId, String author, String examiner, String name, String version) { List fieldList = new ArrayList<>(); ResourceTagItem locationTag = getResourceTag("LOCATION", location); ResourceTagItem resourceIdTag = getResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); ResourceTagItem authorTag = getResourceTag("AUTHOR", author); ResourceTagItem examinerTag = getResourceTag("EXAMINER", examiner); + ResourceTagItem nameTag = getResourceTag("NAME", name); + ResourceTagItem versionTag = getResourceTag("VERSION", version); if (locationTag == null && location != null) { createResourceTag("LOCATION", location); @@ -1124,11 +1121,21 @@ private List generateFields(String location, String sourceReso createResourceTag("EXAMINER", examiner); examinerTag = getResourceTag("EXAMINER", examiner); } + if (nameTag == null && name != null) { + createResourceTag("NAME", name); + nameTag = getResourceTag("NAME", name); + } + if (versionTag == null && version != null) { + createResourceTag("VERSION", version); + versionTag = getResourceTag("VERSION", version); + } fieldList.add(locationTag); fieldList.add(resourceIdTag); fieldList.add(authorTag); fieldList.add(examinerTag); + fieldList.add(nameTag); + fieldList.add(versionTag); return fieldList.stream().filter(Objects::nonNull).toList(); } @@ -1139,7 +1146,9 @@ private List generateFieldsFromResultSet(ResultSet resultSet, locationName, resultSet.getString(ID_FIELD), resultSet.getString("created_by"), - examiner + examiner, + resultSet.getString(NAME_FIELD), + createVersion(resultSet) ); } diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index dbf3cadf..dcc32f3a 100644 --- a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java @@ -172,9 +172,8 @@ public interface CompasSclDataRepository { * Create tags that identify a Location object * * @param location The Location object that receives the tags - * @param author The name of the author who created the Location */ - void addLocationTags(ILocationMetaItem location, String author); + void addLocationTags(ILocationMetaItem location); /** * Return whether either the key or the name are already used in a location diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 50ae49c2..462449cb 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -524,7 +524,7 @@ public List listLocations(int page, int pageSize) { * @return The created Location entry */ @Transactional(REQUIRED) - public ILocationMetaItem createLocation(String key, String name, String description, String author) { + public ILocationMetaItem createLocation(String key, String name, String description) { if (repository.hasDuplicateLocationValues(key, name)) { String errorMessage = String.format("Unable to create location, duplicate location key '%s' or name '%s' provided!", key, name); LOGGER.warn(errorMessage); @@ -532,7 +532,7 @@ public ILocationMetaItem createLocation(String key, String name, String descript errorMessage); } ILocationMetaItem createdLocation = repository.createLocation(key, name, description); - repository.addLocationTags(createdLocation, author); + repository.addLocationTags(createdLocation); archivingService.createLocation(createdLocation); return createdLocation; } diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 7c9db800..7eb859db 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -546,7 +546,7 @@ void createLocation_WhenCalled_ThenReturnCreatedLocation() { ILocationMetaItem expectedLocation = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey", "locationName", null, 0); when(compasSclDataRepository.createLocation("locationKey", "locationName", null)).thenReturn(expectedLocation); when(compasSclDataRepository.hasDuplicateLocationValues("locationKey", "locationName")).thenReturn(false); - ILocationMetaItem actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null, "testUser"); + ILocationMetaItem actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null); verify(compasSclDataRepository, times(1)).createLocation(any(), any(), any()); verify(compasSclDataArchivingServiceImpl, times(1)).createLocation(any()); From dc9fa97061147453c530bc6d8c7b52ecdb095727 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 24 Feb 2025 14:49:02 +0100 Subject: [PATCH 18/23] chore: remove elo connector setup from docker-compose.yml --- .../__files/error_response.json | 4 - _dev/wiremock_resources/__files/response.json | 93 ------------------- .../wiremock_resources/mappings/mappings.json | 14 --- docker-compose.yml | 40 +------- 4 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 _dev/wiremock_resources/__files/error_response.json delete mode 100644 _dev/wiremock_resources/__files/response.json delete mode 100644 _dev/wiremock_resources/mappings/mappings.json diff --git a/_dev/wiremock_resources/__files/error_response.json b/_dev/wiremock_resources/__files/error_response.json deleted file mode 100644 index 0c5b1c1b..00000000 --- a/_dev/wiremock_resources/__files/error_response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": {}, - "exception": "Error while creating doc '8fae399c-caa3-4021-8138-fd3eef3b2b1a' in ELO!" -} \ No newline at end of file diff --git a/_dev/wiremock_resources/__files/response.json b/_dev/wiremock_resources/__files/response.json deleted file mode 100644 index 04bb9b1a..00000000 --- a/_dev/wiremock_resources/__files/response.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "result": { - "docs": [ - { - "preview": null, - "pathId2": 0, - "previewUrl": "string", - "encryptionSet": 0, - "language": "string", - "pathId": 0, - "ownerId": 0, - "relativeFilePath": "string", - "sig": null, - "accessDateIso": "string", - "ownerName": "string", - "tStamp": "string", - "physPath": "string", - "id": 123456789, - "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "deleteDateIso": "string", - "ext": "docx", - "uploadResult": "string", - "tStampSync": "string", - "fileData": { - "data": "VGVzdDEyMzQ=", - "stream": { - "streamId": "string", - "url": "string" - }, - "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - }, - "workVersion": true, - "version": "1.0.0", - "url": "string", - "updateDateIso": "string", - "deleted": false, - "milestone": false, - "size": "string", - "nbOfValidSignatures": 0, - "createDateIso": "string", - "fulltextContent": null, - "guid": "string", - "comment": "string", - "md5": "string" - } - ], - "objId": "8fae399c-caa3-4021-8138-fd3eef3b2b1a", - "atts": [ - { - "preview": null, - "pathId2": 0, - "previewUrl": "string", - "encryptionSet": 0, - "language": "string", - "pathId": 0, - "ownerId": 0, - "relativeFilePath": "string", - "sig": null, - "accessDateIso": "string", - "ownerName": "string", - "tStamp": "string", - "physPath": "string", - "id": 0, - "contentType": "string", - "deleteDateIso": "string", - "ext": "string", - "uploadResult": "string", - "tStampSync": "string", - "fileData": { - "data": "string", - "stream": { - "streamId": "string", - "url": "string" - }, - "contentType": "string" - }, - "workVersion": true, - "version": "string", - "url": "string", - "updateDateIso": "string", - "deleted": true, - "milestone": true, - "size": "string", - "nbOfValidSignatures": 0, - "createDateIso": "string", - "fulltextContent": null, - "guid": "string", - "comment": "string", - "md5": "string" - } - ] - } -} \ No newline at end of file diff --git a/_dev/wiremock_resources/mappings/mappings.json b/_dev/wiremock_resources/mappings/mappings.json deleted file mode 100644 index 527aca83..00000000 --- a/_dev/wiremock_resources/mappings/mappings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "request": { - "method": "POST", - "urlPattern": "/IXServicePortIF/checkinDocEnd\\?docId=.*" - }, - - "response": { - "status": 200, - "bodyFileName": "../__files/response.json", - "headers": { - "Content-Type": "application/json" - } - } -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4d7a1684..5bb6231d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,6 @@ services: container_name: compas-scl-data-service depends_on: - postgres - - transnetbw-elo-connector ports: - "8080:8080" environment: @@ -57,44 +56,7 @@ services: networks: - compas-network - transnetbw-elo-connector: - image: transnetbw/compas-elo-connector:1.0.0-SNAPSHOT - container_name: elo-connector - depends_on: - - keycloak - ports: - - "8181:8080" - environment: - # JWT Security Variables - JWT_VERIFY_KEY: http://localhost:8089/auth/realms/compas/protocol/openid-connect/certs - JWT_VERIFY_ISSUER: http://localhost:8089/auth/realms/compas - JWT_VERIFY_CLIENT_ID: scl-data-service - JWT_GROUPS_PATH: resource_access/scl-data-service/roles - # UserInfo variables - USERINFO_NAME_CLAIMNAME: name - USERINFO_WHO_CLAIMNAME: name - USERINFO_SESSION_WARNING: 20 - USERINFO_SESSION_EXPIRES: 30 - restart: on-failure - networks: - - compas-network - - wiremock: - image: "wiremock/wiremock:3.10.0-alpine" - container_name: wiremock-dev - ports: - - "48080:8080" - volumes: - - ./_dev/wiremock_resources/mappings:/home/wiremock/mappings - - ./_dev/wiremock_resources/__files:/home/wiremock/__files - entrypoint: [ - "/docker-entrypoint.sh", - "--global-response-templating", - "--disable-gzip" - ] - networks: - - compas-network - networks: compas-network: + name: compas-network driver: bridge From b0f20e9c8c548e2d21880fb90903631791a4fd04 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 24 Feb 2025 14:54:43 +0100 Subject: [PATCH 19/23] feat: implement creating location in elo --- .../data/rest/api/locations/LocationsApi.java | 1 + .../rest/api/locations/LocationsResource.java | 9 +- app/src/main/resources/application.properties | 14 +- .../api/locations/LocationsResourceTest.java | 3 +- .../compas/scl/data/dto/LocationData.java | 117 +++++++++++++ .../compas/scl/data/dto/LocationMetaData.java | 160 ++++++++++++++++++ .../CompasSclDataArchivingEloServiceImpl.java | 52 +++--- .../CompasSclDataArchivingServiceImpl.java | 10 +- .../data/service/CompasSclDataService.java | 24 ++- .../ICompasSclDataArchivingService.java | 3 +- .../data/service/IEloConnectorRestClient.java | 7 + .../service/CompasSclDataServiceTest.java | 2 +- 12 files changed, 351 insertions(+), 51 deletions(-) create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationData.java create mode 100644 service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationMetaData.java diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java index cc720f01..d8d023bb 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java @@ -19,6 +19,7 @@ public interface LocationsApi { Uni assignResourceToLocation(@PathParam("locationId") UUID locationId, @PathParam("uuid") UUID uuid); @POST + @Blocking @Consumes({ "application/json" }) @Produces({ "application/json" }) Uni createLocation(@Valid @NotNull Location location); diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java index 0aa88aa8..916875a9 100644 --- a/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -41,12 +41,11 @@ public Uni assignResourceToLocation(UUID locationId, UUID uuid) { @Override public Uni createLocation(Location location) { LOGGER.info("Creating location '{}'", location.getName()); - return Uni.createFrom() - .item(() -> compasSclDataService.createLocation( + return compasSclDataService.createLocation( location.getKey(), location.getName(), location.getDescription() - )) + ) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() .transform(this::mapToLocation); @@ -90,8 +89,8 @@ public Uni unassignResourceFromLocation(UUID locationId, UUID uuid) { @Override public Uni updateLocation(UUID locationId, Location location) { LOGGER.info("Updating resource '{}'", locationId); - return Uni.createFrom(). - item(() -> compasSclDataService.updateLocation(locationId, location.getKey(), location.getName(), location.getDescription())) + return Uni.createFrom() + .item(() -> compasSclDataService.updateLocation(locationId, location.getKey(), location.getName(), location.getDescription())) .runSubscriptionOn(Infrastructure.getDefaultExecutor()) .onItem() .transform(this::mapToLocation); diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 849e1a5f..78df1eae 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -296,17 +296,7 @@ scl-data-service.features.is-history-enabled=true # Feature flag to enable or disable persistent delete mode scl-data-service.features.keep-deleted-files=true -quarkus.log.category."io.quarkus.restclient".level=DEBUG -#scl-data-service.archiving.filesystem.location=/work/locations +scl-data-service.archiving.filesystem.location=${FILESYSTEM_LOCATION_PATH:/work/locations} scl-data-service.archiving.connector.enabled=${ELO_CONNECTOR_ENABLED:false} -#scl-data-service.archiving.connector.clientId=test -#scl-data-service.archiving.connector.clientSecret=testSecret -quarkus.rest-client.elo-connector-client.url=${ELO_CONNECTOR_BASE_URL:http://elo-connector:8080/compas-elo-connector/api} -#quarkus.tls.trust-all=true - -#quarkus.oidc-client.client-enabled=false -# -#quarkus.oidc-client.jwt-secret.auth-server-url=${JWT_VERIFY_ISSUER:http://localhost:8089/auth/realms/compas} -#quarkus.oidc-client.jwt-secret.client-id=${JWT_VERIFY_CLIENT_ID:scl-data-service} -#quarkus.oidc-client.jwt-secret.credentials.jwt.secret=${JWT_VERIFY_CLIENT_SECRET:AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow} \ No newline at end of file +quarkus.rest-client.elo-connector-client.url=${ELO_CONNECTOR_BASE_URL:http://elo-connector:8080/compas-elo-connector/api} \ No newline at end of file diff --git a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java index 4114aca8..86dfc0e7 100644 --- a/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java @@ -5,6 +5,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.jupiter.api.Test; @@ -48,7 +49,7 @@ void createLocation_WhenCalled_ThenReturnsCreatedLocation() { ILocationMetaItem testData = new LocationResourceTestDataBuilder().setId(uuid.toString()).build(); when(jwt.getClaim("name")).thenReturn("test user"); - when(compasSclDataService.createLocation(key, name, description)).thenReturn(testData); + when(compasSclDataService.createLocation(key, name, description)).thenReturn(Uni.createFrom().item(testData)); Response response = given() .contentType(MediaType.APPLICATION_JSON) .body(location) diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationData.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationData.java new file mode 100644 index 00000000..013956c0 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationData.java @@ -0,0 +1,117 @@ +package org.lfenergy.compas.scl.data.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.util.Objects; + + + +@JsonTypeName("LocationData") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2025-02-20T14:59:23.487447700+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class LocationData { + private String key; + private String name; + private String description; + + /** + **/ + public LocationData key(String key) { + this.key = key; + return this; + } + + + @JsonProperty("key") + @NotNull public String getKey() { + return key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + **/ + public LocationData name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public LocationData description(String description) { + this.description = description; + return this; + } + + + @JsonProperty("description") + public String getDescription() { + return description; + } + + @JsonProperty("description") + public void setDescription(String description) { + this.description = description; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocationData locationData = (LocationData) o; + return Objects.equals(this.key, locationData.key) && + Objects.equals(this.name, locationData.name) && + Objects.equals(this.description, locationData.description); + } + + @Override + public int hashCode() { + return Objects.hash(key, name, description); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class LocationData {\n"); + + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationMetaData.java b/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationMetaData.java new file mode 100644 index 00000000..13bb8b3a --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/dto/LocationMetaData.java @@ -0,0 +1,160 @@ +package org.lfenergy.compas.scl.data.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.constraints.NotNull; + +import java.util.Objects; +import java.util.UUID; + + + +@JsonTypeName("LocationMetaData") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2025-02-20T14:59:23.487447700+01:00[Europe/Vienna]", comments = "Generator version: 7.8.0") +public class LocationMetaData { + private UUID uuid; + private String key; + private String name; + private String description; + private Integer assignedResources; + + /** + **/ + public LocationMetaData uuid(UUID uuid) { + this.uuid = uuid; + return this; + } + + + @JsonProperty("uuid") + @NotNull public UUID getUuid() { + return uuid; + } + + @JsonProperty("uuid") + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + /** + **/ + public LocationMetaData key(String key) { + this.key = key; + return this; + } + + + @JsonProperty("key") + @NotNull public String getKey() { + return key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + **/ + public LocationMetaData name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public LocationMetaData description(String description) { + this.description = description; + return this; + } + + + @JsonProperty("description") + @NotNull public String getDescription() { + return description; + } + + @JsonProperty("description") + public void setDescription(String description) { + this.description = description; + } + + /** + **/ + public LocationMetaData assignedResources(Integer assignedResources) { + this.assignedResources = assignedResources; + return this; + } + + + @JsonProperty("assignedResources") + @NotNull public Integer getAssignedResources() { + return assignedResources; + } + + @JsonProperty("assignedResources") + public void setAssignedResources(Integer assignedResources) { + this.assignedResources = assignedResources; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocationMetaData locationMetaData = (LocationMetaData) o; + return Objects.equals(this.uuid, locationMetaData.uuid) && + Objects.equals(this.key, locationMetaData.key) && + Objects.equals(this.name, locationMetaData.name) && + Objects.equals(this.description, locationMetaData.description) && + Objects.equals(this.assignedResources, locationMetaData.assignedResources); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, key, name, description, assignedResources); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class LocationMetaData {\n"); + + sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" assignedResources: ").append(toIndentedString(assignedResources)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java index 6095b4be..6a96ebdc 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java @@ -7,10 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.microprofile.rest.client.inject.RestClient; -import org.lfenergy.compas.scl.data.dto.ResourceData; -import org.lfenergy.compas.scl.data.dto.ResourceMetaData; -import org.lfenergy.compas.scl.data.dto.ResourceTag; -import org.lfenergy.compas.scl.data.dto.TypeEnum; +import org.lfenergy.compas.scl.data.dto.*; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; @@ -34,23 +31,34 @@ public class CompasSclDataArchivingEloServiceImpl implements ICompasSclDataArchi IEloConnectorRestClient eloClient; @Override - public void createLocation(ILocationMetaItem location) { - LOGGER.info("Creating a new location in ELO!"); + public Uni createLocation(ILocationMetaItem location) { + LOGGER.debug("Creating a new location in ELO!"); + + LocationData locationData = new LocationData() + .name(location.getName()) + .key(location.getKey()) + .description(location.getDescription()); + return eloClient.createLocation(locationData) + .onFailure() + .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while creating location '%s' in ELO: %s", location.getId(), throwable.getMessage()))) + .onItem() + .invoke(eloLocation -> + LOGGER.debug("returned created ELO location item {} ", eloLocation)); } @Override public Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource) { LOGGER.debug("Archiving related resource in ELO!"); - ResourceData resourceData = new ResourceData(); String extension = archivedResource.getName().substring(archivedResource.getName().lastIndexOf(".") + 1).toLowerCase(); String contentType = archivedResource.getContentType(); - generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); - resourceData.uuid(uuid); - resourceData.type(TypeEnum.RESOURCE); - resourceData.location(locationName); String name = archivedResource.getName().substring(0, archivedResource.getName().lastIndexOf(".")); - resourceData.name(name); + ResourceData resourceData = new ResourceData(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(UUID.fromString(archivedResource.getId())) + .type(TypeEnum.RESOURCE) + .location(locationName) + .name(name); try (FileInputStream fis = new FileInputStream(body)) { byte[] fileData = new byte[(int) body.length()]; @@ -69,9 +77,9 @@ public Uni archiveData(String locationName, String filename, U } private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archivedResource, ResourceData resourceData, String extension, String contentType) { - resourceData.version(archivedResource.getVersion()); - resourceData.extension(extension); - resourceData.contentType(contentType); + resourceData.version(archivedResource.getVersion()) + .extension(extension) + .contentType(contentType); archivedResource.getFields().stream() .filter(field -> field.getValue() != null) .map(field -> @@ -85,16 +93,16 @@ private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archi public Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) throws CompasSclDataServiceException { LOGGER.debug("Archiving scl resource in ELO!"); - ResourceData resourceData = new ResourceData(); String extension = archivedResource.getType(); String contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE.getType(); - generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); - resourceData.uuid(uuid); - resourceData.type(TypeEnum.RESOURCE); - resourceData.location(locationName); - resourceData.name(archivedResource.getName()); String encodedDataString = Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - resourceData.data(encodedDataString); + ResourceData resourceData = new ResourceData(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(uuid) + .type(TypeEnum.RESOURCE) + .location(locationName) + .name(archivedResource.getName()) + .data(encodedDataString); return eloClient.createArchivedResource(resourceData) .onFailure() diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index 9d6a322a..02ef7bd9 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.dto.ResourceTag; import org.lfenergy.compas.scl.data.dto.TypeEnum; @@ -23,10 +24,17 @@ public class CompasSclDataArchivingServiceImpl implements ICompasSclDataArchivin String locationPath; @Override - public void createLocation(ILocationMetaItem location) { + public Uni createLocation(ILocationMetaItem location) { LOGGER.info("locationPath: {}", locationPath); File newLocationDirectory = new File(locationPath + File.separator + location.getName()); newLocationDirectory.mkdirs(); + return Uni.createFrom() + .item(new LocationMetaData() + .uuid(UUID.fromString(location.getId())) + .key(location.getKey()) + .name(location.getName()) + .description(location.getDescription()) + .assignedResources(location.getAssignedResources())); } @Override diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index 462449cb..bb498d1d 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -524,17 +524,25 @@ public List listLocations(int page, int pageSize) { * @return The created Location entry */ @Transactional(REQUIRED) - public ILocationMetaItem createLocation(String key, String name, String description) { + public Uni createLocation(String key, String name, String description) { if (repository.hasDuplicateLocationValues(key, name)) { String errorMessage = String.format("Unable to create location, duplicate location key '%s' or name '%s' provided!", key, name); LOGGER.warn(errorMessage); - throw new CompasSclDataServiceException(CREATION_ERROR_CODE, - errorMessage); + return Uni.createFrom().failure(new CompasSclDataServiceException(CREATION_ERROR_CODE, errorMessage)); } - ILocationMetaItem createdLocation = repository.createLocation(key, name, description); - repository.addLocationTags(createdLocation); - archivingService.createLocation(createdLocation); - return createdLocation; + return Uni.createFrom() + .item(repository.createLocation(key, name, description)) + .invoke(repository::addLocationTags) + .call(createdLocation -> archivingService.createLocation(createdLocation) + .onFailure() + .invoke(Unchecked.consumer(throwable -> { + LOGGER.warn("Error while creating location: {}", throwable.getMessage()); + try { + tm.setRollbackOnly(); + } catch (SystemException e) { + throw new RuntimeException(e); + } + }))); } /** @@ -667,7 +675,7 @@ public Uni archiveSclResource(UUID id, Versio errorMessage); } - if(!repository.listHistoryVersionsByUUID(id).isEmpty() && + if (!repository.listHistoryVersionsByUUID(id).isEmpty() && repository.listHistoryVersionsByUUID(id).stream().anyMatch(hi -> hi.getVersion().equals(version.toString()) && hi.isArchived())) { return Uni.createFrom().failure(new CompasSclDataServiceException( RESOURCE_ALREADY_ARCHIVED, diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java index 40711fbb..2ac69577 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -1,6 +1,7 @@ package org.lfenergy.compas.scl.data.service; import io.smallrye.mutiny.Uni; +import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.model.IAbstractArchivedResourceMetaItem; import org.lfenergy.compas.scl.data.model.ILocationMetaItem; @@ -10,7 +11,7 @@ public interface ICompasSclDataArchivingService { - void createLocation(ILocationMetaItem location); + Uni createLocation(ILocationMetaItem location); Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java index 6316c598..16d6ec0c 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java @@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.*; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.lfenergy.compas.scl.data.dto.LocationData; +import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; @@ -22,6 +24,11 @@ public interface IEloConnectorRestClient { @Path("/archiving") Uni createArchivedResource(@Valid @NotNull ResourceData resourceData); + @POST + @Path("/location") + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + Uni createLocation(@Valid @NotNull LocationData locationData); @GET @Consumes({ "application/json" }) diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 7eb859db..82d6c6cd 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -546,7 +546,7 @@ void createLocation_WhenCalled_ThenReturnCreatedLocation() { ILocationMetaItem expectedLocation = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey", "locationName", null, 0); when(compasSclDataRepository.createLocation("locationKey", "locationName", null)).thenReturn(expectedLocation); when(compasSclDataRepository.hasDuplicateLocationValues("locationKey", "locationName")).thenReturn(false); - ILocationMetaItem actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null); + Uni actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null); verify(compasSclDataRepository, times(1)).createLocation(any(), any(), any()); verify(compasSclDataArchivingServiceImpl, times(1)).createLocation(any()); From 86d828473e9e9175d469e84b24ac45749ba5d781 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Thu, 27 Feb 2025 16:49:12 +0100 Subject: [PATCH 20/23] chore: move location creation to when resource is archived --- app/src/main/resources/application.properties | 4 -- .../CompasSclDataArchivingEloServiceImpl.java | 59 ++++++++++----- .../CompasSclDataArchivingServiceImpl.java | 9 ++- .../data/service/CompasSclDataService.java | 71 ++++++++++++------- 4 files changed, 92 insertions(+), 51 deletions(-) diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 78df1eae..bb7de6bd 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -260,10 +260,6 @@ quarkus.http.auth.permission.STD_CREATE_POST_WS.policy=STD_CREATE quarkus.http.auth.permission.STD_UPDATE_PUT_WS.paths=/compas-scl-data-service/scl-ws/v1/STD/update quarkus.http.auth.permission.STD_UPDATE_PUT_WS.policy=STD_UPDATE -quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.paths=/compas-scl-data-service/api/resources/* -quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.methods=GET -quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.policy=ARCHIVE_API_READ - #quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.paths=/compas-scl-data-service/api/resources/* #quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.methods=GET #quarkus.http.auth.permission.ARCHIVE_API_READ_GET_RESOURCE_VERSION.policy=ARCHIVE_API_READ diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java index 6a96ebdc..4cd2b55a 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java @@ -1,6 +1,10 @@ package org.lfenergy.compas.scl.data.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.unchecked.Unchecked; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.core.MediaType; @@ -25,6 +29,7 @@ public class CompasSclDataArchivingEloServiceImpl implements ICompasSclDataArchivingService { private static final Logger LOGGER = LogManager.getLogger(CompasSclDataArchivingEloServiceImpl.class); + private final ObjectMapper objectMapper = new ObjectMapper(); @Inject @RestClient @@ -32,8 +37,7 @@ public class CompasSclDataArchivingEloServiceImpl implements ICompasSclDataArchi @Override public Uni createLocation(ILocationMetaItem location) { - LOGGER.debug("Creating a new location in ELO!"); - + LOGGER.debug("Creating a new location '{}' in ELO!", location.getId()); LocationData locationData = new LocationData() .name(location.getName()) .key(location.getKey()) @@ -42,13 +46,19 @@ public Uni createLocation(ILocationMetaItem location) { .onFailure() .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while creating location '%s' in ELO: %s", location.getId(), throwable.getMessage()))) .onItem() - .invoke(eloLocation -> - LOGGER.debug("returned created ELO location item {} ", eloLocation)); + .invoke(Unchecked.consumer(eloLocation -> + { + try { + LOGGER.debug("returned ELO location item {}", objectMapper.writeValueAsString(eloLocation)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + })); } @Override public Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource) { - LOGGER.debug("Archiving related resource in ELO!"); + LOGGER.debug("Archiving related resource '{}' in ELO!", archivedResource.getId()); String extension = archivedResource.getName().substring(archivedResource.getName().lastIndexOf(".") + 1).toLowerCase(); String contentType = archivedResource.getContentType(); @@ -61,16 +71,23 @@ public Uni archiveData(String locationName, String filename, U .name(name); try (FileInputStream fis = new FileInputStream(body)) { - byte[] fileData = new byte[(int) body.length()]; - fis.read(fileData); - resourceData.data(Base64.getEncoder().encodeToString(fileData)); - return eloClient.createArchivedResource(resourceData) - .onFailure() - .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while archiving referenced resource '%s' in ELO!", uuid))) - .onItem() - .invoke(item -> - LOGGER.debug("returned archived related item {} ", item) - ); + return Uni.createFrom() + .item(fis.readAllBytes()) + .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()) + .map( fileData -> resourceData.data(Base64.getEncoder().encodeToString(fileData))) + .flatMap(resource -> eloClient.createArchivedResource(resourceData) + .onFailure() + .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while archiving referenced resource '%s' in ELO!", uuid))) + .onItem() + .invoke(Unchecked.consumer(item -> + { + try { + LOGGER.debug("returned archived related item {} ", objectMapper.writeValueAsString(item)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + )); } catch (IOException e) { throw new RuntimeException(e); } @@ -91,7 +108,7 @@ private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archi @Override public Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) throws CompasSclDataServiceException { - LOGGER.debug("Archiving scl resource in ELO!"); + LOGGER.debug("Archiving scl resource '{}' in ELO!", uuid); String extension = archivedResource.getType(); String contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE.getType(); @@ -108,8 +125,14 @@ public Uni archiveSclData(UUID uuid, IAbstractArchivedResource .onFailure() .transform(throwable -> new CompasSclDataServiceException(CREATION_ERROR_CODE, String.format("Error while archiving scl resource '%s' in ELO!", uuid))) .onItem() - .invoke(item -> - LOGGER.debug("returned archived scl item {} ", item) + .invoke(Unchecked.consumer(item -> + { + try { + LOGGER.debug("returned archived scl item {} ", objectMapper.writeValueAsString(item)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) ); } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index 02ef7bd9..6934fbd9 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -19,15 +19,15 @@ @ApplicationScoped public class CompasSclDataArchivingServiceImpl implements ICompasSclDataArchivingService { - private static final Logger LOGGER = LogManager.getLogger(CompasSclDataArchivingServiceImpl.class); @ConfigProperty(name = "scl-data-service.archiving.filesystem.location", defaultValue = "/work/locations") String locationPath; @Override public Uni createLocation(ILocationMetaItem location) { - LOGGER.info("locationPath: {}", locationPath); File newLocationDirectory = new File(locationPath + File.separator + location.getName()); - newLocationDirectory.mkdirs(); + if (!newLocationDirectory.exists()) { + newLocationDirectory.mkdirs(); + } return Uni.createFrom() .item(new LocationMetaData() .uuid(UUID.fromString(location.getId())) @@ -43,6 +43,9 @@ public Uni archiveData(String locationName, String filename, U File locationDir = new File(absolutePath); locationDir.mkdirs(); File f = new File(absolutePath + File.separator + filename); + if (f.exists() && !f.isDirectory()) { + return Uni.createFrom().failure(new RuntimeException("File '"+filename+"' already exists")); + } try (FileOutputStream fos = new FileOutputStream(f)) { try (FileInputStream fis = new FileInputStream(body)) { fos.write(fis.readAllBytes()); diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index bb498d1d..aff3e45d 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java @@ -16,6 +16,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.lfenergy.compas.core.commons.ElementConverter; import org.lfenergy.compas.core.commons.exception.CompasException; +import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; import org.lfenergy.compas.scl.data.exception.CompasSclDataServiceException; @@ -532,17 +533,7 @@ public Uni createLocation(String key, String name, String des } return Uni.createFrom() .item(repository.createLocation(key, name, description)) - .invoke(repository::addLocationTags) - .call(createdLocation -> archivingService.createLocation(createdLocation) - .onFailure() - .invoke(Unchecked.consumer(throwable -> { - LOGGER.warn("Error while creating location: {}", throwable.getMessage()); - try { - tm.setRollbackOnly(); - } catch (SystemException e) { - throw new RuntimeException(e); - } - }))); + .invoke(repository::addLocationTags); } /** @@ -616,6 +607,7 @@ public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { repository.unassignResourceFromLocation(locationId, resourceId); } } + /** * Archive a resource and link it to the corresponding scl_file entry * @@ -647,16 +639,36 @@ public Uni archiveResource(UUID id, String ve .onItem() .ifNotNull() .call(item -> - storeResourceData(archivingService.archiveData( - repository.findLocationByUUID(UUID.fromString(item.getLocationId())).getName(), - filename, - id, - body, - item - ), "Error while archiving resource: {}") + Uni.createFrom() + .item(repository.findLocationByUUID(UUID.fromString(item.getLocationId()))) + .flatMap(location -> + createLocationInArchive(location) + .onItem() + .call(createdLocation -> + storeResourceDataInArchive(archivingService.archiveData( + location.getKey(), + filename, + id, + body, + item + ), "Error while archiving resource: {}")) + ) ); } + private Uni createLocationInArchive(ILocationMetaItem location) { + return archivingService.createLocation(location) + .onFailure() + .invoke(Unchecked.consumer(throwable -> { + LOGGER.warn("Error while creating location: {}", throwable.getMessage()); + try { + tm.setRollbackOnly(); + } catch (SystemException e) { + throw new RuntimeException(e); + } + })); + } + /** * Archive an existing scl resource * @@ -686,17 +698,24 @@ public Uni archiveSclResource(UUID id, Versio .item(() -> repository.archiveSclResource(id, version, approver)) .onItem() .ifNotNull() - .call(archivedResource -> - storeResourceData(archivingService.archiveSclData( - id, - archivedResource, - repository.findLocationByUUID(UUID.fromString(archivedResource.getLocationId())).getName(), - repository.findByUUID(id, version) - ), "Error while archiving scl resource: {}") + .call(archivedSclResource -> + Uni.createFrom() + .item(repository.findLocationByUUID(UUID.fromString(archivedSclResource.getLocationId()))) + .flatMap( location -> + createLocationInArchive(location) + .onItem() + .call(createdLocation -> + storeResourceDataInArchive(archivingService.archiveSclData( + id, + archivedSclResource, + location.getKey(), + repository.findByUUID(id, version) + ), "Error while archiving scl resource: {}")) + ) ); } - private Uni storeResourceData(Uni archivingRequest, String errorMessage) { + private Uni storeResourceDataInArchive(Uni archivingRequest, String errorMessage) { return archivingRequest .onFailure() .invoke(Unchecked.consumer(throwable -> { From 00b2b0132e7ec5a94049f67fe5c52bbb32ae1f97 Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 3 Mar 2025 07:54:38 +0100 Subject: [PATCH 21/23] chore: use location key instead of location name for archiving --- .../CompasSclDataArchivingEloServiceImpl.java | 8 ++++---- .../CompasSclDataArchivingServiceImpl.java | 18 ++++++++---------- .../ICompasSclDataArchivingService.java | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java index 4cd2b55a..e942db4e 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java @@ -57,7 +57,7 @@ public Uni createLocation(ILocationMetaItem location) { } @Override - public Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource) { + public Uni archiveData(String locationKey, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource) { LOGGER.debug("Archiving related resource '{}' in ELO!", archivedResource.getId()); String extension = archivedResource.getName().substring(archivedResource.getName().lastIndexOf(".") + 1).toLowerCase(); @@ -67,7 +67,7 @@ public Uni archiveData(String locationName, String filename, U generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); resourceData.uuid(UUID.fromString(archivedResource.getId())) .type(TypeEnum.RESOURCE) - .location(locationName) + .location(locationKey) .name(name); try (FileInputStream fis = new FileInputStream(body)) { @@ -107,7 +107,7 @@ private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archi } @Override - public Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) throws CompasSclDataServiceException { + public Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationKey, String data) throws CompasSclDataServiceException { LOGGER.debug("Archiving scl resource '{}' in ELO!", uuid); String extension = archivedResource.getType(); @@ -117,7 +117,7 @@ public Uni archiveSclData(UUID uuid, IAbstractArchivedResource generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); resourceData.uuid(uuid) .type(TypeEnum.RESOURCE) - .location(locationName) + .location(locationKey) .name(archivedResource.getName()) .data(encodedDataString); diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java index 6934fbd9..cbe38907 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -2,8 +2,6 @@ import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; @@ -38,8 +36,8 @@ public Uni createLocation(ILocationMetaItem location) { } @Override - public Uni archiveData(String locationName, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { - String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName) + File.separator + "referenced_resources"; + public Uni archiveData(String locationKey, String filename, UUID resourceId, File body, IAbstractArchivedResourceMetaItem archivedResource) { + String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationKey) + File.separator + "referenced_resources"; File locationDir = new File(absolutePath); locationDir.mkdirs(); File f = new File(absolutePath + File.separator + filename); @@ -54,12 +52,12 @@ public Uni archiveData(String locationName, String filename, U return Uni.createFrom().failure(new RuntimeException(e)); } List archivedResourceTag = archivedResource.getFields().stream().map(field -> new ResourceTag(field.getKey(), field.getValue())).toList(); - return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationName, archivedResourceTag)); + return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationKey, archivedResourceTag)); } @Override - public Uni archiveSclData(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data) { - String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationName); + public Uni archiveSclData(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationKey, String data) { + String absolutePath = generateSclDataLocation(resourceId, archivedResource, locationKey); File locationDir = new File(absolutePath); locationDir.mkdirs(); File f = new File(locationDir + File.separator + archivedResource.getName() + "." + archivedResource.getType().toLowerCase()); @@ -69,10 +67,10 @@ public Uni archiveSclData(UUID resourceId, IAbstractArchivedRe throw new RuntimeException(e); } List archivedResourceTag = archivedResource.getFields().stream().map(field -> new ResourceTag(field.getKey(), field.getValue())).toList(); - return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationName, archivedResourceTag)); + return Uni.createFrom().item(new ResourceMetaData(TypeEnum.RESOURCE, resourceId, locationKey, archivedResourceTag)); } - private String generateSclDataLocation(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationName) { - return locationPath + File.separator + locationName + File.separator + resourceId + File.separator + archivedResource.getVersion(); + private String generateSclDataLocation(UUID resourceId, IAbstractArchivedResourceMetaItem archivedResource, String locationKey) { + return locationPath + File.separator + locationKey + File.separator + resourceId + File.separator + archivedResource.getVersion(); } } diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java index 2ac69577..be6544ef 100644 --- a/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -13,7 +13,7 @@ public interface ICompasSclDataArchivingService { Uni createLocation(ILocationMetaItem location); - Uni archiveData(String locationName, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); + Uni archiveData(String locationKey, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); - Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationName, String data); + Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationKey, String data); } From 990f35523c3390cc0cb1fd100fae52ff09a7bcec Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 3 Mar 2025 11:18:30 +0100 Subject: [PATCH 22/23] chore: use location key instead of location name as resource tag --- .../CompasSclDataPostgreSQLRepository.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index 5e9fdb1e..0040aaad 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -711,12 +711,12 @@ public boolean hasDuplicateLocationValues(String key, String name) { @Override public void addLocationTags(ILocationMetaItem location) { - String locationName = location.getName(); - ResourceTagItem locationNameTag = getResourceTag("LOCATION", locationName); + String locationKey = location.getKey(); + ResourceTagItem locationNameTag = getResourceTag("LOCATION", locationKey); if (locationNameTag == null) { - createResourceTag("LOCATION", locationName); - locationNameTag = getResourceTag("LOCATION", locationName); + createResourceTag("LOCATION", locationKey); + locationNameTag = getResourceTag("LOCATION", locationKey); } UUID locationUuid = UUID.fromString(location.getId()); updateTagMappingForLocation(locationUuid, List.of(Objects.requireNonNull(locationNameTag))); @@ -1141,9 +1141,9 @@ private List generateFields(String location, String sourceReso } private List generateFieldsFromResultSet(ResultSet resultSet, String examiner) throws SQLException { - String locationName = findLocationByUUID(UUID.fromString(resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD))).getName(); + String locationKey = findLocationByUUID(UUID.fromString(resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD))).getKey(); return generateFields( - locationName, + locationKey, resultSet.getString(ID_FIELD), resultSet.getString("created_by"), examiner, From ada4773073839ffd5012ba3e4c0e6db464cbc83a Mon Sep 17 00:00:00 2001 From: cgutmann Date: Mon, 3 Mar 2025 15:08:48 +0100 Subject: [PATCH 23/23] test: fix scl data service unit tests --- .../service/CompasSclDataServiceTest.java | 83 ++++++++++++++----- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java index 82d6c6cd..8df53c73 100644 --- a/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java +++ b/service/src/test/java/org/lfenergy/compas/scl/data/service/CompasSclDataServiceTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.lfenergy.compas.core.commons.ElementConverter; import org.lfenergy.compas.core.commons.exception.CompasException; +import org.lfenergy.compas.scl.data.dto.LocationMetaData; import org.lfenergy.compas.scl.data.dto.ResourceMetaData; import org.lfenergy.compas.scl.data.dto.TypeEnum; import org.lfenergy.compas.scl.data.exception.CompasNoDataFoundException; @@ -546,11 +547,12 @@ void createLocation_WhenCalled_ThenReturnCreatedLocation() { ILocationMetaItem expectedLocation = new LocationMetaItem(UUID.randomUUID().toString(), "locationKey", "locationName", null, 0); when(compasSclDataRepository.createLocation("locationKey", "locationName", null)).thenReturn(expectedLocation); when(compasSclDataRepository.hasDuplicateLocationValues("locationKey", "locationName")).thenReturn(false); - Uni actualLocation = compasSclDataService.createLocation("locationKey", "locationName", null); + + UniAssertSubscriber result = compasSclDataService.createLocation("locationKey", "locationName", null).subscribe().withSubscriber(UniAssertSubscriber.create()); verify(compasSclDataRepository, times(1)).createLocation(any(), any(), any()); - verify(compasSclDataArchivingServiceImpl, times(1)).createLocation(any()); - assertEquals(expectedLocation, actualLocation); + verify(compasSclDataRepository, times(1)).addLocationTags(any()); + assertEquals(expectedLocation, result.getItem()); } @Test @@ -744,7 +746,26 @@ void archiveResource_WhenCalled_ThenResourceIsArchived() { null ) ); - + LocationMetaItem locationMetaItem = new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + null, + 1 + ); + when(compasSclDataRepository.findLocationByUUID(locationId)) + .thenReturn( + locationMetaItem + ); + when(compasSclDataArchivingServiceImpl.createLocation(locationMetaItem)) + .thenReturn(Uni.createFrom() + .item(new LocationMetaData() + .uuid(locationId) + .name("locationName") + .key("locationKey") + .description(null) + .assignedResources(1) + )); when(compasSclDataArchivingServiceImpl.archiveData(any(), any(), any(), any(), any())).thenReturn(Uni.createFrom().item( new ResourceMetaData( TypeEnum.RESOURCE, @@ -789,14 +810,15 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { UUID locationId = UUID.randomUUID(); when(compasSclDataRepository.listHistoryVersionsByUUID(resourceId)).thenReturn(List.of(historyMetaItem)); + LocationMetaItem locationMetaItem = new LocationMetaItem( + locationId.toString(), + "someKey", + "someName", + "someDescription", + 1 + ); when(compasSclDataRepository.findLocationByUUID(UUID.fromString(locationId.toString()))).thenReturn( - new LocationMetaItem( - locationId.toString(), - "someKey", - "someName", - "someDescription", - 1 - ) + locationMetaItem ); when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) .thenReturn( @@ -816,7 +838,6 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { null ) ); - when(compasSclDataArchivingServiceImpl.archiveData(any(), any(), any(), any(), any())).thenReturn(Uni.createFrom().item( new ResourceMetaData( TypeEnum.RESOURCE, @@ -825,11 +846,19 @@ void archiveResource_WhenFileIsNull_ThenFileIsNotWritten() { List.of() ) )); + when(compasSclDataArchivingServiceImpl.createLocation(locationMetaItem)) + .thenReturn(Uni.createFrom() + .item(new LocationMetaData() + .uuid(locationId) + .name("locationName") + .key("locationKey") + .description("someDescription") + .assignedResources(1) + )); UniAssertSubscriber cut = compasSclDataService.archiveResource(resourceId, version, author, approver, contentType, filename, null).subscribe().withSubscriber(UniAssertSubscriber.create()); cut.assertCompleted(); - verify(compasSclDataRepository, times(1)).archiveResource(resourceId, new Version(version), author, approver, contentType, filename); verify(compasSclDataArchivingService, times(0)).archiveSclData(any(), any(), any(), any()); } @@ -859,16 +888,16 @@ void archiveSclResource_WhenCalled_ThenResourceIsArchived() { when(compasSclDataRepository.archiveSclResource(resourceId, new Version(version), approver)).thenReturn(archivedResource); when(compasSclDataRepository.findByUUID(resourceId, new Version(version))).thenReturn(sclData); + LocationMetaItem locationMetaItem = new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + null, + 0 + ); when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn( - new LocationMetaItem( - locationId.toString(), - "locationKey", - "locationName", - null, - 0 - ) + locationMetaItem ); - when(compasSclDataArchivingServiceImpl.archiveSclData(any(), any(), any(), any())).thenReturn(Uni.createFrom().item( new ResourceMetaData( TypeEnum.RESOURCE, @@ -877,14 +906,22 @@ void archiveSclResource_WhenCalled_ThenResourceIsArchived() { List.of() ) )); + when(compasSclDataArchivingServiceImpl.createLocation(locationMetaItem)) + .thenReturn(Uni.createFrom() + .item(new LocationMetaData() + .uuid(locationId) + .name("locationName") + .key("locationKey") + .description(null) + .assignedResources(0) + )); UniAssertSubscriber result = compasSclDataService.archiveSclResource(resourceId, new Version(version), approver).subscribe().withSubscriber(UniAssertSubscriber.create()); result.assertCompleted(); - verify(compasSclDataRepository, times(1)).archiveSclResource(resourceId, new Version(version), approver); verify(compasSclDataRepository, times(1)).findByUUID(resourceId, new Version(version)); - verify(compasSclDataArchivingServiceImpl, times(1)).archiveSclData(resourceId, archivedResource, "locationName", sclData); + verify(compasSclDataArchivingServiceImpl, times(1)).archiveSclData(resourceId, archivedResource, "locationKey", sclData); } @Test