diff --git a/app/pom.xml b/app/pom.xml index 2a57c86f..58894bb4 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -112,6 +112,12 @@ SPDX-License-Identifier: Apache-2.0 quarkus-container-image-docker + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-server + 3.0.0-SNAPSHOT + + io.quarkus diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclHistoryResource.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclHistoryResource.java new file mode 100644 index 00000000..668dfcaf --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/CompasSclHistoryResource.java @@ -0,0 +1,74 @@ +package org.lfenergy.compas.scl.data.rest.v1; + +import io.quarkus.security.Authenticated; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; +import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.service.CompasSclHistoryService; +import org.lfenergy.compas.scl.extensions.model.SclFileType; +import org.lfenergy.compas.scl.rest.api.HistoryResource; +import org.lfenergy.compas.scl.rest.api.beans.DataResourceHistory; +import org.lfenergy.compas.scl.rest.api.beans.DataResourceSearch; +import org.lfenergy.compas.scl.rest.api.beans.DataResourcesResult; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +@Authenticated +@RequestScoped +@Path("/api") +public class CompasSclHistoryResource implements HistoryResource { + + private final CompasSclHistoryService compasSclHistoryService; + + @Inject + public CompasSclHistoryResource(CompasSclHistoryService compasSclHistoryService) { + this.compasSclHistoryService = compasSclHistoryService; + } + + @Override + public DataResourcesResult searchForResources(DataResourceSearch searchQuery) { + List historyItems = fetchHistoryItems(searchQuery); + DataResourcesResult result = new DataResourcesResult(); + result.setResults(historyItems.stream().map(HistoryMapper::convertToDataResource).toList()); + return result; + } + + @Override + public DataResourceHistory retrieveDataResourceHistory(String id) { + List historyItems = compasSclHistoryService.listHistoryVersionsForResource(UUID.fromString(id)); + DataResourceHistory resourcesHistories = new DataResourceHistory(); + resourcesHistories.setVersions(historyItems.stream().map(HistoryMapper::convertToDataResourceVersion).toList()); + return resourcesHistories; + } + + @Override + public Response retrieveDataResourceByVersion(String id, String version) { + String fetchedData = compasSclHistoryService.findFileByIdAndVersion(UUID.fromString(id), new Version(version)); + return Response.status(Response.Status.OK).entity(fetchedData).type(MediaType.APPLICATION_XML).build(); + } + + private List fetchHistoryItems(DataResourceSearch searchQuery) { + String uuid = searchQuery.getUuid(); + + if (uuid != null) { + return compasSclHistoryService.listHistoryVersionsForResource(UUID.fromString(uuid)); + } + + SclFileType type = searchQuery.getType() != null ? SclFileType.valueOf(searchQuery.getType()) : null; + String name = searchQuery.getName(); + String author = searchQuery.getAuthor(); + OffsetDateTime from = DateUtil.convertToOffsetDateTime(searchQuery.getFrom()); + OffsetDateTime to = DateUtil.convertToOffsetDateTime(searchQuery.getTo()); + + if (type != null || name != null || author != null || from != null || to != null) { + return compasSclHistoryService.searchResourcesHistoryVersions(type, name, author, from, to); + } + return compasSclHistoryService.listHistory(); + } +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/DateUtil.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/DateUtil.java new file mode 100644 index 00000000..6528ef41 --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/DateUtil.java @@ -0,0 +1,21 @@ +package org.lfenergy.compas.scl.data.rest.v1; + +import org.apache.commons.lang3.ObjectUtils; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Date; + +public final class DateUtil { + + private DateUtil() { + } + + public static OffsetDateTime convertToOffsetDateTime(Date date) { + return ObjectUtils.isEmpty(date) ? null : date.toInstant().atOffset(ZoneOffset.UTC); + } + + public static Date convertToDate(OffsetDateTime date) { + return ObjectUtils.isEmpty(date) ? null : new Date(date.toInstant().toEpochMilli()); + } +} diff --git a/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/HistoryMapper.java b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/HistoryMapper.java new file mode 100644 index 00000000..91cb332b --- /dev/null +++ b/app/src/main/java/org/lfenergy/compas/scl/data/rest/v1/HistoryMapper.java @@ -0,0 +1,39 @@ +package org.lfenergy.compas.scl.data.rest.v1; + +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; +import org.lfenergy.compas.scl.rest.api.beans.DataResource; +import org.lfenergy.compas.scl.rest.api.beans.DataResourceVersion; + +import java.util.UUID; + +public final class HistoryMapper { + + private HistoryMapper() { + } + + public static DataResourceVersion convertToDataResourceVersion(IHistoryMetaItem item) { + DataResourceVersion version = new DataResourceVersion(); + version.setVersion(item.getVersion()); + version.setAuthor(item.getAuthor()); + version.setName(item.getName()); + version.setDeleted(item.isDeleted()); + version.setUuid(UUID.fromString(item.getId())); + version.setType(item.getType()); + version.setChangedAt(DateUtil.convertToDate(item.getChangedAt())); + version.setComment(item.getComment()); + return version; + } + + public static DataResource convertToDataResource(IHistoryMetaItem item) { + DataResource version = new DataResource(); + version.setVersion(item.getVersion()); + version.setAuthor(item.getAuthor()); + version.setName(item.getName()); + version.setDeleted(item.isDeleted()); + version.setUuid(UUID.fromString(item.getId())); + version.setType(item.getType()); + version.setChangedAt(DateUtil.convertToDate(item.getChangedAt())); + return version; + } + +} diff --git a/app/src/main/openapi/history-api.yaml b/app/src/main/openapi/history-api.yaml new file mode 100644 index 00000000..0897824a --- /dev/null +++ b/app/src/main/openapi/history-api.yaml @@ -0,0 +1,261 @@ +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: + /history/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' + /history/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' + /history/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" + 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 + - 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" + deleted: + type: boolean + description: "Defines if a resource is marked as deleted" + default: false + 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" + 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..c2c7f61f 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -7,6 +7,10 @@ compas.userinfo.who.claimname = ${USERINFO_WHO_CLAIMNAME:name} compas.userinfo.session.warning = ${USERINFO_SESSION_WARNING:10} compas.userinfo.session.expires = ${USERINFO_SESSION_EXPIRES:15} +quarkus.openapi.generator.spec=history-api.yaml +quarkus.openapi.generator.input-base-dir=src/main/openapi +quarkus.openapi.generator.base-package=org.lfenergy.compas.scl.rest.api + quarkus.http.cors = false quarkus.http.root-path = /compas-scl-data-service quarkus.http.limits.max-body-size = 150M 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..bb92b5aa --- /dev/null +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/model/HistoryMetaItem.java @@ -0,0 +1,45 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; + +public class HistoryMetaItem extends AbstractItem implements IHistoryMetaItem { + private final String type; + private final String author; + private final String comment; + private final OffsetDateTime changedAt; + private final boolean deleted; + + public HistoryMetaItem(String id, String name, String version, String type, String author, String comment, OffsetDateTime changedAt, boolean deleted) { + super(id, name, version); + this.type = type; + this.author = author; + this.comment = comment; + this.changedAt = changedAt; + this.deleted = deleted; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public OffsetDateTime getChangedAt() { + return changedAt; + } + + @Override + public boolean isDeleted() { + return deleted; + } +} diff --git a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java index f6685b08..dea7d2fc 100644 --- a/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java +++ b/repository-postgresql/src/main/java/org/lfenergy/compas/scl/data/repository/postgresql/CompasSclDataPostgreSQLRepository.java @@ -14,14 +14,11 @@ import jakarta.inject.Inject; import javax.sql.DataSource; import jakarta.transaction.Transactional; -import java.sql.Array; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; + +import java.sql.*; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.*; import static jakarta.transaction.Transactional.TxType.REQUIRED; import static jakarta.transaction.Transactional.TxType.SUPPORTS; @@ -39,6 +36,12 @@ public class CompasSclDataPostgreSQLRepository implements CompasSclDataRepositor 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_IS_DELETED_FIELD = "is_deleted"; + private final DataSource dataSource; @Inject @@ -56,6 +59,7 @@ public List list(SclFileType type) { from (select distinct on (scl_file.id) * from scl_file where scl_file.type = ? + AND scl_file.is_deleted = false order by scl_file.id , scl_file.major_version desc , scl_file.minor_version desc @@ -115,6 +119,7 @@ left outer join ( and scl_data.patch_version = scl_file.patch_version where scl_file.id = ? and scl_file.type = ? + and scl_file.is_deleted = false order by scl_file.major_version , scl_file.minor_version , scl_file.patch_version @@ -162,6 +167,7 @@ public String findByUUID(SclFileType type, UUID id, Version version) { and scl_file.major_version = ? and scl_file.minor_version = ? and scl_file.patch_version = ? + and scl_file.is_deleted = false """; try (var connection = dataSource.getConnection(); @@ -191,6 +197,7 @@ public boolean hasDuplicateSclName(SclFileType type, String name) { select distinct on (scl_file.id) scl_file.name 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 @@ -221,6 +228,7 @@ public IAbstractItem findMetaInfoByUUID(SclFileType type, UUID id) { from scl_file where scl_file.id = ? and scl_file.type = ? + and scl_file.is_deleted = false order by scl_file.major_version desc, scl_file.minor_version desc, scl_file.patch_version desc """; @@ -303,12 +311,22 @@ insert into scl_label(scl_id, major_version, minor_version, patch_version, label @Override @Transactional(REQUIRED) - public void delete(SclFileType type, UUID id) { - var sql = """ + public void delete(SclFileType type, UUID id, boolean softDelete) { + String sql; + if (softDelete) { + sql = """ + UPDATE scl_file + SET scl_file.is_deleted = true + where scl_file.id = ? + and scl_file.type = ? + """; + } else { + sql = """ delete from scl_file where scl_file.id = ? and scl_file.type = ? """; + } try (var connection = dataSource.getConnection(); var stmt = connection.prepareStatement(sql)) { @@ -322,8 +340,20 @@ public void delete(SclFileType type, UUID id) { @Override @Transactional(REQUIRED) - public void delete(SclFileType type, UUID id, Version version) { - var sql = """ + public void delete(SclFileType type, UUID id, Version version, boolean softDelete) { + String sql; + if (softDelete) { + sql = """ + UPDATE scl_file + SET scl_file.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 = ? + """; + } else { + sql = """ delete from scl_file where scl_file.id = ? and scl_file.type = ? @@ -331,6 +361,7 @@ public void delete(SclFileType type, UUID id, Version version) { and scl_file.minor_version = ? and scl_file.patch_version = ? """; + } try (var connection = dataSource.getConnection(); var stmt = connection.prepareStatement(sql)) { @@ -345,6 +376,116 @@ public void delete(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 List listHistory() { + var sql = """ + select * + from scl_file + order by + scl_file.id ASC, + scl_file.major_version DESC, + scl_file.minor_version DESC, + scl_file.patch_version DESC + """; + return executeHistoryQuery(sql, Collections.emptyList()); + } + + @Override + @Transactional(SUPPORTS) + public List listHistory(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { + StringBuilder sqlBuilder = new StringBuilder(""" + select * + from scl_file + where 1=1 + """); + + List parameters = new ArrayList<>(); + + if (type != null) { + sqlBuilder.append(" AND scl_file.type = ?"); + parameters.add(type.toString()); + } + + if (name != null) { + sqlBuilder.append(" AND scl_file.name ILIKE ?"); + parameters.add("%" + name + "%"); + } + + if (author != null) { + sqlBuilder.append(" AND scl_file.author = ?"); + parameters.add(author); + } + + if (from != null) { + sqlBuilder.append(" AND scl_file.changedAt >= ?"); + parameters.add(from); + } + + if (to != null) { + sqlBuilder.append(" AND scl_file.changedAt <= ?"); + parameters.add(to); + } + + sqlBuilder.append(System.lineSeparator()); + sqlBuilder.append(""" + ORDER BY + scl_file.id ASC, + scl_file.major_version DESC, + scl_file.minor_version DESC, + scl_file.patch_version DESC + """); + + return executeHistoryQuery(sqlBuilder.toString(), parameters); + } + + @Override + @Transactional(SUPPORTS) + public List listHistoryVersionsByUUID(UUID id) { + var sql = """ + select * + from scl_file + where scl_file.id = ? + order by + scl_file.major_version, + scl_file.minor_version, + scl_file.patch_version + """; + + return executeHistoryQuery(sql, Collections.singletonList(id)); + } + private String createVersion(ResultSet resultSet) throws SQLException { var version = new Version(resultSet.getInt(MAJOR_VERSION_FIELD), resultSet.getInt(MINOR_VERSION_FIELD), @@ -364,4 +505,43 @@ private List createLabelList(Array sqlArray) throws SQLException { } return labelsList; } + + private List executeHistoryQuery(String sql, List parameters) { + List items = new ArrayList<>(); + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql); + ) { + for (int i = 0; i < parameters.size(); i++) { + stmt.setObject(i + 1, parameters.get(i)); + } + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + items.add(mapResultSetToHistoryMetaItem(resultSet)); + } + } + } catch (SQLException exp) { + throw new CompasSclDataServiceException(POSTGRES_SELECT_ERROR_CODE, "Error listing scl entries from database!", exp); + } + return items; + } + + private HistoryMetaItem mapResultSetToHistoryMetaItem(ResultSet resultSet) throws SQLException { + return new HistoryMetaItem( + resultSet.getString(ID_FIELD), + resultSet.getString(NAME_FIELD), + createVersion(resultSet), + resultSet.getString(HISTORYMETAITEM_TYPE_FIELD), + resultSet.getString(HISTORYMETAITEM_AUTHOR_FIELD), + resultSet.getString(HISTORYMETAITEM_COMMENT_FIELD), + convertToOffsetDateTime(resultSet.getTimestamp(HISTORYMETAITEM_CHANGEDAT_FIELD)), + resultSet.getBoolean(HISTORYMETAITEM_IS_DELETED_FIELD) + ); + } + + private static OffsetDateTime convertToOffsetDateTime(Timestamp sqlTimestamp) { + return sqlTimestamp != null + ? sqlTimestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() + : null; + } } diff --git a/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__add_is_deleted_flag.sql b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__add_is_deleted_flag.sql new file mode 100644 index 00000000..216ecf0a --- /dev/null +++ b/repository-postgresql/src/main/resources/org/lfenergy/compas/scl/data/repository/postgresql/db/migration/V1_5__add_is_deleted_flag.sql @@ -0,0 +1,14 @@ +/** + * SPDX-FileCopyrightText: 2021 Alliander N.V. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +-- +-- Update SCL File Table to add soft deletion +-- + +ALTER TABLE scl_file +ADD COLUMN is_deleted BOOLEAN DEFAULT false; + +comment on column scl_file.is_deleted is 'Flag is the SCL File is deleted.'; 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..5cac5b3e --- /dev/null +++ b/repository/src/main/java/org/lfenergy/compas/scl/data/model/IHistoryMetaItem.java @@ -0,0 +1,16 @@ +package org.lfenergy.compas.scl.data.model; + +import java.time.OffsetDateTime; + +public interface IHistoryMetaItem extends IAbstractItem { + + String getType(); + + String getAuthor(); + + String getComment(); + + OffsetDateTime getChangedAt(); + + boolean isDeleted(); +} diff --git a/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java b/repository/src/main/java/org/lfenergy/compas/scl/data/repository/CompasSclDataRepository.java index 4535fdda..a8e5d8f8 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. * @@ -91,7 +101,7 @@ public interface CompasSclDataRepository { * @param type The type of SCL where to find the SCL File * @param id The ID of the SCL File to delete. */ - void delete(SclFileType type, UUID id); + void delete(SclFileType type, UUID id, boolean softDelete); /** * Delete passed versions for a specific SCL File using its ID. @@ -100,5 +110,27 @@ public interface CompasSclDataRepository { * @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 delete(SclFileType type, UUID id, Version version, boolean softDelete); + + /** + * 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(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); } diff --git a/service/pom.xml b/service/pom.xml index a9d11023..43b2f67b 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -49,6 +49,10 @@ SPDX-License-Identifier: Apache-2.0 log4j-core provided + + org.eclipse.microprofile.config + microprofile-config-api + 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..e780bdfe 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 @@ -43,13 +43,15 @@ public class CompasSclDataService { private final CompasSclDataRepository repository; private final ElementConverter converter; private final SclElementProcessor sclElementProcessor; + private final FeatureFlagsConfiguration featureFlagsConfiguration; @Inject public CompasSclDataService(CompasSclDataRepository repository, ElementConverter converter, - SclElementProcessor sclElementProcessor) { + SclElementProcessor sclElementProcessor, FeatureFlagsConfiguration featureFlagsConfiguration) { this.repository = repository; this.converter = converter; this.sclElementProcessor = sclElementProcessor; + this.featureFlagsConfiguration = featureFlagsConfiguration; } /** @@ -215,7 +217,7 @@ public String update(SclFileType type, UUID id, ChangeSetType changeSetType, Str */ @Transactional(REQUIRED) public void delete(SclFileType type, UUID id) { - repository.delete(type, id); + repository.delete(type, id, featureFlagsConfiguration.isSoftDeleteEnabled()); } /** @@ -227,7 +229,7 @@ public void delete(SclFileType type, UUID id) { */ @Transactional(REQUIRED) public void delete(SclFileType type, UUID id, Version version) { - repository.delete(type, id, version); + repository.delete(type, id, version, featureFlagsConfiguration.isSoftDeleteEnabled()); } /** diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclHistoryService.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclHistoryService.java new file mode 100644 index 00000000..36e844f4 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/CompasSclHistoryService.java @@ -0,0 +1,68 @@ +package org.lfenergy.compas.scl.data.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.lfenergy.compas.scl.data.model.IHistoryMetaItem; +import org.lfenergy.compas.scl.data.model.Version; +import org.lfenergy.compas.scl.data.repository.CompasSclDataRepository; +import org.lfenergy.compas.scl.extensions.model.SclFileType; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import static jakarta.transaction.Transactional.TxType.SUPPORTS; + +@ApplicationScoped +public class CompasSclHistoryService { + + private final CompasSclDataRepository repository; + + @Inject + public CompasSclHistoryService(CompasSclDataRepository repository) { + this.repository = repository; + } + + /** + * List the latest version of all SCL File history entries. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistory() { + return repository.listHistory(); + } + + /** + * 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 findFileByIdAndVersion(UUID id, Version version) { + return repository.findByUUID(id, version); + } + + /** + * List the history entries of an SCL File specified by an uuid. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List listHistoryVersionsForResource(UUID id) { + return repository.listHistoryVersionsByUUID(id); + } + + /** + * List the latest version of all SCL File history entries. + * + * @return The List of Items found. + */ + @Transactional(SUPPORTS) + public List searchResourcesHistoryVersions(SclFileType type, String name, String author, OffsetDateTime from, OffsetDateTime to) { + return repository.listHistory(type, name, author, from, to); + } +} diff --git a/service/src/main/java/org/lfenergy/compas/scl/data/service/FeatureFlagsConfiguration.java b/service/src/main/java/org/lfenergy/compas/scl/data/service/FeatureFlagsConfiguration.java new file mode 100644 index 00000000..444da518 --- /dev/null +++ b/service/src/main/java/org/lfenergy/compas/scl/data/service/FeatureFlagsConfiguration.java @@ -0,0 +1,15 @@ +package org.lfenergy.compas.scl.data.service; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class FeatureFlagsConfiguration { + + @ConfigProperty(name = "compas.scl-data-service.features.soft-delete-enabled", defaultValue = "false") + boolean softDeleteEnabled; + + public boolean isSoftDeleteEnabled() { + return softDeleteEnabled; + } +} 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..30deaccb 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 @@ -48,10 +48,11 @@ class CompasSclDataServiceTest { private final ElementConverter converter = new ElementConverter(); private final SclElementProcessor processor = new SclElementProcessor(); + private final FeatureFlagsConfiguration featureFlagsConfiguration = new FeatureFlagsConfiguration(); @BeforeEach void beforeEach() { - compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor); + compasSclDataService = new CompasSclDataService(compasSclDataRepository, converter, processor, featureFlagsConfiguration); } @Test @@ -304,11 +305,12 @@ void update_WhenCalledWithXMLStringWithoutSCL_ThenCompasExceptionThrown() { void delete_WhenCalledWithoutVersion_ThenRepositoryIsCalled() { var uuid = UUID.randomUUID(); - doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid); + doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, Boolean.FALSE); + featureFlagsConfiguration.softDeleteEnabled = Boolean.FALSE; compasSclDataService.delete(SCL_TYPE, uuid); - verify(compasSclDataRepository).delete(SCL_TYPE, uuid); + verify(compasSclDataRepository).delete(SCL_TYPE, uuid, Boolean.FALSE); } @Test @@ -316,11 +318,37 @@ void delete_WhenCalledWithVersion_ThenRepositoryIsCalled() { var uuid = UUID.randomUUID(); var version = new Version(1, 0, 0); - doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, version); + doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, version, Boolean.FALSE); + featureFlagsConfiguration.softDeleteEnabled = Boolean.FALSE; compasSclDataService.delete(SCL_TYPE, uuid, version); - verify(compasSclDataRepository).delete(SCL_TYPE, uuid, version); + verify(compasSclDataRepository).delete(SCL_TYPE, uuid, version, Boolean.FALSE); + } + + @Test + void deleteWithSoftDelete_WhenCalledWithoutVersion_ThenRepositoryIsCalled() { + var uuid = UUID.randomUUID(); + + doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, Boolean.TRUE); + + featureFlagsConfiguration.softDeleteEnabled = Boolean.TRUE; + compasSclDataService.delete(SCL_TYPE, uuid); + + verify(compasSclDataRepository).delete(SCL_TYPE, uuid, Boolean.TRUE); + } + + @Test + void deleteWithSoftDelete_WhenCalledWithVersion_ThenRepositoryIsCalled() { + var uuid = UUID.randomUUID(); + var version = new Version(1, 0, 0); + + doNothing().when(compasSclDataRepository).delete(SCL_TYPE, uuid, version, Boolean.TRUE); + + featureFlagsConfiguration.softDeleteEnabled = Boolean.TRUE; + compasSclDataService.delete(SCL_TYPE, uuid, version); + + verify(compasSclDataRepository).delete(SCL_TYPE, uuid, version, Boolean.TRUE); } @Test