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..fc676c14 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/archive/ArchiveResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java new file mode 100644 index 00000000..abe53a0b --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResource.java @@ -0,0 +1,157 @@ +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.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.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.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; + + @Inject + public ArchiveResource(CompasSclDataService compasSclDataService, JsonWebToken jsonWebToken, UserInfoProperties userInfoProperties) { + this.compasSclDataService = compasSclDataService; + this.jsonWebToken = jsonWebToken; + this.userInfoProperties = userInfoProperties; + } + + @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 compasSclDataService.archiveResource(id, version, xAuthor, xApprover, contentType, xFilename, body) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .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 compasSclDataService.archiveSclResource(id, new Version(version), approver) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .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); + } + + @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); + } + + 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(IAbstractArchivedResourceMetaItem archivedResource) { + return new ArchivedResource() + .uuid(archivedResource.getId()) + .location(archivedResource.getLocationId()) + .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() + ); + } + + 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 new file mode 100644 index 00000000..39a6fd15 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchivingApi.java @@ -0,0 +1,42 @@ +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.*; +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; +import java.util.UUID; + + +@Path("/api/archive") +@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}") + @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); + + @GET + @Path("/resources/{id}/versions") + @Produces({ "application/json" }) + Uni retrieveArchivedResourceHistory(@PathParam("id") UUID id); + + @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..794f7cf8 --- /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/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 new file mode 100644 index 00000000..f760ce27 --- /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/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 new file mode 100644 index 00000000..d682cd1e --- /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..53477aa8 --- /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/LocationsApi.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java new file mode 100644 index 00000000..d8d023bb --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsApi.java @@ -0,0 +1,53 @@ +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 + @Blocking + @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..916875a9 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResource.java @@ -0,0 +1,107 @@ +package org.lfenergy.compas.scl.data.rest.api.locations; + +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.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; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@RequestScoped +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, JsonWebToken jsonWebToken, UserInfoProperties userInfoProperties) { + this.compasSclDataService = compasSclDataService; + this.jsonWebToken = jsonWebToken; + this.userInfoProperties = userInfoProperties; + } + + @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 compasSclDataService.createLocation( + location.getKey(), + location.getName(), + location.getDescription() + ) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToLocation); + } + + @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); + return Uni.createFrom() + .item(() -> compasSclDataService.findLocationByUUID(locationId)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .onItem() + .transform(this::mapToLocation); + } + + @Override + public Uni> getLocations(Integer page, Integer pageSize) { + 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()) + .onItem() + .transform(list -> list.stream().map(this::mapToLocation).toList()); + } + + @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()) + .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..8cf43e3b --- /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..c4a5cb59 --- /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..509099f0 --- /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..717f7983 --- /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/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..899b5f17 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResource.java @@ -0,0 +1,132 @@ +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()) + .location(e.getLocation()); + } + + @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()) + .location(e.getLocation()); + } + + @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..8034881f --- /dev/null +++ b/app/src/main/openapi/archiving-api.yaml @@ -0,0 +1,758 @@ +openapi: 3.0.3 +info: + title: CoMPAS SCL Data Archiving 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: unassignResourceFromLocation + 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}: + 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 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' + /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' + /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: + 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" + assignedResources: + 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: + - uuid + - name + - author + - contentType + - version + - fields + - modifiedAt + - archivedAt + properties: + uuid: + type: string + description: "Unique resource identifier" + location: + type: string + description: "Location of the resource, might be empty" + name: + type: string + description: "Resource name" + note: + type: string + description: "Versioning note" + author: + type: string + description: "Modifying author" + approver: + type: string + description: "Name of the approver" + 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: + $ref: '#/components/schemas/ResourceTag' + ResourceTag: + type: object + required: + - key + - value + properties: + key: + type: string + description: "Tag key" + 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" + approver: + 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 + - 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 00000000..f2978dff Binary files /dev/null and b/app/src/main/openapi/data-resource-mockup.png differ diff --git a/app/src/main/openapi/history-api.yaml b/app/src/main/openapi/history-api.yaml new file mode 100644 index 00000000..dc2cb5f3 --- /dev/null +++ b/app/src/main/openapi/history-api.yaml @@ -0,0 +1,276 @@ +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: history + description: Endpoints managing history of scl files + +security: + - open-id-connect: + - read + +paths: + /api/scl/search: + post: + tags: + - history + description: Trigger search enabled by the search filter + summary: Trigger search enabled by the search filter + operationId: searchForResources + requestBody: + description: Search filter + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DataResourceSearch' + responses: + '200': + description: Successfully retrieved data resources meta data for given search query + content: + application/json: + schema: + $ref: '#/components/schemas/DataResourcesResult' + '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/scl/{id}/versions: + get: + tags: + - history + description: Trigger search enabled by the search filter + summary: Trigger search enabled by the search filter + operationId: retrieveDataResourceHistory + 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/DataResourceHistory' + '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' + /api/scl/{id}/version/{version}: + get: + tags: + - history + description: Retrieve data resource for a specific version + summary: Retrieve data resource for a specific version + operationId: retrieveDataResourceByVersion + parameters: + - name: id + in: path + description: Unique data resource identifier + required: true + schema: + type: string + format: uuid + - name: version + in: path + description: Combined with unique identifier this combination defines a specific document + required: true + schema: + type: string + responses: + '200': + description: Succefully retrieved data resource + content: + application/octet-stream: + schema: + type: string + format: binary + '401': + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '404': + description: One of the specified Parameters is not valid. + 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: + DataResourceSearch: + type: object + properties: + uuid: + type: string + description: "If uuid is set no other filter must be set" + type: + type: string + description: "Fulltext match set to one of the supported scl types: SSD, IID, ICD, SCD, CID, SED, ISD, STD, etc." + 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" + from: + type: string + format: date-time + description: "Starting date and time for filtering results. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z)." + to: + type: string + format: date-time + description: "Ending date and time for filtering results. Use ISO 8601 format (e.g., 2024-10-22T14:48:00Z)." + DataResourcesResult: + type: object + required: + - results + properties: + results: + type: array + items: + $ref: '#/components/schemas/DataResource' + DataResource: + type: object + required: + - uuid + - name + - author + - type + - changedAt + - version + - available + - deleted + properties: + uuid: + type: string + format: uuid + description: "Unique identifier" + name: + type: string + description: "Name of the resource" + author: + type: string + description: "Name of the author last changed the document" + type: + type: string + description: "One of the supported types: SSD, IID, ICD, SCD, CID, SED, ISD, STD, etc." + changedAt: + type: string + format: date-time + description: "Point in time of last modification/upload" + version: + type: string + description: "Generated version by the scl-data-service" + available: + type: boolean + description: "Defines if a resource is available as download or version was created while uploading a file" + default: true + deleted: + 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: + - versions + properties: + versions: + type: array + items: + $ref: '#/components/schemas/DataResourceVersion' + DataResourceVersion: + allOf: + - $ref: '#/components/schemas/DataResource' + - 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 + ErrorResponseDto: + required: + - timestamp + - code + - message + type: object + properties: + timestamp: + type: string + description: 2017-07-21T17:32:28Z. + format: 'date-time' + code: + type: string + example: TASK_NOT_FOUND + message: + type: string + example: Es wurde kein Task mit der id 'IdontExist' gefunden. diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 88e84e90..bb7de6bd 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -258,4 +258,41 @@ quarkus.http.auth.permission.STD_READ_GET_VERSION_WS.policy=STD_READ quarkus.http.auth.permission.STD_CREATE_POST_WS.paths=/compas-scl-data-service/scl-ws/v1/STD/create 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 \ No newline at end of file +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 + +# Feature flag to enable or disable the history feature +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 + +scl-data-service.archiving.filesystem.location=${FILESYSTEM_LOCATION_PATH:/work/locations} +scl-data-service.archiving.connector.enabled=${ELO_CONNECTOR_ENABLED:false} + +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/archive/ArchiveResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java new file mode 100644 index 00000000..5c687efe --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/archive/ArchiveResourceTest.java @@ -0,0 +1,168 @@ +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 io.smallrye.mutiny.Uni; +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.*; +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 +@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(Uni.createFrom().item(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(); + 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) + .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()); + } + + @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 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/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); + } +} 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/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..86dfc0e7 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/locations/LocationsResourceTest.java @@ -0,0 +1,218 @@ +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 io.smallrye.mutiny.Uni; +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; +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; + @InjectMock + private JsonWebToken jwt; + + @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(jwt.getClaim("name")).thenReturn("test user"); + when(compasSclDataService.createLocation(key, name, description)).thenReturn(Uni.createFrom().item(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/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTest.java b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTest.java new file mode 100644 index 00000000..62ab490b --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTest.java @@ -0,0 +1,122 @@ +package org.lfenergy.compas.scl.data.rest.api.scl; + +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 jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +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.DataResourceHistory; +import org.lfenergy.compas.scl.data.rest.api.scl.model.DataResourceSearch; +import org.lfenergy.compas.scl.data.rest.api.scl.model.DataResourcesResult; +import org.lfenergy.compas.scl.data.service.CompasSclDataService; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@QuarkusTest +@TestHTTPEndpoint(HistoryResource.class) +@TestSecurity(user = "test-user") +class HistoryResourceTest { + + @InjectMock + private CompasSclDataService compasSclDataService; + + @Test + void searchForResources_WhenCalled_ThenReturnsDataResourcesResult() { + var search = new DataResourceSearch(); // Populate with necessary data + + List 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..80986820 --- /dev/null +++ b/app/src/test/java/org/lfenergy/compas/scl/data/rest/api/scl/HistoryResourceTestdataBuilder.java @@ -0,0 +1,78 @@ +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 String location = "some location"; + 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, location, 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..5bb6231d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +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 + # 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 + +networks: + compas-network: + name: compas-network + driver: bridge 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/AbstractArchivedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java new file mode 100644 index 00000000..29b60917 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/AbstractArchivedResourceMetaItem.java @@ -0,0 +1,60 @@ +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 locationId; + private final List fields; + private final OffsetDateTime modifiedAt; + private final 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.locationId = locationId; + 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; + } + + @Override + public String getLocationId() { + return locationId; + } + + 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/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/ArchivedReferencedResourceMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedReferencedResourceMetaItem.java new file mode 100644 index 00000000..a9153c10 --- /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 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; + 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..11b2c128 --- /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, null); + 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/ArchivedResourcesMetaItem.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/ArchivedResourcesMetaItem.java new file mode 100644 index 00000000..e4e4cc33 --- /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/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/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 new file mode 100644 index 00000000..65265fd5 --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java @@ -0,0 +1,66 @@ +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 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, String location,OffsetDateTime changedAt, boolean archived, boolean available, boolean deleted) { + super(id, name, version, null); + this.type = type; + this.author = author; + this.comment = comment; + this.location = location; + 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 String getLocation() { + return location; + } + + @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/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/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/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/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 f6685b08..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 @@ -4,24 +4,24 @@ package org.lfenergy.compas.scl.data.repository.postgresql; +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.*; 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.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.*; import static jakarta.transaction.Transactional.TxType.REQUIRED; import static jakarta.transaction.Transactional.TxType.SUPPORTS; @@ -34,13 +34,36 @@ 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 KEY_FIELD = "key"; + private static final String VALUE_FIELD = "value"; + private static final String LOCATIONMETAITEM_DESCRIPTION_FIELD = "description"; 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"; 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 static final String ARCHIVEMETAITEM_LOCATION_FIELD = "location"; + 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 static final Logger LOGGER = LogManager.getLogger(CompasSclDataPostgreSQLRepository.class); private final DataSource dataSource; + @Inject + TransactionManager tm; + @Inject public CompasSclDataPostgreSQLRepository(DataSource dataSource) { this.dataSource = dataSource; @@ -56,6 +79,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 @@ -77,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), @@ -115,6 +138,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 +208,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) { @@ -217,10 +272,11 @@ 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 = ? + and scl_file.is_deleted = false order by scl_file.major_version desc, scl_file.minor_version desc, scl_file.patch_version desc """; @@ -234,7 +290,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); @@ -322,7 +379,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 +421,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 +466,1252 @@ private List createLabelList(Array sqlArray) throws SQLException { } return labelsList; } -} + + @Override + @Transactional(SUPPORTS) + public List listHistory() { + 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.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.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + 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 + 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) { + 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.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.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + 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 + WHERE scl_file.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)); + } + + @Override + @Transactional(SUPPORTS) + public List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { + StringBuilder sqlBuilder = new StringBuilder(""" + 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.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.*, + UNNEST( + XPATH( + '(/scl:SCL/scl:Header//scl:Hitem[(not(@revision) or @revision="") and @version="' || + 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 + 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<>(); + + 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.created_by = ?"); + parameters.add(author); + } + + if (from != null) { + sqlBuilder.append(" AND subquery.creation_date >= ?"); + parameters.add(from); + } + + if (to != null) { + sqlBuilder.append(" AND subquery.creation_date <= ?"); + parameters.add(to); + } + + sqlBuilder.append(System.lineSeparator()); + sqlBuilder.append("ORDER BY subquery.name;"); + + return executeHistoryQuery(sqlBuilder.toString(), parameters); + } + + + @Override + @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 + 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.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 ( + 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)); + } + + @Override + @Transactional(REQUIRED) + 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); + sclStmt.setString(2, key); + sclStmt.setString(3, name); + sclStmt.setString(4, description); + sclStmt.executeUpdate(); + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_INSERT_ERROR_CODE, "Error adding Location to database!", exp); + } + + 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 locationKey = location.getKey(); + ResourceTagItem locationNameTag = getResourceTag("LOCATION", locationKey); + + if (locationNameTag == null) { + createResourceTag("LOCATION", locationKey); + locationNameTag = getResourceTag("LOCATION", locationKey); + } + UUID locationUuid = UUID.fromString(location.getId()); + updateTagMappingForLocation(locationUuid, List.of(Objects.requireNonNull(locationNameTag))); + } + + @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 *, (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 ? + 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 *, (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)); + } + return retrievedLocation.get(0); + } + + @Override + @Transactional(REQUIRED) + public void deleteLocation(UUID locationId) { + String deleteLocationQuery = """ + DELETE FROM location + WHERE id = ?; + """; + + try (Connection connection = dataSource.getConnection(); + PreparedStatement sclStmt = connection.prepareStatement(deleteLocationQuery)) { + 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(System.lineSeparator()); + } + sqlBuilder.append("WHERE id = ?;"); + try (Connection connection = dataSource.getConnection(); + PreparedStatement 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 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); + } + } + + @Override + @Transactional(REQUIRED) + public void unassignResourceFromLocation(UUID locationId, UUID resourceId) { + String archivedResourceSql = """ + UPDATE scl_file + SET location_id = NULL + WHERE id = ? AND location_id = ?; + """; + 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); + } + } + + 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); + } + } + + @Override + public IAbstractArchivedResourceMetaItem archiveResource(UUID id, Version version, String author, String approver, String contentType, String filename) { + ArchivedSclResourceMetaItem sclResourceMetaItem = getSclFileAsArchivedSclResourceMetaItem(id, version, approver); + String locationId = sclResourceMetaItem.getLocationId(); + UUID assignedResourceId = UUID.randomUUID(); + + insertRelatedResourceEntry(id, version, author, approver, contentType, filename, assignedResourceId, locationId); + + String locationName = findLocationByUUID(UUID.fromString(locationId)).getName(); + List resourceTags = generateFields(locationName, id.toString(), author, approver, filename, version.toString()); + ArchivedReferencedResourceMetaItem archivedResourcesMetaItem = new ArchivedReferencedResourceMetaItem( + assignedResourceId.toString(), + filename, + version.toString(), + author, + approver, + null, + contentType, + locationId, + 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.getLocationId(), + archivedResourcesMetaItem.getFields(), + archivedResourcesMetaItem.getModifiedAt(), + archivedResourcesMetaItem.getArchivedAt(), + archivedResourcesMetaItem.getComment() + ); + } + + 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); + if (convertedArchivedResourceMetaItem != null) { + 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.getLocationId(), + 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.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 + 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, + resultSet.getString("type"), + null, + 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, 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); + locationTag = getResourceTag("LOCATION", location); + } + if (resourceIdTag == null && sourceResourceId != null) { + createResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); + resourceIdTag = getResourceTag("SOURCE_RESOURCE_ID", sourceResourceId); + } + if (authorTag == null && author != null) { + createResourceTag("AUTHOR", author); + authorTag = getResourceTag("AUTHOR", author); + } + if (examinerTag == null && examiner != null) { + 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(); + } + + private List generateFieldsFromResultSet(ResultSet resultSet, String examiner) throws SQLException { + String locationKey = findLocationByUUID(UUID.fromString(resultSet.getString(ARCHIVEMETAITEM_LOCATION_FIELD))).getKey(); + return generateFields( + locationKey, + resultSet.getString(ID_FIELD), + resultSet.getString("created_by"), + examiner, + resultSet.getString(NAME_FIELD), + createVersion(resultSet) + ); + } + + 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) { + 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); + 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) { + 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); + } + } + + @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, + ARRAY_AGG(rt.id || ';' || rt.key || ';' || COALESCE(rt.value, 'null')) AS tags, + l.id 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, 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))); + } + + @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.*, + 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.id 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 l.name = ?"); + } + if (name != null && !name.isBlank()) { + parameters.add("%"+name+"%"); + parameters.add("%"+name+"%"); + sb.append(" AND (rr.filename ILIKE ? OR sf.name ILIKE ?)"); + } + if (approver != null && !approver.isBlank()) { + parameters.add(approver); + parameters.add(approver); + sb.append(" AND (rr.approver = ? OR xml_data.hitem_who::varchar = ?)"); + } + if (contentType != null && !contentType.isBlank()) { + parameters.add(contentType); + sb.append(" AND rr.content_type = ?"); + } + if (type != null && !type.isBlank()) { + parameters.add(type); + parameters.add(type); + sb.append(" AND (rr.type = ? OR sf.type = ?)"); + } + if (voltage != null && !voltage.isBlank()) { +// ToDo cgutmann: find out how to retrieve 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, 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)); + } + + @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.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 + 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 ar.id = ? + 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<>(); + + 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); + } + if (versions.isEmpty()) { + throw new CompasNoDataFoundException(String.format("Unable to find resource with id '%s'.", uuid)); + } + 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), + 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<>(); + 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 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 location resource_tag mapping 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 archived_resource resource_tag mapping entries from database!", exp + ); + } + } + + 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), + resultSet.getString(ARCHIVEMETAITEM_LOCATION_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().atOffset(ZoneOffset.UTC) + : 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 { + return new LocationMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(KEY_FIELD), + resultSet.getString(NAME_FIELD), + 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)) { + 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].equalsIgnoreCase("null") ? null : entry.split(";")[2] + ) + ).forEach(resourceTags::add); + } + return 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__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_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/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..78024ebc --- /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,17 @@ +-- +-- 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), + 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'; 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-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/exception/CompasSclDataServiceErrorCode.java b/repository/src/main/java/org/lfenergy/compas/scl/data/exception/CompasSclDataServiceErrorCode.java index 19fca817..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 @@ -16,6 +16,10 @@ 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-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/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..2fb866cd --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IAbstractArchivedResourceMetaItem.java @@ -0,0 +1,29 @@ +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(); + + 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/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/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/model/IArchivedResourcesMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IArchivedResourcesMetaItem.java new file mode 100644 index 00000000..a65feaad --- /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/IHistoryMetaItem.java b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java new file mode 100644 index 00000000..ed350520 --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java @@ -0,0 +1,26 @@ +// 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(); + + String getLocation(); + + OffsetDateTime getChangedAt(); + + boolean isArchived(); + + boolean isAvailable(); + + boolean isDeleted(); + +} 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/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 4535fdda..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 @@ -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,196 @@ 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 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 delete(SclFileType type, UUID id, Version version); + 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); + + /** + * Create a new 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(String key, String name, String description); + + /** + * Create tags that identify a Location object + * + * @param location The Location object that receives the tags + */ + void addLocationTags(ILocationMetaItem location); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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 + */ + void assignResourceToLocation(UUID locationId, UUID resourceId); + + /** + * 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/pom.xml b/service/pom.xml index a9d11023..11a3165d 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 @@ -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 + @@ -66,6 +80,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/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/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..e942db4e --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingEloServiceImpl.java @@ -0,0 +1,138 @@ +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; +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.*; +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); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Inject + @RestClient + IEloConnectorRestClient eloClient; + + @Override + public Uni createLocation(ILocationMetaItem location) { + LOGGER.debug("Creating a new location '{}' in ELO!", location.getId()); + 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(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 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(); + String contentType = archivedResource.getContentType(); + String name = archivedResource.getName().substring(0, archivedResource.getName().lastIndexOf(".")); + ResourceData resourceData = new ResourceData(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(UUID.fromString(archivedResource.getId())) + .type(TypeEnum.RESOURCE) + .location(locationKey) + .name(name); + + try (FileInputStream fis = new FileInputStream(body)) { + 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); + } + } + + private void generateBaseResourceDataDto(IAbstractArchivedResourceMetaItem archivedResource, ResourceData resourceData, String extension, String contentType) { + resourceData.version(archivedResource.getVersion()) + .extension(extension) + .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 locationKey, String data) throws CompasSclDataServiceException { + LOGGER.debug("Archiving scl resource '{}' in ELO!", uuid); + + String extension = archivedResource.getType(); + String contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE.getType(); + String encodedDataString = Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + ResourceData resourceData = new ResourceData(); + generateBaseResourceDataDto(archivedResource, resourceData, extension, contentType); + resourceData.uuid(uuid) + .type(TypeEnum.RESOURCE) + .location(locationKey) + .name(archivedResource.getName()) + .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(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 new file mode 100644 index 00000000..cbe38907 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataArchivingServiceImpl.java @@ -0,0 +1,76 @@ +package org.lfenergy.compas.scl.data.service; + +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +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; +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 { + + @ConfigProperty(name = "scl-data-service.archiving.filesystem.location", defaultValue = "/work/locations") + String locationPath; + + @Override + public Uni createLocation(ILocationMetaItem location) { + File newLocationDirectory = new File(locationPath + File.separator + location.getName()); + if (!newLocationDirectory.exists()) { + 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 + 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); + 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()); + } + } catch (IOException 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, locationKey, archivedResourceTag)); + } + + @Override + 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()); + 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, locationKey, archivedResourceTag)); + } + + 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/CompasSclDataService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclDataService.java index dbbccd31..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 @@ -3,33 +3,42 @@ // 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.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; -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.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; +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.io.File; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +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.*; @@ -40,9 +49,23 @@ */ @ApplicationScoped public class CompasSclDataService { + private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; + 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, @@ -52,6 +75,17 @@ public CompasSclDataService(CompasSclDataRepository repository, ElementConverter this.sclElementProcessor = sclElementProcessor; } + @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; + } + } + /** * List the latest version of all SCL XML Files for a specific type. * @@ -81,9 +115,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 +132,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 +169,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 +203,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 +214,11 @@ 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); + return newSclData; } - public boolean hasDuplicateSclName(SclFileType type, String name){ + public boolean hasDuplicateSclName(SclFileType type, String name) { return repository.hasDuplicateSclName(type, name); } @@ -171,6 +236,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 +276,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 +289,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 (currentSclMetaInfo.getLocationId() != null) { + assignResourceToLocation(UUID.fromString(currentSclMetaInfo.getLocationId()), id); + } + 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 +375,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 +438,8 @@ List validateLabels(Element scl) { } return labelElements.stream() - .map(Element::getTextContent) - .toList(); + .map(Element::getTextContent) + .toList(); } /** @@ -353,4 +452,334 @@ 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); + } + + /** + * 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 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); + return Uni.createFrom().failure(new CompasSclDataServiceException(CREATION_ERROR_CODE, errorMessage)); + } + return Uni.createFrom() + .item(repository.createLocation(key, name, description)) + .invoke(repository::addLocationTags); + } + + /** + * 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) { + 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, + errorMessage); + } + + repository.deleteLocationTags(locationToDelete); + 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); + } + + /** + * 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 locationItem = repository.findLocationByUUID(locationId); + if (repository.listHistory(resourceId).isEmpty()) { + 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); + } + } + + /** + * Unassign a Resource 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) { + ILocationMetaItem locationItem = repository.findLocationByUUID(locationId); + if (repository.listHistory(resourceId).isEmpty()) { + 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); + } + } + + /** + * 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 + * @param body The content of the resource + * @return The created archived resource item + */ + @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) { + 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))) { + 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)); + } + return Uni.createFrom() + .item(() -> repository.archiveResource(id, new Version(version), author, approver, contentType, filename)) + .onItem() + .ifNotNull() + .call(item -> + 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 + * + * @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(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, + errorMessage); + } + + 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 Uni.createFrom() + .item(() -> repository.archiveSclResource(id, version, approver)) + .onItem() + .ifNotNull() + .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 storeResourceDataInArchive(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); + } + })); + } + + /** + * 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); + } + + 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; + } } 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..be6544ef --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/ICompasSclDataArchivingService.java @@ -0,0 +1,19 @@ +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; + +import java.io.File; +import java.util.UUID; + +public interface ICompasSclDataArchivingService { + + Uni createLocation(ILocationMetaItem location); + + Uni archiveData(String locationKey, String filename, UUID uuid, File body, IAbstractArchivedResourceMetaItem archivedResource); + + Uni archiveSclData(UUID uuid, IAbstractArchivedResourceMetaItem archivedResource, String locationKey, 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..16d6ec0c --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/IEloConnectorRestClient.java @@ -0,0 +1,44 @@ +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.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; + +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); + + @POST + @Path("/location") + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + Uni createLocation(@Valid @NotNull LocationData locationData); + + @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/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..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 @@ -6,26 +6,31 @@ // 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.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; 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.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; import org.lfenergy.compas.scl.extensions.model.SclFileType; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.w3c.dom.Element; +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; @@ -44,14 +49,23 @@ class CompasSclDataServiceTest { @Mock private CompasSclDataRepository compasSclDataRepository; + @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() { + 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 @@ -188,6 +202,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 +340,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 @@ -369,6 +403,663 @@ 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", + "Comment 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", + "Comment 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", + "Comment 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", + "Comment 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); + when(compasSclDataRepository.hasDuplicateLocationValues("locationKey", "locationName")).thenReturn(false); + + UniAssertSubscriber result = compasSclDataService.createLocation("locationKey", "locationName", null).subscribe().withSubscriber(UniAssertSubscriber.create()); + + verify(compasSclDataRepository, times(1)).createLocation(any(), any(), any()); + verify(compasSclDataRepository, times(1)).addLocationTags(any()); + assertEquals(expectedLocation, result.getItem()); + } + + @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); + } + + @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); + } + + @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); + 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(historyMetaItem)); + + compasSclDataService.assignResourceToLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).assignResourceToLocation(locationId, resourceId); + verify(compasSclDataRepository, times(1)).listHistory(resourceId); + } + + @Test + void assignResourceToLocation_WhenResourceHasLocationAssignedWithArchivedItems_ThenNewLocationIsAssignedAndArchivedItemsAreMoved() { + UUID resourceId = UUID.randomUUID(); + UUID newLocationId = UUID.randomUUID(); + ILocationMetaItem newLocationItem = new LocationMetaItem(newLocationId.toString(), "newLocationKey", "newLocationName", null, 0); + IHistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", + "1.0.0", + null, + "someUser", + "IID", + "locationName", + null, + true, + true, + false + ); + when(compasSclDataRepository.findLocationByUUID(newLocationId)).thenReturn(newLocationItem); + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyMetaItem)); + + 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(); + + ILocationMetaItem assignedLocation = new LocationMetaItem(locationId.toString(), "locationKey", "locationName", null, 1); + HistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", + "1.0.0", + null, + "someUser", + "IID", + "locationName", + null, + true, + true, + false + ); + + when(compasSclDataRepository.listHistory(resourceId)).thenReturn(List.of(historyMetaItem)); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn(assignedLocation); + + compasSclDataService.unassignResourceFromLocation(locationId, resourceId); + + verify(compasSclDataRepository, times(1)).findLocationByUUID(locationId); + verify(compasSclDataRepository, times(1)).listHistory(resourceId); + verify(compasSclDataRepository, times(1)).unassignResourceFromLocation(locationId, resourceId); + } + + @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()); + } + + @Test + void archiveResource_WhenCalled_ThenResourceIsArchived() { + UUID resourceId = UUID.randomUUID(); + UUID locationId = 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.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( + UUID.randomUUID().toString(), + "someName", + version, + author, + approver, + null, + contentType, + locationId.toString(), + List.of(), + null, + OffsetDateTime.now(), + null, + 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, + 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); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + file.delete(); + } + } + + @Test + 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"; + IHistoryMetaItem historyMetaItem = new HistoryMetaItem( + resourceId.toString(), + "sclDataName", + version, + null, + "someUser", + "IID", + "locationName", + null, + true, + true, + false + ); + + 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( + locationMetaItem + ); + when(compasSclDataRepository.archiveResource(resourceId, new Version(version), author, approver, contentType, filename)) + .thenReturn( + new ArchivedSclResourceMetaItem( + UUID.randomUUID().toString(), + "someName", + version, + author, + approver, + null, + contentType, + locationId.toString(), + List.of(), + null, + OffsetDateTime.now(), + null, + null + ) + ); + when(compasSclDataArchivingServiceImpl.archiveData(any(), any(), any(), any(), any())).thenReturn(Uni.createFrom().item( + new ResourceMetaData( + TypeEnum.RESOURCE, + UUID.randomUUID(), + locationId.toString(), + 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()); + } + + @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( + UUID.randomUUID().toString(), + "someName", + version, + "someAuthor", + approver, + "IID", + null, + locationId.toString(), + 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); + LocationMetaItem locationMetaItem = new LocationMetaItem( + locationId.toString(), + "locationKey", + "locationName", + null, + 0 + ); + when(compasSclDataRepository.findLocationByUUID(locationId)).thenReturn( + locationMetaItem + ); + when(compasSclDataArchivingServiceImpl.archiveSclData(any(), any(), any(), any())).thenReturn(Uni.createFrom().item( + new ResourceMetaData( + TypeEnum.RESOURCE, + UUID.randomUUID(), + locationId.toString(), + 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, "locationKey", 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); 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));