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 535b9847..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
@@ -373,6 +376,116 @@ public void delete(SclFileType type, UUID id, Version version, boolean softDelet
}
}
+ @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